Skip to content

Commit bd5a69f

Browse files
authored
feat: Support nixpkgs URIs (#13)
Closes #10 Adds the ability to refer to the local nixpkgs URI. This is equivalent to `nix shell nixpkgs#pkg` or `nix-shell -p pkg`.
1 parent c2deae0 commit bd5a69f

8 files changed

Lines changed: 621 additions & 249 deletions

File tree

bin/main.ml

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,34 @@ let main
77
Cli.strategy)
88
=
99
let ({ entrypoint; attribute; subshell_dir } : Cli.target_info) = target_info in
10-
Option.value ~default:(Option.value ~default:(Sys.getcwd ()) entrypoint) subshell_dir
11-
|> Unix.cd;
12-
let cmd =
13-
match entrypoint with
14-
| Some entrypoint ->
15-
(match Unix.flake_exists_at entrypoint with
16-
| true ->
17-
Cmd.builder "nix"
18-
|>+ [ "develop" ] @ [ Uri.sprintf_uri_attr_opt entrypoint attribute ]
19-
|>+ Option.value ~default:[] force_experimental_features
20-
| false ->
21-
(match Unix.shell_exists_at entrypoint with
22-
| true ->
23-
Cmd.builder "nix-shell"
24-
|>+ Option.value
25-
~default:[]
26-
(Option.map (fun attr -> [ "--attr"; attr ]) attribute)
27-
|>+ [ entrypoint ]
28-
| false ->
29-
Error.handle_ns_error "no available devshell entrypoint: %s\n%!" entrypoint))
30-
|>+ [ "--command"; Unix.shell ]
31-
| None ->
32-
Cmd.builder "nix"
33-
|>+ [ "shell" ]
34-
|>+ installables
35-
|>+ Option.value ~default:[] force_experimental_features
10+
let strategy =
11+
let workdir =
12+
Option.value
13+
~default:(Option.value ~default:(Sys.getcwd ()) entrypoint)
14+
subshell_dir
15+
and primary, fallback =
16+
match entrypoint with
17+
| Some entrypoint ->
18+
if Unix.flake_exists_at entrypoint
19+
then
20+
( Cmd.nix_develop entrypoint attribute force_experimental_features
21+
, if Unix.shell_exists_at entrypoint
22+
then Some (Cmd.legacy_nix_shell_from_entrypoint entrypoint attribute)
23+
else None )
24+
else if Unix.shell_exists_at entrypoint
25+
then Cmd.legacy_nix_shell_from_entrypoint entrypoint attribute, None
26+
else Error.handle_ns_error "no available devshell entrypoint: %s\n%!" entrypoint
27+
| None ->
28+
( Cmd.nix_shell
29+
(List.map Uri.uri_to_string installables)
30+
force_experimental_features
31+
, Option.map
32+
(fun installables -> Cmd.legacy_nix_shell_from_installables installables)
33+
(Uri.combine_installables_tr installables) )
34+
in
35+
{ workdir; primary; fallback }
3636
in
37-
if printcmd then print_endline (Cmd.to_string cmd) else ignore (Cmd.run cmd)
37+
if printcmd then print_strategy strategy else execute_strategy strategy
3838
;;
3939

4040
Cli.eval main

lib/cli.ml

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module Cli = struct
1515
(** Processed args that will be consumed by the main function to determine the
1616
behavior of the program. *)
1717
type strategy =
18-
{ installables : string list
18+
{ installables : Uri.uri list
1919
; target_info : target_info
2020
; printcmd : bool
2121
; force_experimental_features : string list option
@@ -25,7 +25,12 @@ module Cli = struct
2525
let uris = Arg.(value & pos_all dirpath [] & info [])
2626

2727
let printcmd =
28-
let doc = "Print the command to stdout instead of executing it." in
28+
let doc =
29+
"Print the command strategy as newline separated values to stdout instead of \
30+
executing it. The first command changes to the working directory, the second is \
31+
the primary nix command and the last is an optional fallback nix command in the \
32+
event that flake/nix-command features fail."
33+
in
2934
Arg.(value & flag & info [ "printcmd" ] ~doc)
3035
;;
3136

@@ -64,7 +69,9 @@ module Cli = struct
6469
(* Two args passed *)
6570
(*******************)
6671
| [ maybe_subshell_dir; target ] ->
67-
(match Uri.parse_target maybe_subshell_dir, Uri.parse_target target with
72+
let maybe_subshell_dir = Uri.parse_target maybe_subshell_dir
73+
and target = Uri.parse_target target in
74+
(match maybe_subshell_dir, target with
6875
(* If maybe_subshell_dir does not have any attributes we know the user wants to change directories. *)
6976
| ( LocalResourceMaybeAttr (subshell_dir, None)
7077
, LocalResourceMaybeAttr (entrypoint, attribute) ) ->
@@ -73,12 +80,10 @@ module Cli = struct
7380
; attribute
7481
; subshell_dir = Some subshell_dir
7582
} )
76-
| LocalResourceMaybeAttr (subshell_dir, None), LocalResourceMultiAttr _ ->
83+
| ( LocalResourceMaybeAttr (subshell_dir, None)
84+
, (LocalResourceMultiAttr _ | RemoteResource _) ) ->
7785
( [ target ]
7886
, { entrypoint = None; attribute = None; subshell_dir = Some subshell_dir } )
79-
| LocalResourceMaybeAttr (subshell_dir, None), RemoteResource uri ->
80-
( [ uri ]
81-
, { entrypoint = None; attribute = None; subshell_dir = Some subshell_dir } )
8287
| _ -> Uri.parse_targets_tr original_args, default_target)
8388
(*****************************)
8489
(* Three or more args passed *)

lib/cmd.ml

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,85 @@
11
(** Functions and operators used in running commands *)
22
module Cmd = struct
3-
open Bos
3+
open Util.Util
44

5-
(** Construct a command builder
6-
7-
Example:
8-
Cmd.builder "nix" |>+ \[ "flake" "show" "--json" \]
9-
*)
10-
let builder cmd = Cmd.v cmd
5+
type strategy =
6+
{ workdir : string
7+
; primary : Bos.Cmd.t
8+
; fallback : Bos.Cmd.t option
9+
}
1110

1211
(** Pipe-arg operator, used in adding arguments to a command *)
1312
let ( |>+ ) cmd = function
1413
| [] -> cmd
15-
| [ arg ] -> Cmd.add_arg cmd arg
16-
| args -> List.fold_left (fun cmd arg -> Cmd.add_arg cmd arg) cmd args
14+
| [ arg ] -> Bos.Cmd.add_arg cmd arg
15+
| args -> List.fold_left (fun cmd arg -> Bos.Cmd.add_arg cmd arg) cmd args
16+
;;
17+
18+
let nix_develop entrypoint attribute force_experimental_features =
19+
Bos.Cmd.v "nix"
20+
|>+ [ "develop" ] @ [ Uri.sprintf_uri_attr_opt entrypoint attribute ]
21+
|>+ Option.value ~default:[] force_experimental_features
22+
|>+ [ "--command"; Unix.shell ]
23+
;;
24+
25+
let legacy_nix_shell_from_entrypoint entrypoint attribute =
26+
Bos.Cmd.v "nix-shell"
27+
|>+ Option.value ~default:[] (Option.map (fun attr -> [ "--attr"; attr ]) attribute)
28+
|>+ [ entrypoint ]
29+
|>+ [ "--command"; Unix.shell ]
1730
;;
1831

19-
(** Convert a command into a string *)
20-
let to_string cmd = Cmd.to_string cmd
32+
let nix_shell installables force_experimental_features =
33+
Bos.Cmd.v "nix"
34+
|>+ [ "shell" ]
35+
|>+ installables
36+
|>+ Option.value ~default:[] force_experimental_features
37+
;;
2138

22-
(** Run a command
39+
let legacy_nix_shell_from_installables installables =
40+
(* Need to combine/validate that the installables given are all Nixpkgs *)
41+
Bos.Cmd.v "nix-shell"
42+
|>+ [ "--packages" ] @ installables
43+
|>+ [ "--command"; Unix.shell ]
44+
;;
2345

24-
Example:
25-
Cmd.run (Cmd.builder "nix" |>+ \[ "show-config" "--json" \])
26-
*)
27-
let run cmd = OS.Cmd.run cmd
46+
let print_strategy ({ workdir; primary; fallback } : strategy) =
47+
print_string
48+
(Printf.sprintf
49+
"'cd' '%s'\n%s\n%s"
50+
workdir
51+
(Bos.Cmd.to_string primary)
52+
(Option.value ~default:"" (Option.map Bos.Cmd.to_string fallback)))
53+
;;
54+
55+
let execute_strategy ({ workdir; primary; fallback } : strategy) =
56+
Unix.cd workdir;
57+
match Bos.OS.Cmd.run_status primary with
58+
| Ok (`Exited 0) -> () (* success, nothing else to do *)
59+
| Ok (`Exited code | `Signaled code) ->
60+
(* primary failed *)
61+
(match fallback with
62+
| Some fallback ->
63+
(* show primary error but don't exit *)
64+
Printf.eprintf "primary command failed (exit %d), trying fallback\n%!" code;
65+
(match Bos.OS.Cmd.run_status fallback with
66+
| Ok (`Exited 0) -> ()
67+
| Ok (`Exited code | `Signaled code) -> exit code
68+
| Error (`Msg msg) ->
69+
prerr_endline msg;
70+
exit 1)
71+
| None -> exit code)
72+
| Error (`Msg msg) ->
73+
(* primary couldn't even be spawned *)
74+
prerr_endline msg;
75+
(match fallback with
76+
| Some fallback ->
77+
(match Bos.OS.Cmd.run_status fallback with
78+
| Ok (`Exited 0) -> ()
79+
| Ok (`Exited code | `Signaled code) -> exit code
80+
| Error (`Msg msg) ->
81+
prerr_endline msg;
82+
exit 1)
83+
| None -> exit 1)
84+
;;
2885
end

lib/util.ml

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ module Util = struct
4040
module Uri = struct
4141
(* *)
4242

43+
(** A local reference to nixpkgs, eg. {nixpkgs#...} or {pkgs#...} (which resolves to the former) *)
44+
type nixpkgs =
45+
{ uri : string
46+
; installables : string list
47+
}
48+
4349
(** URI variants, like {/path/to/flake#drv} or {github:NixOS/nixpkgs#drv} *)
4450
type uri =
4551
(* A path with up to one attribute, eg. /path/to/resouce or /path/to/resource#... *)
@@ -48,6 +54,8 @@ module Util = struct
4854
| LocalResourceMultiAttr of string * string
4955
(* A remote resource, eg. github:NixOS/nixpkgs *)
5056
| RemoteResource of string
57+
(* A local reference to nixpkgs, eg. nixpkgs#... or pkgs#... (which resolves to the former) *)
58+
| Nixpkgs of nixpkgs
5159

5260
(** Format a uri and attribute into a string *)
5361
let sprintf_uri_attr path attr = Printf.sprintf "%s#%s" path attr
@@ -65,17 +73,38 @@ module Util = struct
6573
| LocalResourceMaybeAttr (path, attr_opt) -> sprintf_uri_attr_opt path attr_opt
6674
| LocalResourceMultiAttr (path, attr) -> sprintf_uri_attr path attr
6775
| RemoteResource uri -> uri
76+
| Nixpkgs { uri; _ } -> uri
6877
;;
6978

7079
(** Parse target argument for optional attribute selection *)
7180
let parse_target target =
7281
let parse_uri uri attr_opt =
82+
let is_multiattr maybe_multiattr =
83+
String.starts_with ~prefix:"{" maybe_multiattr
84+
&& String.ends_with ~suffix:"}" maybe_multiattr
85+
in
7386
match String.split_on_char ':' uri with
74-
| [ path ] ->
75-
let path = Unix.realpath path
76-
and is_multiattr maybe_multiattr =
77-
String.starts_with ~prefix:"{" maybe_multiattr
87+
| [ "nixpkgs" ] | [ "pkgs" ] ->
88+
let parse_installables attr_opt =
89+
match attr_opt with
90+
| Some attr ->
91+
if is_multiattr attr
92+
then (
93+
match String.split_on_char '{' attr with
94+
| [ _; rhs ] ->
95+
(match String.split_on_char '}' rhs with
96+
| [ lhs; _ ] -> String.split_on_char ',' lhs
97+
| _ -> [])
98+
| _ -> [])
99+
else [ attr ]
100+
| None -> []
78101
in
102+
Nixpkgs
103+
{ uri = sprintf_uri_attr_opt "nixpkgs" attr_opt
104+
; installables = parse_installables attr_opt
105+
}
106+
| [ path ] ->
107+
let path = Unix.realpath path in
79108
(match attr_opt with
80109
| Some attr ->
81110
if is_multiattr attr
@@ -91,15 +120,24 @@ module Util = struct
91120
Error.handle_ns_error "invalid uri or attribute selection syntax: %s\n%!" target
92121
;;
93122

94-
(** Parse target arguments recursively, returning a list of strings.
123+
(** Parse target arguments recursively, returning a list of {!type:uri}.
95124
This function is tail recursive optimized. *)
96125
let parse_targets_tr targets =
97126
let rec parse_targets acc = function
98127
| [] -> acc
99-
| target :: remaining ->
100-
parse_targets (uri_to_string (parse_target target) :: acc) remaining
128+
| target :: remaining -> parse_targets (parse_target target :: acc) remaining
101129
in
102130
parse_targets [] targets
103131
;;
132+
133+
let combine_installables_tr installables =
134+
let rec combine_installables acc = function
135+
| [] -> Some acc
136+
| Nixpkgs { installables; _ } :: remaining ->
137+
combine_installables (acc @ installables) remaining
138+
| _ -> None
139+
in
140+
combine_installables [] installables
141+
;;
104142
end
105143
end

test/dune

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
(test
44
(name test_ns)
5-
(deps test_ns src/flake/flake.nix src/shell/shell.nix)
5+
(deps
6+
test_ns
7+
src/flake/flake.nix
8+
src/shell/shell.nix
9+
src/flake_shell/flake.nix
10+
src/flake_shell/shell.nix)
611
(flags (:standard)))
712

813
;; Rule to compile the Rust test file into a binary

test/src/flake_shell/flake.nix

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
inputs.nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
3+
outputs = { self, nixpkgs }:
4+
let
5+
inherit (nixpkgs.lib) genAttrs;
6+
inherit (nixpkgs.lib.systems) flakeExposed;
7+
eachSystem = f: genAttrs flakeExposed (system: f (import nixpkgs { inherit system; }));
8+
in
9+
{
10+
packages = eachSystem (pkgs: { default = pkgs.hello; });
11+
};
12+
}

test/src/flake_shell/shell.nix

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{ pkgs ? import <nixpkgs> { } }:
2+
pkgs.mkShell {
3+
buildInputs = builtins.attrValues {
4+
inherit (pkgs) hello;
5+
};
6+
}

0 commit comments

Comments
 (0)