diff --git a/pkgs/build-support/dev-shell-tools/default.nix b/pkgs/build-support/dev-shell-tools/default.nix index 264b6a31f07a..62395f6b7dda 100644 --- a/pkgs/build-support/dev-shell-tools/default.nix +++ b/pkgs/build-support/dev-shell-tools/default.nix @@ -1,5 +1,8 @@ { lib, + runtimeShell, + bashInteractive, + stdenv, writeTextFile, }: let @@ -72,4 +75,95 @@ rec { # https://github.com/NixOS/nix/blob/2.8.0/src/libexpr/primops.cc#L1253 lib.genAttrs outputList (output: builtins.unsafeDiscardStringContext outputMap.${output}.outPath); + toBashEnv = + { env }: + lib.concatStringsSep "\n" ( + lib.mapAttrsToList ( + name: value: + if lib.isValidPosixName name then ''export ${name}=${lib.escapeShellArg value}'' else "" + ) env + ); + + buildShellEnv = + { drvAttrs }: + let + name = drvAttrs.pname or drvAttrs.name or "shell"; + env = unstructuredDerivationInputEnv { inherit drvAttrs; }; + in + stdenv.mkDerivation (finalAttrs: { + name = "${name}-env"; + passAsFile = [ + "bashEnv" + "bashrc" + "runShell" + ]; + bashEnv = toBashEnv { inherit env; }; + bashrc = '' + export NIXPKGS_SHELL_TMP="$(mktemp -d --tmpdir nixpkgs-shell-${name}.XXXXXX)" + export TMPDIR="$NIXPKGS_SHELL_TMP" + export TEMPDIR="$NIXPKGS_SHELL_TMP" + export TMP="$NIXPKGS_SHELL_TMP" + export TEMP="$NIXPKGS_SHELL_TMP" + + echo "Using TMPDIR=$TMPDIR" + + source @envbash@ + + mkdir -p $TMP/outputs + for _output in $outputs; do + export "''${_output}=$TMP/outputs/''${_output}" + done + + source @stdenv@/setup + + # Set a distinct prompt to make it clear that we are in a build shell + case "$PS1" in + *"(build shell $name)"*) + echo "It looks like your running a build shell inside a build shell." + echo "It might work, but this is probably not what you want." + echo "You may want to exit your shell before loading a new one." + ;; + esac + + # Prefix a line to the prompt to indicate that we are in a build shell + PS1=$"\n(\[\033[1;33m\]build shell: \[\033[1;34m\]$name\[\033[1;33m\]\[\033[0m\]) $PS1" + + runHook shellHook + ''; + buildCommand = '' + mkdir -p $out/lib $out/bin + bashrc="$out/lib/bashrc" + envbash="$out/lib/env.bash" + + mv "$bashEnvPath" "$envbash" + substitute "$bashrcPath" "$bashrc" \ + --replace-fail "@envbash@" "$envbash" \ + --replace-fail "@stdenv@" "$stdenv" \ + ; + + substitute ${./run-shell.sh} "$out/bin/run-shell" \ + --replace-fail "@bashrc@" "$bashrc" \ + --replace-fail "@runtimeShell@" "${runtimeShell}" \ + --replace-fail "@bashInteractive@" "${bashInteractive}" \ + ; + + # NOTE: most other files are script for the source command, and not + # standalone executables, so they should not be made executable. + chmod a+x $out/bin/run-shell + ''; + preferLocalBuild = true; + passthru = { + # Work this as a shell environment, so that commands like `nix-shell` + # will be able to check it and use it correctly. This also lets Nix know + # to stop when the user requests pkg.devShell explicitly, or a different + # attribute containing a shell environment. + isShellEnv = true; + devShell = throw "You're trying to access the devShell attribute of a shell environment. We appreciate that this is very \"meta\" and interesting, but it's usually just not what you want. Most likely you've selected one `.devShell` to deep in an expression or on the command line. Try removing the last one."; + }; + meta = { + description = "An environment similar to the build environment of ${name}"; + # TODO longDescription + mainProgram = "run-shell"; + }; + }); } diff --git a/pkgs/build-support/dev-shell-tools/run-shell.sh b/pkgs/build-support/dev-shell-tools/run-shell.sh new file mode 100644 index 000000000000..227a41b1feeb --- /dev/null +++ b/pkgs/build-support/dev-shell-tools/run-shell.sh @@ -0,0 +1,87 @@ +#!@runtimeShell@ + +# baked variables, not exported +bashrc='@bashrc@' +bash='@bashInteractive@/bin/bash' + +# detected variables and option defaults +shell="$(basename $SHELL)" +mode=interactive + +# remaining args +args=() + +# parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + -c) + mode=commands + shift + break + ;; + *) + mode=exec + break + ;; + esac +done + +while [[ $# -gt 0 ]]; do + args+=("$1") + shift +done + +case "$mode" in + interactive) + ;; + commands) + if ${#args[@]} -eq 0; then + echo "nixpkgs: You've requested a command shell, but didn't provide a command." + echo " Please provide a command to run." + exit 1 + fi + ;; + exec) + ;; +esac + +invokeWithArgs() { + bash -c 'source "'"$bashrc"'"; _script="$(shift)"; _args=("$@"); eval "$_script"' -- "$@" +} + +# For bash, we'll run the interactive variant of the version Nixpkgs uses. +# For other shells, version correctness can't be a goal, so it's best +# to launch the user's shell from $SHELL. This also avoids bringing in +# dependencies of which N-1 aren't needed. Keeps it quick. +launchBash() { + case "$mode" in + interactive) + exec "$bash" --rcfile "$bashrc" "${args[@]}" + ;; + commands) + exec "$bash" -c 'source "'"$bashrc"'"; _script="$1"; shift; eval "$_script"' -- "${args[@]}" + ;; + exec) + exec "$bash" -c 'source "'"$bashrc"'"; "$@"' -- "${args[@]}" + ;; + esac +} + +launchShell() { + case "$shell" in + bash|"") + launchBash + ;; + *) + ( + echo "nixpkgs: I see that you weren't running bash. That's cool, but" + echo " stdenv derivations are built with bash, so that's what" + echo " I'll run for you by default. We'd love to have support" + echo " for many shells, so PRs are welcome!" + ) >&2 + launchBash + ;; + esac +} + +launchShell diff --git a/pkgs/build-support/dev-shell-tools/tests/default.nix b/pkgs/build-support/dev-shell-tools/tests/default.nix index 074aa8462a7c..d29a042ff577 100644 --- a/pkgs/build-support/dev-shell-tools/tests/default.nix +++ b/pkgs/build-support/dev-shell-tools/tests/default.nix @@ -1,7 +1,11 @@ { + bash, + coreutils, devShellTools, emptyFile, + gnugrep, lib, + runCommand, stdenv, hello, writeText, @@ -187,4 +191,70 @@ lib.recurseIntoAttrs { "args" ] ); + + # derivation: Call it directly so that we get a minimal environment to run in + buildHelloInShell = + derivation { + inherit (stdenv.buildPlatform) system; + name = "buildHelloInShell"; + builder = "${bash}/bin/bash"; + PATH = lib.makeBinPath [ + coreutils + gnugrep + ]; + args = [ + "-euo" + "pipefail" + "-c" + '' + ${lib.getExe hello.devShell} -c 'genericBuild && ln -s $out "'"$PWD"'/result"' + ls -al + ./result/bin/hello | grep -i 'hello, world' + (set -x; [[ ! -e $out ]]) + touch $out + '' + ]; + } + // { + # Required package attributes for Nixpkgs (CI) + meta = { }; + }; + + various = + let + hello2 = hello.overrideAttrs (o: { + shellHook = '' + echo 'shellHook says hi :)' + ''; + }); + inherit (hello2) devShell; + in + derivation { + inherit (stdenv.buildPlatform) system; + name = "various"; + builder = "${bash}/bin/bash"; + PATH = lib.makeBinPath [ + coreutils + gnugrep + ]; + args = [ + "-euo" + "pipefail" + "-c" + '' + ${lib.getExe devShell} -c 'echo "hello, world"' \ + | grep -F 'hello, world' + ${lib.getExe devShell} -c 'echo "hello, world"' \ + | grep -F 'hello, world' + + ls -al + (set -x; [[ ! -e $out ]]) + touch $out + '' + ]; + } + // { + # Required package attributes for Nixpkgs (CI) + meta = { }; + }; } diff --git a/pkgs/development/haskell-modules/generic-builder.nix b/pkgs/development/haskell-modules/generic-builder.nix index acfead1a5125..48096276a760 100644 --- a/pkgs/development/haskell-modules/generic-builder.nix +++ b/pkgs/development/haskell-modules/generic-builder.nix @@ -11,6 +11,7 @@ ghcWithHoogle, ghcWithPackages, nodejs, + devShellTools, }: let @@ -1045,6 +1046,8 @@ lib.fix ( // env'; } "echo $nativeBuildInputs $buildInputs > $out"; + # Specialise the devShell attribute, so we get our improved shell. + devShell = env.devShell; env = envFunc { }; }; diff --git a/pkgs/stdenv/generic/default.nix b/pkgs/stdenv/generic/default.nix index 2be7375f5357..7b2b80c64ba2 100644 --- a/pkgs/stdenv/generic/default.nix +++ b/pkgs/stdenv/generic/default.nix @@ -64,6 +64,9 @@ let # This is convenient to have as a parameter so the stdenv "adapters" work better mkDerivationFromStdenv ? stdenv: (import ./make-derivation.nix { inherit lib config; } stdenv).mkDerivation, + + # callPackage for "passthru" utilities only + callPackage ? null, }: let @@ -91,6 +94,11 @@ let stdenv = (stdenv-overridable argsStdenv); + devShellTools = + if callPackage == null then + null + else + callPackage ../../build-support/dev-shell-tools { inherit stdenv; }; in # The stdenv that we are producing. derivation ( @@ -214,6 +222,8 @@ let # commands and extglob affects the Bash parser, we enable extglob always. shellDryRun = "${stdenv.shell} -n -O extglob"; + buildShellEnv = if devShellTools ? buildShellEnv then devShellTools.buildShellEnv else _: null; + tests = { succeedOnFailure = import ../tests/succeedOnFailure.nix { inherit stdenv; }; }; diff --git a/pkgs/stdenv/generic/make-derivation.nix b/pkgs/stdenv/generic/make-derivation.nix index f453e61510c3..6a585f7ad624 100644 --- a/pkgs/stdenv/generic/make-derivation.nix +++ b/pkgs/stdenv/generic/make-derivation.nix @@ -874,6 +874,8 @@ let inherit passthru overrideAttrs; inherit meta; + devShell = stdenv.buildShellEnv { drvAttrs = derivationArg; }; + } // # Pass through extra attributes that are not inputs, but diff --git a/pkgs/stdenv/linux/default.nix b/pkgs/stdenv/linux/default.nix index 6287a3877b41..ed37ad779699 100644 --- a/pkgs/stdenv/linux/default.nix +++ b/pkgs/stdenv/linux/default.nix @@ -167,6 +167,7 @@ let name, overrides ? (self: super: { }), extraNativeBuildInputs ? [ ], + callPackage ? null, }: let @@ -223,6 +224,7 @@ let ); overrides = self: super: (overrides self super) // { fetchurl = thisStdenv.fetchurlBoot; }; + inherit callPackage; }; in diff --git a/pkgs/top-level/stage.nix b/pkgs/top-level/stage.nix index bfc2ee8f664c..efd1a4835e06 100644 --- a/pkgs/top-level/stage.nix +++ b/pkgs/top-level/stage.nix @@ -170,7 +170,12 @@ let pkgs = self.pkgsHostTarget; targetPackages = self.pkgsTargetTarget; - inherit stdenv stdenvNoCC; + stdenv = stdenv.override (o: { + callPackage = self.callPackage; + }); + stdenvNoCC = stdenvNoCC.override (o: { + callPackage = self.callPackage; + }); }; splice = self: super: import ./splice.nix lib self (adjacentPackages != null);