diff --git a/default.nix b/default.nix index a1b56444..9fb6cb58 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 ac62e2cf..8a50e64c 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/docs/major-differences-nixpkgs.md b/docs/major-differences-nixpkgs.md index 3e2426ff..c9ec90b4 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. diff --git a/stdenv/generic/make-derivation.nix b/stdenv/generic/make-derivation.nix index 89d41393..adb7f59a 100644 --- a/stdenv/generic/make-derivation.nix +++ b/stdenv/generic/make-derivation.nix @@ -1060,6 +1060,20 @@ let } ); + # Convert this derivation to a development shell, preserving its + # 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 attrsToRemoveLast; + toShell = import ./to-dev-shell.nix lib originalArgs; + shellFunc = f: if builtins.isFunction f then f stdenv else f; + in + 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 new file mode 100644 index 00000000..acea99ae --- /dev/null +++ b/stdenv/generic/to-dev-shell.nix @@ -0,0 +1,166 @@ +lib: + +originalArgs: + +# 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 "dev-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 ? [ ], + 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: + 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" + "packages" + "inputsFrom" + "buildInputs" + "nativeBuildInputs" + "propagatedBuildInputs" + "propagatedNativeBuildInputs" + "shellHook" + "modules" + "env" + ]; +in +{ + inherit name modules; + + buildInputs = mergeInputs "buildInputs"; + nativeBuildInputs = packages ++ (mergeInputs "nativeBuildInputs"); + propagatedBuildInputs = mergeInputs "propagatedBuildInputs"; + propagatedNativeBuildInputs = mergeInputs "propagatedNativeBuildInputs"; + + inherit inputsFrom shellHook; + + # Merge original derivation env vars with user-supplied env + env = originalEnv // env; +} +// rest