Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,26 @@ development dependencies is as easy as:
```sh
nix-build shell.nix | cachix push <mycache>
```

### Runnable as a Nix application

Devshells are runnable as Nix applications (via `nix run`). This makes it
possible to run commands defined in your devshell without entering a
`nix-shell` or `nix develop` session:

```sh
nix run '.#<myapp>' -- <devshell-command> <and-args>
```

This project itself exposes a Nix application; you can try it out with:


```sh
nix run 'github:numtide/devshell' -- hello
```

See [here](docs/flake-app.md) for how to export your devshell as a flake app.

## TODO

A lot of things!
Expand Down
66 changes: 66 additions & 0 deletions docs/flake-app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Using a devshell as a Nix application

Devshells provide the attribute `flakeApp`, which contains an attribute set
suitable for use as an entry in the `apps` flake output structure. Export this
attribute under `apps.<system>.<myapp>`, and then you can run commands within
your devshell as follows:

```sh
nix run '.#<myapp>' -- <devshell-command> <and-args>
```

For example, given the following `flake.nix`:

```nix
{
inputs.devshell.url = "github:numtide/devshell";
inputs.flake-utils.url = "github:numtide/flake-utils";

outputs = { self, flake-utils, devshell, nixpkgs }:
flake-utils.lib.eachDefaultSystem (system: {
apps.devshell = self.outputs.devShells.${system}.default.flakeApp;

devShells.default =
let
pkgs = import nixpkgs {
inherit system;

overlays = [ devshell.overlays.default ];
};
in
pkgs.devshell.mkShell ({ config, ... }: {
commands = [
{
name = "greet";
command = ''
printf -- 'Hello, %s!\n' "''${1:-world}"
'';
}
];
});
});
}
```

You can execute your devshell's `greet` command like this:

```console
$ nix run '.#devshell' -- greet myself
Hello, myself!
```

## Setting `PRJ_ROOT`

By default, the `PRJ_ROOT` environment variable is set to the value of the
`PWD` environment variable. You can override this by defining `PRJ_ROOT` in
`nix run`'s environment:

```sh
PRJ_ROOT=/some/where/else nix run '.#<myapp>' -- <devshell-command> <and-args>
```

You can also use the `--prj-root` option:

```sh
nix run '.#<myapp>' -- --prj-root /yet/another/path -- <devshell-command> <and-args>
```
13 changes: 9 additions & 4 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@
}
);

devShells = eachSystem (system: {
default = self.legacyPackages.${system}.fromTOML ./devshell.toml;
});

templates = rec {
toml = {
path = ./templates/toml;
Expand All @@ -33,6 +29,15 @@
};
default = toml;
};

devShells = eachSystem (system: {
default = self.legacyPackages.${system}.fromTOML ./devshell.toml;
});

apps = eachSystem (system: {
default = self.devShells.${system}.default.flakeApp;
});

# Import this overlay into your instance of nixpkgs
overlays.default = import ./overlay.nix;
lib = {
Expand Down
124 changes: 105 additions & 19 deletions modules/devshell.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{ config, lib, pkgs, ... }:
{ config, lib, pkgs, options, ... }:
with lib;
let
cfg = config.devshell;
Expand All @@ -18,11 +18,11 @@ let
program = "${bin}";
};

mkSetupHook = entrypoint:
mkSetupHook = rc:
pkgs.stdenvNoCC.mkDerivation {
name = "devshell-setup-hook";
setupHook = pkgs.writeText "devshell-setup-hook.sh" ''
source ${devshell_dir}/env.bash
source ${rc}
'';
dontUnpack = true;
dontBuild = true;
Expand Down Expand Up @@ -60,12 +60,18 @@ let
envBash = pkgs.writeText "devshell-env.bash" ''
if [[ -n ''${IN_NIX_SHELL:-} || ''${DIRENV_IN_ENVRC:-} = 1 ]]; then
# We know that PWD is always the current directory in these contexts
export PRJ_ROOT=$PWD
PRJ_ROOT=$PWD
elif [[ -z ''${PRJ_ROOT:-} ]]; then
echo "ERROR: please set the PRJ_ROOT env var to point to the project root" >&2
return 1
${lib.optionalString (cfg.prj_root_fallback != null) cfg.prj_root_fallback}

if [[ -z "''${PRJ_ROOT:-}" ]]; then
echo "ERROR: please set the PRJ_ROOT env var to point to the project root" >&2
return 1
fi
fi

export PRJ_ROOT

# Expose the folder that contains the assembled environment.
export DEVSHELL_DIR=@DEVSHELL_DIR@

Expand Down Expand Up @@ -96,37 +102,87 @@ let

# If the file is sourced, skip all of the rest and just source the env
# script.
if [[ $0 != "''${BASH_SOURCE[0]}" ]]; then
if (return 0) &>/dev/null; then
source "$DEVSHELL_DIR/env.bash"
return
fi

# Be strict!
set -euo pipefail

if [[ $# = 0 ]]; then
# Start an interactive shell
exec "${bashPath}" --rcfile "$DEVSHELL_DIR/env.bash" --noprofile
elif [[ $1 == "-h" || $1 == "--help" ]]; then
while (( "$#" > 0 )); do
case "$1" in
-h|--help)
help=1
;;
--pure)
pure=1
;;
--prj-root)
if (( "$#" < 2 )); then
echo 1>&2 '${cfg.name}: missing required argument to --prj-root'
exit 1
fi

PRJ_ROOT="$2"

shift
;;
--env-bin)
if (( "$#" < 2 )); then
echo 1>&2 '${cfg.name}: missing required argument to --env-bin'
exit 1
fi

env_bin="$2"

shift
;;
--)
shift
break
;;
*)
break
;;
esac

shift
done

if [[ -n "''${help:-}" ]]; then
cat <<USAGE
Usage: ${cfg.name}
$0 -h | --help # show this help
$0 [--pure] # start a bash sub-shell
$0 [--pure] <cmd> [...] # run a command in the environment

Options:
* --pure : execute the script in a clean environment
* --pure : execute the script in a clean environment
* --prj-root <path> : set the project root (\$PRJ_ROOT)
* --env-bin <path> : path to the env executable (default: /usr/bin/env)
USAGE
exit
elif [[ $1 == "--pure" ]]; then
# re-execute the script in a clean environment
shift
exec /usr/bin/env -i -- "HOME=$HOME" "PRJ_ROOT=$PRJ_ROOT" "$0" "$@"
fi

if (( "$#" == 0 )); then
# Start an interactive shell
set -- ${lib.escapeShellArg bashPath} --rcfile "$DEVSHELL_DIR/env.bash" --noprofile
fi

if [[ -n "''${pure:-}" ]]; then
# re-execute the script in a clean environment.
# note that the `--` in between `"$0"` and `"$@"` will immediately
# short-circuit options processing on the second pass through this
# script, in case we get something like:
# <entrypoint> --pure -- --pure <cmd>
set -- "''${env_bin:-/usr/bin/env}" -i -- ''${HOME:+"HOME=''${HOME:-}"} ''${PRJ_ROOT:+"PRJ_ROOT=''${PRJ_ROOT:-}"} "$0" -- "$@"
else
# Start a script
source "$DEVSHELL_DIR/env.bash"
exec -- "$@"
fi

exec -- "$@"
'';

# Builds the DEVSHELL_DIR with all the dependencies
Expand Down Expand Up @@ -240,6 +296,36 @@ in
type = types.package;
description = "TODO";
};

prj_root_fallback = mkOption {
type = let
envType = options.env.type.nestedTypes.elemType;
coerceFunc = value: { inherit value; };
in types.nullOr (types.coercedTo types.nonEmptyStr coerceFunc envType);
apply = x: if x == null then x else x // { name = "PRJ_ROOT"; };
default = { eval = "$PWD"; };
example = lib.literalExpression ''
{
# Use the top-level directory of the working tree
eval = "$(git rev-parse --show-toplevel)";
};
'';
description = ''
If IN_NIX_SHELL is nonempty, or DIRENV_IN_ENVRC is set to '1', then
PRJ_ROOT is set to the value of PWD.

This option specifies the path to use as the value of PRJ_ROOT in case
IN_NIX_SHELL is empty or unset and DIRENV_IN_ENVRC is any value other
than '1'.

Set this to null to force PRJ_ROOT to be defined at runtime (except if
IN_NIX_SHELL or DIRENV_IN_ENVRC are defined as described above).

Otherwise, you can set this to a string representing the desired
default path, or to a submodule of the same type valid in the 'env'
options list (except that the 'name' field is ignored).
'';
};
};

config.devshell = {
Expand Down Expand Up @@ -313,8 +399,8 @@ in
profile = cfg.package;
passthru = {
inherit config;
flakeApp = mkFlakeApp entrypoint;
hook = mkSetupHook entrypoint;
flakeApp = mkFlakeApp "${devshell_dir}/entrypoint";
hook = mkSetupHook "${devshell_dir}/env.bash";
inherit (config._module.args) pkgs;
};
};
Expand Down
12 changes: 10 additions & 2 deletions modules/env.nix
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,17 @@ let
};

unset = mkEnableOption "unsets the variable";

__toString = mkOption {
type = types.functionTo types.str;
internal = true;
readOnly = true;
default = envToBash;
description = "Function used to translate this submodule to Bash code";
};
};

envToBash = { name, value, eval, prefix, unset }@args:
envToBash = { name, value, eval, prefix, unset, ... }@args:
let
vals = filter (key: args.${key} != null && args.${key} != false) [
"eval"
Expand Down Expand Up @@ -113,6 +121,6 @@ in
}
];

devshell.startup_env = concatStringsSep "\n" (map envToBash config.env);
devshell.startup_env = concatStringsSep "\n" config.env;
};
}
34 changes: 34 additions & 0 deletions tests/core/devshell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,38 @@
# Check that the profile got loaded
assert "$FAKE_PROFILE" == "1"
'';

# Devshell entrypoint script features
devshell-entrypoint-1 =
let
shell = devshell.mkShell {
devshell.name = "devshell-entrypoint-1";
devshell.packages = [ pkgs.git ];

# Force PRJ_ROOT to be defined by caller (possibly via `--prj-root`).
devshell.prj_root_fallback = null;
};
in
runTest "devshell-entrypoint-1" { } ''
entrypoint_clean() {
env -u IN_NIX_SHELL -u PRJ_ROOT ${shell}/entrypoint "$@"
}

# No packages in PATH
! type -p git

# Exits badly if PRJ_ROOT isn't set, or if we cannot assume PRJ_ROOT
# should be PWD.
! msg="$(entrypoint_clean /bin/sh -c 'exit 0' 2>&1)"
assert "$msg" == 'ERROR: please set the PRJ_ROOT env var to point to the project root'

# Succeeds with --prj-root set
entrypoint_clean --prj-root . /bin/sh -c 'exit 0'

# Packages available through entrypoint
entrypoint_clean --prj-root . /bin/sh -c 'type -p git'

# Packages available through entrypoint in pure mode
entrypoint_clean --pure --env-bin env --prj-root . /bin/sh -c 'type -p git'
'';
}