{ config, lib, pkgs, options, ... }: let cfg = config.devshell; sanitizedName = lib.strings.sanitizeDerivationName cfg.name; ansi = import ../nix/ansi.nix; bashBin = "${cfg.bashPackage}/bin"; bashPath = "${cfg.bashPackage}/bin/bash"; # Because we want to be able to push pure JSON-like data into the # environment. strOrPackage = import ../nix/strOrPackage.nix { inherit lib pkgs; }; # Use this to define a flake app for the environment. mkFlakeApp = bin: { type = "app"; program = "${bin}"; }; mkSetupHook = rc: pkgs.stdenvNoCC.mkDerivation { name = "devshell-setup-hook"; setupHook = pkgs.writeText "devshell-setup-hook.sh" '' source ${rc} ''; dontUnpack = true; dontBuild = true; dontInstall = true; }; mkNakedShell = pkgs.callPackage ../nix/mkNakedShell.nix { }; addAttributeName = prefix: lib.mapAttrs ( k: v: v // { text = '' #### ${prefix}.${k} ${v.text} ''; } ); inherit (lib) mkOption mkEnableOption attrNames attrValues textClosureMap replaceStrings types id ; entryOptions = { text = mkOption { type = types.str; description = '' Script to run. ''; }; deps = mkOption { type = types.listOf types.str; default = [ ]; description = '' A list of other steps that this one depends on. ''; }; }; # Write a bash profile to load 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 PRJ_ROOT=$PWD elif [[ -z ''${PRJ_ROOT:-} ]]; then ${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@ # Prepend the PATH with the devshell dir and bash PATH=''${PATH%:/path-not-set} PATH=''${PATH#${bashBin}:} export PATH=$DEVSHELL_DIR/bin:${bashBin}:$PATH ${cfg.startup_env} ${textClosureMap id (addAttributeName "startup" cfg.startup) (attrNames cfg.startup)} # Interactive sessions if [[ $- == *i* ]]; then ${textClosureMap id (addAttributeName "interactive" cfg.interactive) (attrNames cfg.interactive)} fi # Interactive session ''; # This is our entrypoint script. entrypoint = pkgs.writeScript "${cfg.name}-entrypoint" '' #!${bashPath} # Script that sets-up the environment. Can be both sourced or invoked. export DEVSHELL_DIR=@DEVSHELL_DIR@ # If the file is sourced, skip all of the rest and just source the env # script. if (return 0) &>/dev/null; then source "$DEVSHELL_DIR/env.bash" return fi # Be strict! set -euo pipefail 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 ${pkgs.coreutils}/bin/cat < [...] # run a command in the environment Options: * --pure : execute the script in a clean environment * --prj-root : set the project root (\$PRJ_ROOT) * --env-bin : path to the env executable (default: /usr/bin/env) USAGE exit 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: # --pure -- --pure 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" fi exec -- "$@" ''; # Builds the DEVSHELL_DIR with all the dependencies devshell_dir = pkgs.buildEnv rec { name = "${sanitizedName}-dir"; paths = cfg.packages; postBuild = '' substitute ${envBash} $out/env.bash --subst-var-by DEVSHELL_DIR $out substitute ${entrypoint} $out/entrypoint --subst-var-by DEVSHELL_DIR $out chmod +x $out/entrypoint mainProgram="${meta.mainProgram}" # ensure mainProgram doesn't collide if [ -e "$out/bin/$mainProgram" ]; then echo "Warning: Cannot create entry point for this devshell at '\$out/bin/$mainProgram' because an executable with that name already exists." >&2 echo "Set meta.mainProgram to something else than '$mainProgram'." >&2 else # if $out/bin is a single symlink, transform it into a directory tree # (buildEnv does that when there is only one package in the environment) if [ -L "$out/bin" ]; then mv "$out/bin" bin-tmp mkdir "$out/bin" ln -s bin-tmp/* "$out/bin/" fi ln -s $out/entrypoint "$out/bin/$mainProgram" fi ''; meta.mainProgram = config.meta.mainProgram or sanitizedName; }; # Returns a list of all the input derivation ... for a derivation. inputsOf = drv: lib.filter lib.isDerivation ( (drv.buildInputs or [ ]) ++ (drv.nativeBuildInputs or [ ]) ++ (drv.propagatedBuildInputs or [ ]) ++ (drv.propagatedNativeBuildInputs or [ ]) ); in { options.devshell = { bashPackage = mkOption { internal = true; type = strOrPackage; default = pkgs.bashInteractive; defaultText = "pkgs.bashInteractive"; description = "Version of bash to use in the project"; }; package = mkOption { internal = true; type = types.package; description = '' This package contains the DEVSHELL_DIR ''; }; startup = mkOption { type = types.attrsOf (types.submodule { options = entryOptions; }); default = { }; internal = true; description = '' A list of scripts to execute on startup. ''; }; startup_env = mkOption { type = types.str; default = ""; internal = true; description = '' Please ignore. Used by the env module. ''; }; interactive = mkOption { type = types.attrsOf (types.submodule { options = entryOptions; }); default = { }; internal = true; description = '' A list of scripts to execute on interactive startups. ''; }; # TODO: rename motd to something better. motd = mkOption { type = types.str; default = '' {202}🔨 Welcome to ${cfg.name}{reset} $(type -p menu &>/dev/null && menu) ''; apply = replaceStrings (map (key: "{${key}}") (attrNames ansi)) (attrValues ansi); description = '' Message Of The Day. This is the welcome message that is being printed when the user opens the shell. You may use any valid ansi color from the 8-bit ansi color table. For example, to use a green color you would use something like {106}. You may also use {bold}, {italic}, {underline}. Use {reset} to turn off all attributes. ''; }; load_profiles = mkEnableOption "load etc/profiles.d/*.sh in the shell"; name = mkOption { type = types.str; default = "devshell"; description = '' Name of the shell environment. It usually maps to the project name. ''; }; meta = mkOption { type = types.attrsOf types.anything; default = { }; description = '' Metadata, such as 'meta.description'. Can be useful as metadata for downstream tooling. ''; }; packages = mkOption { type = types.listOf strOrPackage; default = [ ]; description = '' The set of packages to appear in the project environment. Those packages come from and can be searched by going to ''; }; packagesFrom = mkOption { type = types.listOf strOrPackage; default = [ ]; description = '' Add all the build dependencies from the listed packages to the environment. ''; }; shell = mkOption { internal = true; 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 = { package = devshell_dir; packages = lib.foldl' (sum: drv: sum ++ (inputsOf drv)) [ ] cfg.packagesFrom; startup = { motd = lib.noDepEntry '' __devshell-motd() { ${pkgs.coreutils}/bin/cat <