Skip to content

Commit 6b2554d

Browse files
bors[bot]tomeon
andauthored
Merge #243
243: feat(entrypoint): fixes/improvements to support `nix run` (devshell-as-flake-app) r=zimbatm a=tomeon Co-authored-by: Matt Schreiber <[email protected]>
2 parents 5143ea6 + edca0bb commit 6b2554d

File tree

6 files changed

+244
-25
lines changed

6 files changed

+244
-25
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,26 @@ development dependencies is as easy as:
102102
```sh
103103
nix-build shell.nix | cachix push <mycache>
104104
```
105+
106+
### Runnable as a Nix application
107+
108+
Devshells are runnable as Nix applications (via `nix run`). This makes it
109+
possible to run commands defined in your devshell without entering a
110+
`nix-shell` or `nix develop` session:
111+
112+
```sh
113+
nix run '.#<myapp>' -- <devshell-command> <and-args>
114+
```
115+
116+
This project itself exposes a Nix application; you can try it out with:
117+
118+
119+
```sh
120+
nix run 'github:numtide/devshell' -- hello
121+
```
122+
123+
See [here](docs/flake-app.md) for how to export your devshell as a flake app.
124+
105125
## TODO
106126

107127
A lot of things!

docs/flake-app.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Using a devshell as a Nix application
2+
3+
Devshells provide the attribute `flakeApp`, which contains an attribute set
4+
suitable for use as an entry in the `apps` flake output structure. Export this
5+
attribute under `apps.<system>.<myapp>`, and then you can run commands within
6+
your devshell as follows:
7+
8+
```sh
9+
nix run '.#<myapp>' -- <devshell-command> <and-args>
10+
```
11+
12+
For example, given the following `flake.nix`:
13+
14+
```nix
15+
{
16+
inputs.devshell.url = "github:numtide/devshell";
17+
inputs.flake-utils.url = "github:numtide/flake-utils";
18+
19+
outputs = { self, flake-utils, devshell, nixpkgs }:
20+
flake-utils.lib.eachDefaultSystem (system: {
21+
apps.devshell = self.outputs.devShells.${system}.default.flakeApp;
22+
23+
devShells.default =
24+
let
25+
pkgs = import nixpkgs {
26+
inherit system;
27+
28+
overlays = [ devshell.overlays.default ];
29+
};
30+
in
31+
pkgs.devshell.mkShell ({ config, ... }: {
32+
commands = [
33+
{
34+
name = "greet";
35+
command = ''
36+
printf -- 'Hello, %s!\n' "''${1:-world}"
37+
'';
38+
}
39+
];
40+
});
41+
});
42+
}
43+
```
44+
45+
You can execute your devshell's `greet` command like this:
46+
47+
```console
48+
$ nix run '.#devshell' -- greet myself
49+
Hello, myself!
50+
```
51+
52+
## Setting `PRJ_ROOT`
53+
54+
By default, the `PRJ_ROOT` environment variable is set to the value of the
55+
`PWD` environment variable. You can override this by defining `PRJ_ROOT` in
56+
`nix run`'s environment:
57+
58+
```sh
59+
PRJ_ROOT=/some/where/else nix run '.#<myapp>' -- <devshell-command> <and-args>
60+
```
61+
62+
You can also use the `--prj-root` option:
63+
64+
```sh
65+
nix run '.#<myapp>' -- --prj-root /yet/another/path -- <devshell-command> <and-args>
66+
```

flake.nix

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@
1818
}
1919
);
2020

21-
devShells = eachSystem (system: {
22-
default = self.legacyPackages.${system}.fromTOML ./devshell.toml;
23-
});
24-
2521
templates = rec {
2622
toml = {
2723
path = ./templates/toml;
@@ -33,6 +29,15 @@
3329
};
3430
default = toml;
3531
};
32+
33+
devShells = eachSystem (system: {
34+
default = self.legacyPackages.${system}.fromTOML ./devshell.toml;
35+
});
36+
37+
apps = eachSystem (system: {
38+
default = self.devShells.${system}.default.flakeApp;
39+
});
40+
3641
# Import this overlay into your instance of nixpkgs
3742
overlays.default = import ./overlay.nix;
3843
lib = {

modules/devshell.nix

Lines changed: 105 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{ config, lib, pkgs, ... }:
1+
{ config, lib, pkgs, options, ... }:
22
with lib;
33
let
44
cfg = config.devshell;
@@ -18,11 +18,11 @@ let
1818
program = "${bin}";
1919
};
2020

21-
mkSetupHook = entrypoint:
21+
mkSetupHook = rc:
2222
pkgs.stdenvNoCC.mkDerivation {
2323
name = "devshell-setup-hook";
2424
setupHook = pkgs.writeText "devshell-setup-hook.sh" ''
25-
source ${devshell_dir}/env.bash
25+
source ${rc}
2626
'';
2727
dontUnpack = true;
2828
dontBuild = true;
@@ -60,12 +60,18 @@ let
6060
envBash = pkgs.writeText "devshell-env.bash" ''
6161
if [[ -n ''${IN_NIX_SHELL:-} || ''${DIRENV_IN_ENVRC:-} = 1 ]]; then
6262
# We know that PWD is always the current directory in these contexts
63-
export PRJ_ROOT=$PWD
63+
PRJ_ROOT=$PWD
6464
elif [[ -z ''${PRJ_ROOT:-} ]]; then
65-
echo "ERROR: please set the PRJ_ROOT env var to point to the project root" >&2
66-
return 1
65+
${lib.optionalString (cfg.prj_root_fallback != null) cfg.prj_root_fallback}
66+
67+
if [[ -z "''${PRJ_ROOT:-}" ]]; then
68+
echo "ERROR: please set the PRJ_ROOT env var to point to the project root" >&2
69+
return 1
70+
fi
6771
fi
6872
73+
export PRJ_ROOT
74+
6975
# Expose the folder that contains the assembled environment.
7076
export DEVSHELL_DIR=@DEVSHELL_DIR@
7177
@@ -96,37 +102,87 @@ let
96102
97103
# If the file is sourced, skip all of the rest and just source the env
98104
# script.
99-
if [[ $0 != "''${BASH_SOURCE[0]}" ]]; then
105+
if (return 0) &>/dev/null; then
100106
source "$DEVSHELL_DIR/env.bash"
101107
return
102108
fi
103109
104110
# Be strict!
105111
set -euo pipefail
106112
107-
if [[ $# = 0 ]]; then
108-
# Start an interactive shell
109-
exec "${bashPath}" --rcfile "$DEVSHELL_DIR/env.bash" --noprofile
110-
elif [[ $1 == "-h" || $1 == "--help" ]]; then
113+
while (( "$#" > 0 )); do
114+
case "$1" in
115+
-h|--help)
116+
help=1
117+
;;
118+
--pure)
119+
pure=1
120+
;;
121+
--prj-root)
122+
if (( "$#" < 2 )); then
123+
echo 1>&2 '${cfg.name}: missing required argument to --prj-root'
124+
exit 1
125+
fi
126+
127+
PRJ_ROOT="$2"
128+
129+
shift
130+
;;
131+
--env-bin)
132+
if (( "$#" < 2 )); then
133+
echo 1>&2 '${cfg.name}: missing required argument to --env-bin'
134+
exit 1
135+
fi
136+
137+
env_bin="$2"
138+
139+
shift
140+
;;
141+
--)
142+
shift
143+
break
144+
;;
145+
*)
146+
break
147+
;;
148+
esac
149+
150+
shift
151+
done
152+
153+
if [[ -n "''${help:-}" ]]; then
111154
cat <<USAGE
112155
Usage: ${cfg.name}
113156
$0 -h | --help # show this help
114157
$0 [--pure] # start a bash sub-shell
115158
$0 [--pure] <cmd> [...] # run a command in the environment
116159
117160
Options:
118-
* --pure : execute the script in a clean environment
161+
* --pure : execute the script in a clean environment
162+
* --prj-root <path> : set the project root (\$PRJ_ROOT)
163+
* --env-bin <path> : path to the env executable (default: /usr/bin/env)
119164
USAGE
120165
exit
121-
elif [[ $1 == "--pure" ]]; then
122-
# re-execute the script in a clean environment
123-
shift
124-
exec /usr/bin/env -i -- "HOME=$HOME" "PRJ_ROOT=$PRJ_ROOT" "$0" "$@"
166+
fi
167+
168+
if (( "$#" == 0 )); then
169+
# Start an interactive shell
170+
set -- ${lib.escapeShellArg bashPath} --rcfile "$DEVSHELL_DIR/env.bash" --noprofile
171+
fi
172+
173+
if [[ -n "''${pure:-}" ]]; then
174+
# re-execute the script in a clean environment.
175+
# note that the `--` in between `"$0"` and `"$@"` will immediately
176+
# short-circuit options processing on the second pass through this
177+
# script, in case we get something like:
178+
# <entrypoint> --pure -- --pure <cmd>
179+
set -- "''${env_bin:-/usr/bin/env}" -i -- ''${HOME:+"HOME=''${HOME:-}"} ''${PRJ_ROOT:+"PRJ_ROOT=''${PRJ_ROOT:-}"} "$0" -- "$@"
125180
else
126181
# Start a script
127182
source "$DEVSHELL_DIR/env.bash"
128-
exec -- "$@"
129183
fi
184+
185+
exec -- "$@"
130186
'';
131187

132188
# Builds the DEVSHELL_DIR with all the dependencies
@@ -240,6 +296,36 @@ in
240296
type = types.package;
241297
description = "TODO";
242298
};
299+
300+
prj_root_fallback = mkOption {
301+
type = let
302+
envType = options.env.type.nestedTypes.elemType;
303+
coerceFunc = value: { inherit value; };
304+
in types.nullOr (types.coercedTo types.nonEmptyStr coerceFunc envType);
305+
apply = x: if x == null then x else x // { name = "PRJ_ROOT"; };
306+
default = { eval = "$PWD"; };
307+
example = lib.literalExpression ''
308+
{
309+
# Use the top-level directory of the working tree
310+
eval = "$(git rev-parse --show-toplevel)";
311+
};
312+
'';
313+
description = ''
314+
If IN_NIX_SHELL is nonempty, or DIRENV_IN_ENVRC is set to '1', then
315+
PRJ_ROOT is set to the value of PWD.
316+
317+
This option specifies the path to use as the value of PRJ_ROOT in case
318+
IN_NIX_SHELL is empty or unset and DIRENV_IN_ENVRC is any value other
319+
than '1'.
320+
321+
Set this to null to force PRJ_ROOT to be defined at runtime (except if
322+
IN_NIX_SHELL or DIRENV_IN_ENVRC are defined as described above).
323+
324+
Otherwise, you can set this to a string representing the desired
325+
default path, or to a submodule of the same type valid in the 'env'
326+
options list (except that the 'name' field is ignored).
327+
'';
328+
};
243329
};
244330

245331
config.devshell = {
@@ -313,8 +399,8 @@ in
313399
profile = cfg.package;
314400
passthru = {
315401
inherit config;
316-
flakeApp = mkFlakeApp entrypoint;
317-
hook = mkSetupHook entrypoint;
402+
flakeApp = mkFlakeApp "${devshell_dir}/entrypoint";
403+
hook = mkSetupHook "${devshell_dir}/env.bash";
318404
inherit (config._module.args) pkgs;
319405
};
320406
};

modules/env.nix

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,17 @@ let
3636
};
3737

3838
unset = mkEnableOption "unsets the variable";
39+
40+
__toString = mkOption {
41+
type = types.functionTo types.str;
42+
internal = true;
43+
readOnly = true;
44+
default = envToBash;
45+
description = "Function used to translate this submodule to Bash code";
46+
};
3947
};
4048

41-
envToBash = { name, value, eval, prefix, unset }@args:
49+
envToBash = { name, value, eval, prefix, unset, ... }@args:
4250
let
4351
vals = filter (key: args.${key} != null && args.${key} != false) [
4452
"eval"
@@ -113,6 +121,6 @@ in
113121
}
114122
];
115123

116-
devshell.startup_env = concatStringsSep "\n" (map envToBash config.env);
124+
devshell.startup_env = concatStringsSep "\n" config.env;
117125
};
118126
}

tests/core/devshell.nix

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,38 @@
4646
# Check that the profile got loaded
4747
assert "$FAKE_PROFILE" == "1"
4848
'';
49+
50+
# Devshell entrypoint script features
51+
devshell-entrypoint-1 =
52+
let
53+
shell = devshell.mkShell {
54+
devshell.name = "devshell-entrypoint-1";
55+
devshell.packages = [ pkgs.git ];
56+
57+
# Force PRJ_ROOT to be defined by caller (possibly via `--prj-root`).
58+
devshell.prj_root_fallback = null;
59+
};
60+
in
61+
runTest "devshell-entrypoint-1" { } ''
62+
entrypoint_clean() {
63+
env -u IN_NIX_SHELL -u PRJ_ROOT ${shell}/entrypoint "$@"
64+
}
65+
66+
# No packages in PATH
67+
! type -p git
68+
69+
# Exits badly if PRJ_ROOT isn't set, or if we cannot assume PRJ_ROOT
70+
# should be PWD.
71+
! msg="$(entrypoint_clean /bin/sh -c 'exit 0' 2>&1)"
72+
assert "$msg" == 'ERROR: please set the PRJ_ROOT env var to point to the project root'
73+
74+
# Succeeds with --prj-root set
75+
entrypoint_clean --prj-root . /bin/sh -c 'exit 0'
76+
77+
# Packages available through entrypoint
78+
entrypoint_clean --prj-root . /bin/sh -c 'type -p git'
79+
80+
# Packages available through entrypoint in pure mode
81+
entrypoint_clean --pure --env-bin env --prj-root . /bin/sh -c 'type -p git'
82+
'';
4983
}

0 commit comments

Comments
 (0)