From 36c1f5388db1bdfb65feb39172edc9cbc0c6d0d4 Mon Sep 17 00:00:00 2001 From: Jonathan Ringer Date: Sat, 29 Aug 2026 08:58:29 -0700 Subject: [PATCH 1/3] mkDerivation: add toDevShell passthru (EEP 0001) Expose a toDevShell function on every mkDerivation output that converts the derivation into a development shell, preserving its build environment (compiler, flags, phases, environment variables). Accepts either an attrset or a function (stdenv -> attrset) for conditional inputs. See: https://github.com/ekala-project/eeps/pull/1 --- stdenv/generic/make-derivation.nix | 11 +++++ stdenv/generic/to-dev-shell.nix | 77 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 stdenv/generic/to-dev-shell.nix diff --git a/stdenv/generic/make-derivation.nix b/stdenv/generic/make-derivation.nix index 89d413931..85321a9f9 100644 --- a/stdenv/generic/make-derivation.nix +++ b/stdenv/generic/make-derivation.nix @@ -1060,6 +1060,17 @@ let } ); + # Convert this derivation to a development shell, preserving its + # build environment (compiler, flags, phases, env vars). + # Accepts either an attrset or a function (stdenv -> attrset). + toDevShell = + let + originalArgs = removeAttrs derivationArg (fixedOutputRelatedAttrs ++ outputCheckAttrs); + toShell = import ./to-dev-shell.nix lib originalArgs; + shellFunc = f: if builtins.isFunction f then f stdenv else f; + in + f: derivation (toShell (shellFunc f)); + inherit passthru overrideAttrs; inherit meta; } diff --git a/stdenv/generic/to-dev-shell.nix b/stdenv/generic/to-dev-shell.nix new file mode 100644 index 000000000..3379dd783 --- /dev/null +++ b/stdenv/generic/to-dev-shell.nix @@ -0,0 +1,77 @@ +lib: + +originalArgs: + +# A special kind of derivation that is only meant to be consumed by the +# nix-shell. Merges the original derivation's build inputs with +# user-supplied overrides. +{ + name ? if originalArgs ? name then "${originalArgs.name}-dev-shell" else "nix-shell", + # a list of packages to add to the shell environment + packages ? [ ], + # propagate all the inputs from the given derivations + inputsFrom ? [ ], + buildInputs ? [ ], + nativeBuildInputs ? [ ], + propagatedBuildInputs ? [ ], + propagatedNativeBuildInputs ? [ ], + ... +}@attrs: +let + mergeInputs = + name: + (originalArgs.${name} or [ ]) + ++ (attrs.${name} or [ ]) + # 1. get all `{build,nativeBuild,...}Inputs` from the elements of `inputsFrom` + # 2. since that is a list of lists, `flatten` that into a regular list + # 3. filter out of the result everything that's in `inputsFrom` itself + # this leaves actual dependencies of the derivations in `inputsFrom`, + # but never the derivations themselves + ++ (lib.subtractLists inputsFrom (lib.flatten (lib.catAttrs name inputsFrom))); + + rest = builtins.removeAttrs attrs [ + "name" + "packages" + "inputsFrom" + "buildInputs" + "nativeBuildInputs" + "propagatedBuildInputs" + "propagatedNativeBuildInputs" + "shellHook" + ]; +in +originalArgs +// { + inherit name; + + buildInputs = mergeInputs "buildInputs"; + nativeBuildInputs = packages ++ (mergeInputs "nativeBuildInputs"); + propagatedBuildInputs = mergeInputs "propagatedBuildInputs"; + propagatedNativeBuildInputs = mergeInputs "propagatedNativeBuildInputs"; + + # Shell derivations only need a single output + outputs = [ "out" ]; + + # Avoid explicit checkout, and assume that shell will be used on source in repo + src = null; + + shellHook = lib.concatStringsSep "\n" ( + lib.catAttrs "shellHook" (lib.reverseList inputsFrom ++ [ attrs ]) + ); + + phases = [ "buildPhase" ]; + + buildPhase = '' + { echo "------------------------------------------------------------"; + echo " WARNING: the existence of this path is not guaranteed."; + echo " It is an internal implementation detail for pkgs.mkShell."; + echo "------------------------------------------------------------"; + echo; + # Record all build inputs as runtime dependencies + export; + } >> "$out" + ''; + + preferLocalBuild = true; +} +// rest From 5c26316aa2f607bd1c8df3aa7b6885529c8f8fb0 Mon Sep 17 00:00:00 2001 From: Jonathan Ringer Date: Sat, 5 Sep 2026 10:38:49 -0700 Subject: [PATCH 2/3] toDevShell: call mkDevShell for service and language module support Rewire toDevShell passthru so that drv.toDevShell {} calls mkDevShell under the hood, giving every derivation access to ekaos service modules, language modules, and process-compose integration in its dev shell. mkDevShell gains nativeBuildInputs, propagatedBuildInputs, propagatedNativeBuildInputs, inputsFrom, env, and name parameters so it can receive the full build environment from toDevShell. to-dev-shell.nix now produces a mkDevShell-compatible attrset, merging the original derivation's dependencies and extracting env vars while filtering out phase hooks and build infrastructure attrs. mkDevShell is injected lazily into config so make-derivation.nix can reference it without circular evaluation. --- default.nix | 7 +- dev-shell/default.nix | 77 +++++++++++++- stdenv/generic/make-derivation.nix | 9 +- stdenv/generic/to-dev-shell.nix | 165 ++++++++++++++++++++++------- 4 files changed, 211 insertions(+), 47 deletions(-) diff --git a/default.nix b/default.nix index a1b56444e..9fb6cb587 100644 --- a/default.nix +++ b/default.nix @@ -145,7 +145,12 @@ let config = lib.asserts.checkAssertWarn configEval.config.assertions configEval.config.warnings - configEval.config; + configEval.config + // { + # Injected lazily for toDevShell passthru — only forced when a user + # actually calls drv.toDevShell, at which point pkgs is fully resolved. + mkDevShell = pkgs.mkDevShell; + }; # A few packages make a new package set to draw their dependencies from. # (Currently to get a cross tool chain, or forced-i686 package.) Rather than diff --git a/dev-shell/default.nix b/dev-shell/default.nix index ac62e2cf3..8a50e64cc 100644 --- a/dev-shell/default.nix +++ b/dev-shell/default.nix @@ -102,9 +102,16 @@ let name = "dev-shell"; phases = [ "buildPhase" ]; buildPhase = '' - echo "This derivation is not meant to be built, only to be used with nix-shell" - touch $out + { echo "------------------------------------------------------------"; + echo " WARNING: the existence of this path is not guaranteed."; + echo " It is an internal implementation detail for mkDevShell."; + echo "------------------------------------------------------------"; + echo; + # Record all build inputs as runtime dependencies + export; + } >> "$out" ''; + preferLocalBuild = true; shellHook = ""; } // attrs @@ -116,6 +123,9 @@ in # Main function: Create a development shell with services mkDevShell = { + # Shell name + name ? "dev-shell", + # Service and language configuration via modules modules ? [ ], @@ -123,6 +133,15 @@ in packages ? [ ], shellHook ? "", buildInputs ? [ ], + nativeBuildInputs ? [ ], + propagatedBuildInputs ? [ ], + propagatedNativeBuildInputs ? [ ], + + # Propagate all inputs from the given derivations + inputsFrom ? [ ], + + # Environment variables to set in the shell (attrset of strings) + env ? { }, # process-compose specific options processCompose ? { @@ -189,9 +208,39 @@ in ) langVariables ); + # Merge inputs from inputsFrom derivations (same logic as mkShell/to-dev-shell) + mergeInputs = + attr: + (args.${attr} or [ ]) + ++ (lib.subtractLists inputsFrom (lib.flatten (lib.catAttrs attr inputsFrom))); + + mergedBuildInputs = mergeInputs "buildInputs"; + mergedNativeBuildInputs = mergeInputs "nativeBuildInputs"; + mergedPropagatedBuildInputs = mergeInputs "propagatedBuildInputs"; + mergedPropagatedNativeBuildInputs = mergeInputs "propagatedNativeBuildInputs"; + + # Merge shellHooks from inputsFrom + inputsFromShellHook = lib.concatStringsSep "\n" ( + lib.catAttrs "shellHook" (lib.reverseList inputsFrom) + ); + + # Generate export statements for env variables + envExports = lib.concatStringsSep "\n" ( + lib.mapAttrsToList (n: v: "export ${n}=${lib.escapeShellArg (toString v)}") env + ); + # Extract non-service options for mkShell shellArgs = builtins.removeAttrs args [ + "name" "modules" + "packages" + "buildInputs" + "nativeBuildInputs" + "propagatedBuildInputs" + "propagatedNativeBuildInputs" + "inputsFrom" + "env" + "shellHook" "processCompose" ]; @@ -201,6 +250,11 @@ in mkdir -p ${processCompose.logDir} mkdir -p ${processCompose.dataDir} + ${lib.optionalString (env != { }) '' + # Derivation environment variables + ${envExports} + ''} + ${lib.optionalString (langVariables != { }) '' # Language environment variables ${langExports} @@ -208,13 +262,13 @@ in # Display service information echo "================================================" - echo "Development Shell with Services" + echo "Development Shell: ${name}" echo "================================================" ${lib.optionalString (enabledServices != { }) '' echo "" echo "Available services:" ${lib.concatStringsSep "\n" ( - lib.mapAttrsToList (name: _: " echo \" - ${name}\"") enabledServices + lib.mapAttrsToList (svcName: _: " echo \" - ${svcName}\"") enabledServices )} echo "" echo "Service management commands:" @@ -248,6 +302,9 @@ in trap _pc_cleanup EXIT ''} + # Shell hooks from inputsFrom derivations + ${inputsFromShellHook} + # User's custom shellHook ${shellHook} ''; @@ -256,8 +313,18 @@ in mkShell ( shellArgs // { + inherit name; + buildInputs = - buildInputs ++ packages ++ langPackages ++ [ processComposePackage ] ++ (lib.attrValues utilities); + mergedBuildInputs + ++ packages + ++ langPackages + ++ [ processComposePackage ] + ++ (lib.attrValues utilities); + + nativeBuildInputs = mergedNativeBuildInputs; + propagatedBuildInputs = mergedPropagatedBuildInputs; + propagatedNativeBuildInputs = mergedPropagatedNativeBuildInputs; shellHook = enhancedShellHook; diff --git a/stdenv/generic/make-derivation.nix b/stdenv/generic/make-derivation.nix index 85321a9f9..adb7f59a1 100644 --- a/stdenv/generic/make-derivation.nix +++ b/stdenv/generic/make-derivation.nix @@ -1061,15 +1061,18 @@ let ); # Convert this derivation to a development shell, preserving its - # build environment (compiler, flags, phases, env vars). + # build environment and gaining mkDevShell features (services, + # language modules, process-compose). + # myPkg.toDevShell { } + # myPkg.toDevShell { modules = [ ... ]; packages = [ ... ]; } # Accepts either an attrset or a function (stdenv -> attrset). toDevShell = let - originalArgs = removeAttrs derivationArg (fixedOutputRelatedAttrs ++ outputCheckAttrs); + originalArgs = removeAttrs derivationArg attrsToRemoveLast; toShell = import ./to-dev-shell.nix lib originalArgs; shellFunc = f: if builtins.isFunction f then f stdenv else f; in - f: derivation (toShell (shellFunc f)); + f: config.mkDevShell (toShell (shellFunc f)); inherit passthru overrideAttrs; inherit meta; diff --git a/stdenv/generic/to-dev-shell.nix b/stdenv/generic/to-dev-shell.nix index 3379dd783..acea99ae0 100644 --- a/stdenv/generic/to-dev-shell.nix +++ b/stdenv/generic/to-dev-shell.nix @@ -2,11 +2,11 @@ lib: originalArgs: -# A special kind of derivation that is only meant to be consumed by the -# nix-shell. Merges the original derivation's build inputs with -# user-supplied overrides. +# Extract shell-relevant information from a mkDerivation's derivationArg +# and merge it with user-supplied overrides, producing an attrset +# suitable for mkDevShell. { - name ? if originalArgs ? name then "${originalArgs.name}-dev-shell" else "nix-shell", + name ? if originalArgs ? name then "${originalArgs.name}-dev-shell" else "dev-shell", # a list of packages to add to the shell environment packages ? [ ], # propagate all the inputs from the given derivations @@ -15,19 +15,127 @@ originalArgs: nativeBuildInputs ? [ ], propagatedBuildInputs ? [ ], propagatedNativeBuildInputs ? [ ], + shellHook ? "", + modules ? [ ], + env ? { }, ... }@attrs: let + # Merge original derivation inputs with user-supplied overrides mergeInputs = + attrName: + (originalArgs.${attrName} or [ ]) + ++ (attrs.${attrName} or [ ]) + ++ (lib.subtractLists inputsFrom (lib.flatten (lib.catAttrs attrName inputsFrom))); + + # Attrs from derivationArg that are build infrastructure, not environment + # variables. Anything not in this set AND string/path-valued will be + # forwarded as an env var to the dev shell. + infrastructureAttrs = [ + "name" + "pname" + "version" + "builder" + "args" + "system" + "outputs" + "out" + "src" + "srcs" + "sourceRoot" + "setSourceRoot" + "stdenv" + "buildInputs" + "nativeBuildInputs" + "propagatedBuildInputs" + "propagatedNativeBuildInputs" + "depsBuildBuild" + "depsBuildBuildPropagated" + "depsBuildTarget" + "depsBuildTargetPropagated" + "depsHostHost" + "depsHostHostPropagated" + "depsTargetTarget" + "depsTargetTargetPropagated" + "shellHook" + "patches" + "patchFlags" + "doCheck" + "doInstallCheck" + "strictDeps" + "userHook" + "__ignoreNulls" + "__structuredAttrs" + "__contentAddressed" + "outputHashAlgo" + "outputHashMode" + "outputHash" + "preferLocalBuild" + "allowSubstitutes" + "enableParallelBuilding" + "enableParallelChecking" + "enableParallelInstalling" + "meta" + "passthru" + "pos" + "separateDebugInfo" + "hardeningEnable" + "hardeningDisable" + "NIX_HARDENING_ENABLE" + "requiredSystemFeatures" + "__darwinAllowLocalNetworking" + "__sandboxProfile" + "__propagatedSandboxProfile" + "__impureHostDeps" + "__propagatedImpureHostDeps" + "allowedImpureDLLs" + "outputChecks" + "disallowedReferences" + "disallowedRequisites" + "allowedReferences" + "allowedRequisites" + "cmakeFlags" + "mesonFlags" + "configureFlags" + "configurePlatforms" + "makeFlags" + "makefile" + "installFlags" + "installTargets" + "dontInstall" + "dontBuild" + "dontConfigure" + "dontFixup" + "dontPatchShebangs" + "dontPatchELF" + "dontStrip" + "forceShare" + "setupHook" + "setupHooks" + "passAsFile" + ]; + + # Phase hooks and scripts (pre/post hooks, phase definitions, configure + # scripts, etc.) are build-time concerns and should not leak as env vars. + isPhaseAttr = name: - (originalArgs.${name} or [ ]) - ++ (attrs.${name} or [ ]) - # 1. get all `{build,nativeBuild,...}Inputs` from the elements of `inputsFrom` - # 2. since that is a list of lists, `flatten` that into a regular list - # 3. filter out of the result everything that's in `inputsFrom` itself - # this leaves actual dependencies of the derivations in `inputsFrom`, - # but never the derivations themselves - ++ (lib.subtractLists inputsFrom (lib.flatten (lib.catAttrs name inputsFrom))); + lib.hasPrefix "pre" name + || lib.hasPrefix "post" name + || lib.hasSuffix "Phase" name + || lib.hasSuffix "Phases" name + || lib.hasSuffix "Hook" name + || lib.hasSuffix "Script" name + || lib.hasSuffix "Flags" name; + + # Environment variables from the original derivation (everything that's + # a string and not infrastructure — these are typically set via `env` or + # as top-level attrs in mkDerivation). + originalEnv = lib.filterAttrs ( + n: v: + !(builtins.elem n infrastructureAttrs) + && !(isPhaseAttr n) + && (builtins.isString v || builtins.isPath v) + ) originalArgs; rest = builtins.removeAttrs attrs [ "name" @@ -38,40 +146,21 @@ let "propagatedBuildInputs" "propagatedNativeBuildInputs" "shellHook" + "modules" + "env" ]; in -originalArgs -// { - inherit name; +{ + inherit name modules; buildInputs = mergeInputs "buildInputs"; nativeBuildInputs = packages ++ (mergeInputs "nativeBuildInputs"); propagatedBuildInputs = mergeInputs "propagatedBuildInputs"; propagatedNativeBuildInputs = mergeInputs "propagatedNativeBuildInputs"; - # Shell derivations only need a single output - outputs = [ "out" ]; - - # Avoid explicit checkout, and assume that shell will be used on source in repo - src = null; - - shellHook = lib.concatStringsSep "\n" ( - lib.catAttrs "shellHook" (lib.reverseList inputsFrom ++ [ attrs ]) - ); - - phases = [ "buildPhase" ]; - - buildPhase = '' - { echo "------------------------------------------------------------"; - echo " WARNING: the existence of this path is not guaranteed."; - echo " It is an internal implementation detail for pkgs.mkShell."; - echo "------------------------------------------------------------"; - echo; - # Record all build inputs as runtime dependencies - export; - } >> "$out" - ''; + inherit inputsFrom shellHook; - preferLocalBuild = true; + # Merge original derivation env vars with user-supplied env + env = originalEnv // env; } // rest From 3b05a84668505a539f55ec0dcfd084185e5ed2bc Mon Sep 17 00:00:00 2001 From: Jonathan Ringer Date: Sat, 5 Sep 2026 11:28:27 -0700 Subject: [PATCH 3/3] docs: add mkDevShell and toDevShell to major differences --- docs/major-differences-nixpkgs.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/major-differences-nixpkgs.md b/docs/major-differences-nixpkgs.md index 3e2426ffc..c9ec90b41 100644 --- a/docs/major-differences-nixpkgs.md +++ b/docs/major-differences-nixpkgs.md @@ -58,3 +58,27 @@ changes differ significantly from whath one would expct with Nixpkgs. run its tests. - This decouples test execution from the main build, allowing test failures or test-only dependency churn to avoid invalidating downstream consumers. + +## Development shells + +- Every `mkDerivation` output exposes a `toDevShell` passthru attribute that + converts the derivation into a development shell via `mkDevShell`. + - `toDevShell` preserves the original derivation's build environment + (dependencies, compiler, flags, environment variables) and forwards them + to `mkDevShell`, so the resulting shell automatically gains ekaos service + modules, language modules, and process-compose integration. + - Usage: `myPkg.toDevShell { }` returns a shell derivation directly. + Additional `mkDevShell` options (`modules`, `packages`, `shellHook`, etc.) + can be passed in the attrset argument. + - Accepts a function form for conditional inputs: + `myPkg.toDevShell (stdenv: { packages = lib.optionals stdenv.isLinux [ pkgs.strace ]; })`. +- `mkDevShell` replaces `mkShell` as the primary shell builder. + - Accepts `modules` for ekaos service and language configuration, with + automatic process-compose integration for running services in the + background. + - Supports `inputsFrom` for propagating inputs from other derivations, + `env` for arbitrary environment variables, and the full set of dependency + list parameters (`nativeBuildInputs`, `propagatedBuildInputs`, + `propagatedNativeBuildInputs`). + - Nixpkgs has no equivalent; `mkShell` there is a thin wrapper around + `mkDerivation` with no service or module integration.