diff --git a/.agents/skills/mkekapackage/SKILL.md b/.agents/skills/mkekapackage/SKILL.md new file mode 100644 index 00000000..4b816e32 --- /dev/null +++ b/.agents/skills/mkekapackage/SKILL.md @@ -0,0 +1,343 @@ +--- +name: mkekapackage +description: Write mkEkaPackage expressions for corepkgs. Use when creating or reviewing packages that use mkEkaPackage — covers scope-based dependency declaration, the cc attribute, conditional dependencies, overrides, and migration from mkDerivation. +--- + +# mkEkaPackage + +`mkEkaPackage` is the preferred way to define packages in core-pkgs. It +replaces `stdenv.mkDerivation` with scope-based dependency declaration: +dependencies are pulled from the package scope inside `commands` and +`libraries` functions rather than injected via `callPackage` arguments. + +## Minimal Example + +```nix +{ mkEkaPackage, fetchFromGitHub, lib }: + +mkEkaPackage (finalAttrs: { + pname = "example"; + version = "1.0.0"; + + src = fetchFromGitHub { + owner = "example"; + repo = "example"; + tag = "v${finalAttrs.version}"; + hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + }; + + commands = scope: { + inherit (scope) pkg-config; + }; + + libraries = scope: { + inherit (scope) zlib openssl; + }; + + meta = { + description = "Example package"; + license = lib.licenses.mit; + platforms = lib.platforms.unix; + }; +}) +``` + +## Key Differences from mkDerivation + +| `mkDerivation` | `mkEkaPackage` | +|----------------|----------------| +| `stdenv.mkDerivation` | `mkEkaPackage` (scope member, not on stdenv) | +| `nativeBuildInputs = [ cmake pkg-config ];` | `commands = scope: { inherit (scope) cmake pkg-config; };` | +| `buildInputs = [ zlib openssl ];` | `libraries = scope: { inherit (scope) zlib openssl; };` | +| Dependencies in `callPackage` args | Dependencies from scope; only `mkEkaPackage`, fetchers, `lib`, and config flags in args | +| `clangStdenv.mkDerivation` | `cc = scope: scope.clang;` | +| `stdenvNoCC.mkDerivation` | `cc = null;` | + +## Function Arguments + +The `callPackage` argument list should contain **only**: + +- `mkEkaPackage` — the builder +- Fetchers (`fetchurl`, `fetchFromGitHub`, etc.) — these are functions, not derivations +- `lib` — when needed for helpers +- Configuration flags (`withFoo ? false`, etc.) + +**All package dependencies** (libraries, build tools, setup hooks) must come +from the scope passed to `commands` and `libraries`. Do not inherit package +dependencies from the `callPackage` arguments. + +```nix +# CORRECT — dependencies come from scope +{ mkEkaPackage, fetchurl, lib, withGui ? false }: + +mkEkaPackage (finalAttrs: { + # ... + commands = scope: { + inherit (scope) pkg-config; + }; + libraries = scope: { + inherit (scope) zlib; + } // lib.optionalAttrs withGui { + inherit (scope) gtk3; + }; +}) +``` + +```nix +# WRONG — do not pull package dependencies from callPackage args +{ mkEkaPackage, fetchurl, lib, zlib, pkg-config, gtk3, withGui ? false }: + +mkEkaPackage (finalAttrs: { + # ... + commands = scope: { + inherit pkg-config; # BAD: from callPackage, not scope + }; + libraries = scope: { + inherit zlib; # BAD: from callPackage, not scope + }; +}) +``` + +## Dependency Declaration + +Dependencies are declared as functions that receive the appropriate package +scope and return a named attrset. + +| Attribute | Replaces | Scope received | +|-----------|----------|----------------| +| `commands` | `nativeBuildInputs` | `pkgsBuildHost` | +| `libraries` | `buildInputs` | `pkgsHostTarget` | +| `propagatedCommands` | `propagatedNativeBuildInputs` | `pkgsBuildHost` | +| `propagatedLibraries` | `propagatedBuildInputs` | `pkgsHostTarget` | +| `depsBuildBuild` | `depsBuildBuild` | `pkgsBuildBuild` | +| `depsBuildTarget` | `depsBuildTarget` | `pkgsBuildTarget` | +| `depsHostHost` | `depsHostHost` | `pkgsHostHost` | +| `depsTargetTarget` | `depsTargetTarget` | `pkgsTargetTarget` | + +Each attrset is flattened to a list (via `builtins.attrValues`), with `null` +values filtered out and `getDev` applied. The flattened lists are passed to +the underlying derivation call. + +## CC Attribute + +The `cc` attribute controls which C compiler is used. It is separate from +`commands` and resolved before dependency flattening. + +```nix +# Default — omit cc; uses mkEkaPackage.cc (same as stdenv.cc) +mkEkaPackage (finalAttrs: { + pname = "normal-package"; + # ... +}) + +# Use Clang (replaces clangStdenv) +mkEkaPackage (finalAttrs: { + pname = "mesa"; + # ... + cc = scope: scope.clang; +}) + +# Pin a GCC version (replaces gcc12Stdenv) +mkEkaPackage (finalAttrs: { + pname = "legacy-app"; + # ... + cc = scope: scope.gcc12; +}) + +# No compiler (replaces stdenvNoCC) +mkEkaPackage (finalAttrs: { + pname = "tzdata"; + # ... + cc = null; +}) +``` + +When `cc` is a function, it receives `pkgsBuildHost`. The resolved `cc` is +merged into `commands` under the key `cc` (user `commands` entries with key +`cc` take precedence). Hardening flags are derived from the resolved compiler. + +## Conditional Dependencies + +Two patterns: + +```nix +# Null filtering — good for single deps +libraries = scope: { + inherit (scope) openssl zlib; + perl = if withPerl then scope.perl else null; +}; + +# optionalAttrs — good for groups +libraries = scope: { + inherit (scope) openssl zlib; +} // lib.optionalAttrs withGui { + inherit (scope) gtk3 cairo pango; +}; +``` + +## Referencing Dependencies in Phases + +Access resolved dependencies through `finalAttrs`: + +```nix +mkEkaPackage (finalAttrs: { + # ... + commands = scope: { + cmake = scope.cmake.minimal; + }; + + checkPhase = '' + ${lib.getBin finalAttrs.commands.cmake}/bin/ctest --test-dir build + ''; +}) +``` + +## Non-Scope Items + +Items not in the package scope can be added directly: + +```nix +commands = scope: { + inherit (scope) pkg-config ninja; + mesonHook = scope.meson.configurePhaseHook; # sub-attributes + myLocalTool = someLocalDerivation; # locally defined +}; +``` + +## Overriding + +`overrideAttrs` composes naturally with scope-based dependencies: + +```nix +# Add a dependency +pkg.overrideAttrs (prev: { + libraries = scope: prev.libraries scope // { extra = scope.extra; }; +}) + +# Remove a dependency +pkg.overrideAttrs (prev: { + libraries = scope: removeAttrs (prev.libraries scope) [ "libxslt" ]; +}) + +# Replace a dependency +pkg.overrideAttrs (prev: { + libraries = scope: prev.libraries scope // { openssl = myCustomOpenssl; }; +}) + +# Switch the compiler +pkg.overrideAttrs { + cc = scope: scope.clang; +} +``` + +## CMake Packages + +```nix +{ mkEkaPackage, fetchFromGitHub, lib }: + +mkEkaPackage (finalAttrs: { + pname = "example"; + version = "1.0.0"; + + src = fetchFromGitHub { + owner = "example"; + repo = "example"; + tag = "v${finalAttrs.version}"; + hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + }; + + commands = scope: { + inherit (scope) pkg-config; + cmake = scope.cmake; + cmakeHook = scope.cmake.configurePhaseHook; + }; + + libraries = scope: { + inherit (scope) zlib openssl; + }; + + cmakeFlags = [ + "-DBUILD_SHARED_LIBS=ON" + ]; + + meta = { + description = "Example CMake package"; + license = lib.licenses.mit; + }; +}) +``` + +## Meson Packages + +```nix +{ mkEkaPackage, fetchurl, lib }: + +mkEkaPackage (finalAttrs: { + pname = "example"; + version = "2.0.0"; + + src = fetchurl { + url = "https://example.com/example-${finalAttrs.version}.tar.xz"; + hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + }; + + commands = scope: { + inherit (scope) pkg-config ninja; + mesonHook = scope.meson.configurePhaseHook; + }; + + libraries = scope: { + inherit (scope) glib gtk3; + }; + + meta = { + description = "Example Meson package"; + license = lib.licenses.gpl3Plus; + }; +}) +``` + +## Introspection + +`mkEkaPackage` is an attrset with `__functor`, so it can be inspected: + +- `mkEkaPackage.cc` — the default C compiler +- `mkEkaPackage.stdenv.hostPlatform` — platform information +- `mkEkaPackage.scopes.buildHost` — the build-time package scope + +## Complete Example + +```nix +{ + mkEkaPackage, + fetchurl, + lib, + withPerl ? false, +}: + +mkEkaPackage (finalAttrs: { + pname = "nginx"; + version = "1.30.4"; + + src = fetchurl { + url = "https://nginx.org/download/nginx-${finalAttrs.version}.tar.gz"; + hash = "sha256-QmHckOnkfBxAQSdumqo9SOvi5mT3KOFPqVrmxn1XoIs="; + }; + + commands = scope: { + inherit (scope) installShellFiles removeReferencesTo; + }; + + libraries = scope: { + inherit (scope) openssl zlib pcre2 libxml2 libxslt; + } // lib.optionalAttrs withPerl { + inherit (scope) perl; + }; + + meta = { + description = "HTTP and reverse proxy server"; + license = lib.licenses.bsd2; + platforms = lib.platforms.unix; + }; +}) +``` diff --git a/AGENTS.md b/AGENTS.md index 2377ce7c..8e8b4460 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,7 @@ This document provides high-level guidelines for AI agents working with the core-pkgs repository. For detailed information on specific topics, see the `.agents/skills/` directory. **Quick access to detailed guides:** +- [`.agents/skills/mkekapackage/SKILL.md`](.agents/skills/mkekapackage/SKILL.md) - mkEkaPackage scope-based dependency declaration, cc attribute, and migration from mkDerivation - [`.agents/skills/cmake/SKILL.md`](.agents/skills/cmake/SKILL.md) - CMake build system - [`.agents/skills/meson/SKILL.md`](.agents/skills/meson/SKILL.md) - Meson build system - [`.agents/skills/packaging/SKILL.md`](.agents/skills/packaging/SKILL.md) - Packaging conventions, including dependency management, cross-compilation, and passthru attributes diff --git a/pkgs-many/cmake/nushell-hook.nu b/pkgs-many/cmake/nushell-hook.nu new file mode 100644 index 00000000..873b23b3 --- /dev/null +++ b/pkgs-many/cmake/nushell-hook.nu @@ -0,0 +1,78 @@ +# CMake nushell hook for mkEkaPackage +# +# This script is passed as a subcommand argument when invoked by setup.nu. +# Usage in mkEkaPackage: +# commands = scope: { +# inherit (scope) cmake ninja; +# cmakeHook = scope.cmake.nushellHook; +# }; +# +# The hook overrides configurePhase, buildPhase, checkPhase, and installPhase +# to use cmake/ninja instead of make. + +def main [phase: string] { + let attrs = (open $env.NIX_ATTRS_JSON_FILE) + + match $phase { + "configure" => { cmakeConfigure $attrs } + "build" => { cmakeBuild $attrs } + "check" => { cmakeCheck $attrs } + "install" => { cmakeInstall $attrs } + _ => { print -e $"cmake hook: unknown phase ($phase)" } + } +} + +def cmakeConfigure [attrs: record] { + let cmakeBuildDir = ($attrs | get -o cmakeBuildDir | default "build") + let cmakeFlags = ($attrs | get -o cmakeFlags | default []) + let prefix = $env.out + let cores = ($env | get -o NIX_BUILD_CORES | default "1") + + mkdir $cmakeBuildDir + cd $cmakeBuildDir + + # Collect dependency paths for CMAKE_PREFIX_PATH + let buildInputs = ($attrs | get -o buildInputs | default []) + let nativeBuildInputs = ($attrs | get -o nativeBuildInputs | default []) + let allDeps = ($buildInputs ++ $nativeBuildInputs | where {|d| not ($d | str ends-with ".nu")}) + let cmakePrefixPath = ($allDeps | str join ";") + + mut flags = [ + $"-DCMAKE_INSTALL_PREFIX=($prefix)" + "-DCMAKE_BUILD_TYPE=Release" + "-DCMAKE_INSTALL_LIBDIR=lib" + $"-DCMAKE_PREFIX_PATH=($cmakePrefixPath)" + "-DBUILD_SHARED_LIBS=ON" + ] + + $flags = ($flags | append $cmakeFlags) + + print -e $"+ cmake -S .. -B . -G Ninja ($flags | str join ' ')" + ^cmake -S .. -B . -G Ninja ...$flags +} + +def cmakeBuild [attrs: record] { + let cmakeBuildDir = ($attrs | get -o cmakeBuildDir | default "build") + let cores = ($env | get -o NIX_BUILD_CORES | default "1") + + cd $cmakeBuildDir + print -e $"+ cmake --build . -j($cores)" + ^cmake --build . $"-j($cores)" +} + +def cmakeCheck [attrs: record] { + let cmakeBuildDir = ($attrs | get -o cmakeBuildDir | default "build") + let cores = ($env | get -o NIX_BUILD_CORES | default "1") + + cd $cmakeBuildDir + print -e $"+ ctest --output-on-failure -j($cores)" + ^ctest --output-on-failure $"-j($cores)" +} + +def cmakeInstall [attrs: record] { + let cmakeBuildDir = ($attrs | get -o cmakeBuildDir | default "build") + + cd $cmakeBuildDir + print -e "+ cmake --install ." + ^cmake --install . +} diff --git a/pkgs-many/cmake/v3/package.nix b/pkgs-many/cmake/v3/package.nix index d7a33de3..83acdc03 100644 --- a/pkgs-many/cmake/v3/package.nix +++ b/pkgs-many/cmake/v3/package.nix @@ -205,6 +205,7 @@ stdenv.mkDerivation (finalAttrs: { passthru = mkVariantPassthru variantArgs // { configurePhaseHook = ../configure-phase-hook.sh; + nushellHook = ../nushell-hook.nu; updateScript = gitUpdater { url = "https://gitlab.kitware.com/cmake/cmake.git"; rev-prefix = "v"; diff --git a/pkgs-many/cmake/v4/package.nix b/pkgs-many/cmake/v4/package.nix index 00113078..76574132 100644 --- a/pkgs-many/cmake/v4/package.nix +++ b/pkgs-many/cmake/v4/package.nix @@ -197,6 +197,7 @@ stdenv.mkDerivation (finalAttrs: { passthru = mkVariantPassthru variantArgs // { configurePhaseHook = ../configure-phase-hook.sh; + nushellHook = ../nushell-hook.nu; updateScript = gitUpdater { url = "https://gitlab.kitware.com/cmake/cmake.git"; rev-prefix = "v"; diff --git a/pkgs-many/meson/generic.nix b/pkgs-many/meson/generic.nix index cd922a19..57bb0dd8 100644 --- a/pkgs-many/meson/generic.nix +++ b/pkgs-many/meson/generic.nix @@ -190,6 +190,7 @@ python3.pkgs.buildPythonApplication rec { env.hostPlatform = stdenv.targetPlatform.system; passthru = { configurePhaseHook = ./setup-hook.sh; + nushellHook = ./nushell-hook.nu; ekapkgs-update.semver-strategy = "patch"; tests = { version = testers.testVersion { diff --git a/pkgs-many/meson/nushell-hook.nu b/pkgs-many/meson/nushell-hook.nu new file mode 100644 index 00000000..382e64ce --- /dev/null +++ b/pkgs-many/meson/nushell-hook.nu @@ -0,0 +1,68 @@ +# Meson nushell hook for mkEkaPackage +# +# Usage in mkEkaPackage: +# commands = scope: { +# inherit (scope) ninja pkg-config; +# mesonHook = scope.meson.nushellHook; +# }; +# +# Overrides configurePhase, buildPhase, checkPhase, and installPhase +# to use meson/ninja. + +def main [phase: string] { + let attrs = (open $env.NIX_ATTRS_JSON_FILE) + + match $phase { + "configure" => { mesonConfigure $attrs } + "build" => { mesonBuild $attrs } + "check" => { mesonCheck $attrs } + "install" => { mesonInstall $attrs } + _ => { print -e $"meson hook: unknown phase ($phase)" } + } +} + +def mesonConfigure [attrs: record] { + let mesonBuildDir = ($attrs | get -o mesonBuildDir | default "build") + let mesonFlags = ($attrs | get -o mesonFlags | default []) + let prefix = $env.out + let mesonBuildType = ($attrs | get -o mesonBuildType | default "release") + + mkdir $mesonBuildDir + + mut flags = [ + $"--prefix=($prefix)" + "--libdir=lib" + $"--buildtype=($mesonBuildType)" + "--default-library=shared" + "--wrap-mode=nodownload" + "--auto-features=enabled" + ] + + $flags = ($flags | append $mesonFlags) + + print -e $"+ meson setup ($mesonBuildDir) . ($flags | str join ' ')" + ^meson setup $mesonBuildDir . ...$flags +} + +def mesonBuild [attrs: record] { + let mesonBuildDir = ($attrs | get -o mesonBuildDir | default "build") + let cores = ($env | get -o NIX_BUILD_CORES | default "1") + + print -e $"+ ninja -C ($mesonBuildDir) -j($cores)" + ^ninja -C $mesonBuildDir $"-j($cores)" +} + +def mesonCheck [attrs: record] { + let mesonBuildDir = ($attrs | get -o mesonBuildDir | default "build") + let cores = ($env | get -o NIX_BUILD_CORES | default "1") + + print -e $"+ meson test -C ($mesonBuildDir) --no-rebuild --print-errorlogs" + ^meson test -C $mesonBuildDir --no-rebuild --print-errorlogs $"--num-processes=($cores)" +} + +def mesonInstall [attrs: record] { + let mesonBuildDir = ($attrs | get -o mesonBuildDir | default "build") + + print -e $"+ meson install -C ($mesonBuildDir) --no-rebuild" + ^meson install -C $mesonBuildDir --no-rebuild +} diff --git a/pkgs/jq/nushell.nix b/pkgs/jq/nushell.nix new file mode 100644 index 00000000..931ec95b --- /dev/null +++ b/pkgs/jq/nushell.nix @@ -0,0 +1,81 @@ +# jq — mkEkaPackage variant using the nushell builder +# +# This is a translation of default.nix from stdenv.mkDerivation (bash) +# to mkEkaPackage (nushell). Phase strings use nushell syntax. +{ + mkEkaPackage, + fetchurl, + lib, + onigurumaSupport ? true, +}: + +mkEkaPackage (finalAttrs: { + pname = "jq"; + version = "1.8.2"; + + # Note: do not use fetchpatch or fetchFromGitHub to keep this package available in __bootPackages + src = fetchurl { + url = "https://github.com/jqlang/jq/releases/download/jq-${finalAttrs.version}/jq-${finalAttrs.version}.tar.gz"; + hash = "sha256-cbjW6PX+gfbG0NEQ44kiUfbOdu0JWr0xXibm4Rk6868="; + }; + + outputs = [ + "bin" + "doc" + "man" + "dev" + "out" + ]; + + commands = scope: { + inherit (scope) removeReferencesTo bison; + autoreconfHook = scope.autoreconfHook; + }; + + libraries = + scope: + { + } + // lib.optionalAttrs onigurumaSupport { + inherit (scope) oniguruma; + }; + + configureFlags = [ + "--bindir=\${bin}/bin" + "--sbindir=\${bin}/bin" + "--datadir=\${doc}/share" + "--mandir=\${man}/share/man" + ] + ++ lib.optional (!onigurumaSupport) "--with-oniguruma=no"; + + # Upstream script that writes the version that's eventually compiled + # and printed in `jq --help` relies on a .git directory which our src + # doesn't keep. + preConfigure = '' + "#!/bin/sh" | save scripts/version + $"echo ($env.__attrs.version)" | save --append scripts/version + ^chmod +x scripts/version + ''; + + # paranoid mode: make sure we never use vendored version of oniguruma + # Note: it must be run after automake, or automake will complain + preBuild = '' + rm -rf ./vendor/oniguruma + ''; + + # jq binary includes the whole `configureFlags` in: + # https://github.com/jqlang/jq/commit/583e4a27188a2db097dd043dd203b9c106bba100 + # Strip unnecessary dependencies here to reduce closure size and break the + # dependency cycle: $dev also refers to $bin via propagated-build-outputs + postFixup = '' + ^remove-references-to -t $env.dev -t $env.man -t $env.doc $"($env.bin)/bin/jq" + ''; + + meta = { + description = "Lightweight and flexible command-line JSON processor"; + homepage = "https://jqlang.github.io/jq/"; + license = lib.licenses.mit; + platforms = lib.platforms.unix; + mainProgram = "jq"; + }; +}) diff --git a/pkgs/nushell/default.nix b/pkgs/nushell/default.nix new file mode 100644 index 00000000..2a170bd9 --- /dev/null +++ b/pkgs/nushell/default.nix @@ -0,0 +1,45 @@ +{ + lib, + stdenvNoCC, + fetchurl, + autoPatchelfHook, +}: + +let + version = "0.115.1"; + + sources = { + "x86_64-linux" = fetchurl { + url = "https://github.com/nushell/nushell/releases/download/${version}/nu-${version}-x86_64-unknown-linux-musl.tar.gz"; + hash = "sha256-ipciF33dQh66kntLquSuNBpNHDQ0UEvnsbF9mYuv3Yc="; + }; + "aarch64-linux" = fetchurl { + url = "https://github.com/nushell/nushell/releases/download/${version}/nu-${version}-aarch64-unknown-linux-musl.tar.gz"; + hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + }; + }; +in +stdenvNoCC.mkDerivation { + pname = "nushell"; + inherit version; + + src = + sources.${stdenvNoCC.hostPlatform.system} + or (throw "nushell: unsupported system ${stdenvNoCC.hostPlatform.system}"); + + dontFixup = true; + + installPhase = '' + install -Dm755 nu $out/bin/nu + ''; + + meta = { + description = "Modern shell written in Rust"; + homepage = "https://www.nushell.sh/"; + license = lib.licenses.mit; + platforms = [ + "x86_64-linux" + "aarch64-linux" + ]; + }; +} diff --git a/stdenv/generic/make-eka-package.nix b/stdenv/generic/make-eka-package.nix new file mode 100644 index 00000000..0a9458ee --- /dev/null +++ b/stdenv/generic/make-eka-package.nix @@ -0,0 +1,696 @@ +# mkEkaPackage — scope-based dependency declaration (EEP 0041) +# +# Unlike stdenv.mkDerivation, mkEkaPackage is a scope member, not attached to +# stdenv. Dependencies are declared as functions that receive the correct +# package scope, eliminating the need for spliced packages. +# +# See: https://github.com/ekala-project/eeps/blob/jonringer/mkekapackage/eeps/0041-mkekapackage.md + +{ + lib, + config, + stdenv, + cc ? stdenv.cc, + scopes, + nushell, +}: + +let + defaultCC = cc; +in + +let + inherit (lib) + attrValues + concatLists + extendDerivation + filter + filterAttrs + getDev + intersectAttrs + isAttrs + isBool + isDerivation + isFunction + isInt + isPath + isString + mapAttrs + mapNullable + optional + optionalString + optionals + toFunction + typeOf + unsafeDiscardStringContext + unsafeGetAttrPos + warn + ; + + inherit (lib.generators) toPretty; + inherit (lib.strings) sanitizeDerivationName; + + checkMeta = import ./check-meta.nix { + inherit lib config; + }; + + inherit (import ../lib/cmake.nix { inherit lib stdenv; }) makeCMakeFlags; + inherit (import ../lib/meson.nix { inherit lib stdenv; }) makeMesonFlags; + + knownHardeningFlags = [ + "bindnow" + "format" + "fortify" + "fortify3" + "strictflexarrays1" + "strictflexarrays3" + "shadowstack" + "nostrictaliasing" + "pacret" + "pic" + "relro" + "stackprotector" + "glibcxxassertions" + "libcxxhardeningfast" + "libcxxhardeningextensive" + "stackclashprotection" + "strictoverflow" + "trivialautovarinit" + "zerocallusedregs" + ]; + + doCheckByDefault = config.doCheckByDefault or false; + enableParallelBuildingByDefault = config.enableParallelBuildingByDefault or true; + contentAddressedByDefault = config.contentAddressedByDefault or false; + + inherit (stdenv) + hostPlatform + buildPlatform + targetPlatform + extraNativeBuildInputs + extraBuildInputs + extraSandboxProfile + __extraImpureHostDeps + ; + + buildPlatformSystem = buildPlatform.system; + buildIsDarwin = buildPlatform.isDarwin; + + inherit (hostPlatform) + isLinux + isWindows + isCygwin + isStatic + isMusl + ; + + useDefaultConfigurePlatforms = hostPlatform != buildPlatform || config.configurePlatformsByDefault; + defaultConfigurePlatforms = optionals useDefaultConfigurePlatforms [ + "build" + "host" + ]; + buildPlatformConfigureFlag = "--build=${buildPlatform.config}"; + hostPlatformConfigureFlag = "--host=${hostPlatform.config}"; + targetPlatformConfigureFlag = "--target=${targetPlatform.config}"; + defaultConfigurePlatformsFlags = optionals useDefaultConfigurePlatforms [ + buildPlatformConfigureFlag + hostPlatformConfigureFlag + ]; + + defaultStrictDeps = + if config.strictDepsByDefault or true then true else hostPlatform != buildPlatform; + canExecuteHostOnBuild = buildPlatform.canExecute hostPlatform; + + stdenvHasCC = stdenv.hasCC; + stdenvShell = stdenv.shell; + hostSuffixNecessary = hostPlatform != buildPlatform && stdenvHasCC; + stdenvHostSuffix = "-${hostPlatform.config}"; + stdenvStaticMarker = optionalString isStatic "-static"; + + nuBuilderSrc = builtins.path { + name = "nushell-builder"; + path = ../nushell-builder; + filter = path: _type: builtins.match ".*\\.nu$" path != null || _type == "directory"; + }; + + defaultBuilderArgs = [ + "--no-config-file" + (nuBuilderSrc + "/setup.nu") + ]; + + requiredSystemFeaturesShouldBeSet = + buildPlatform ? gcc.arch + && !(buildPlatform.isAarch64 && (buildPlatform.isDarwin || buildPlatform.gcc.arch == "armv8-a")); + gccArchFeature = [ "gccarch-${buildPlatform.gcc.arch}" ]; + + commonMeta = checkMeta.commonMeta hostPlatform; + assertValidity = checkMeta.assertValidity hostPlatform; + + unsafeDerivationToUntrackedOutpath = + drv: + if isDerivation drv && (!drv.__contentAddressed or false) then + unsafeDiscardStringContext drv.outPath + else + drv; + + makeOutputChecks = attrs: { + ${if (attrs ? disallowedReferences) then "disallowedReferences" else null} = + map unsafeDerivationToUntrackedOutpath attrs.disallowedReferences; + ${if (attrs ? disallowedRequisites) then "disallowedRequisites" else null} = + map unsafeDerivationToUntrackedOutpath attrs.disallowedRequisites; + ${if (attrs ? allowedReferences) then "allowedReferences" else null} = + mapNullable unsafeDerivationToUntrackedOutpath attrs.allowedReferences; + ${if (attrs ? allowedRequisites) then "allowedRequisites" else null} = + mapNullable unsafeDerivationToUntrackedOutpath attrs.allowedRequisites; + }; + + # Flatten a scope-receiving dependency function into a list. + # Calls fn with the scope, filters out nulls, applies getDev. + flattenDeps = + scope: fn: + let + raw = fn scope; + filtered = filterAttrs (_: v: v != null) raw; + validated = mapAttrs ( + name: v: + if isDerivation v || isPath v || isString v then + v + else + throw "mkEkaPackage dependency '${name}' is not a derivation, path, or string (got ${typeOf v})" + ) filtered; + in + map getDev (attrValues validated); + + mkEkaPackage = fnOrAttrs: makeDerivationExtensible (toFunction fnOrAttrs); + + # Resolve scope-receiving dependency functions so that finalAttrs.commands.foo + # returns the actual package. The raw function form is preserved in `prev` + # inside overrideAttrs (via `rattrs final`), so composition still works: + # pkg.overrideAttrs (prev: { commands = scope: prev.commands scope // { ... }; }) + resolveScoped = + attrs: + let + resolve = + name: scopeKey: + if attrs ? ${name} then + { + ${name} = + let + fn = attrs.${name}; + in + if isFunction fn then fn scopes.${scopeKey} else fn; + } + else + { }; + in + resolve "commands" "buildHost" + // resolve "libraries" "hostTarget" + // resolve "propagatedCommands" "buildHost" + // resolve "propagatedLibraries" "hostTarget" + // resolve "depsBuildBuild" "buildBuild" + // resolve "depsBuildTarget" "buildTarget" + // resolve "depsHostHost" "hostHost" + // resolve "depsTargetTarget" "targetTarget"; + + makeDerivationExtensible = + rattrs: + let + # rawArgs is the fixpoint: rattrs receives finalAttrs (with resolved deps) + # but returns the user's attrs (with raw functions). + rawArgs = rattrs (rawArgs // resolveScoped rawArgs // { inherit finalPackage overrideAttrs; }); + + # What mkDerivationSimple receives — raw function forms for dep attrs. + args = rawArgs; + + overrideAttrs = + f0: + makeDerivationExtensible ( + final: + let + prev = rattrs final; + thisOverlay = + if isFunction f0 then + let + fPrev = f0 prev; + in + if isFunction fPrev then f0 final prev else fPrev + else + f0; + in + ( + if + prev ? src + && thisOverlay ? version + && prev ? version + && !(thisOverlay ? src) + && !(thisOverlay.__intentionallyOverridingVersion or false) + then + warn ( + let + pos = unsafeGetAttrPos "version" thisOverlay; + in + '' + ${ + args.name or "${args.pname or ""}-${args.version or ""}" + } was overridden with `version` but not `src` at ${pos.file or ""}:${ + toString pos.line or "" + }:${toString pos.column or ""}. + '' + ) + else + x: x + ) + (prev // (removeAttrs thisOverlay [ "__intentionallyOverridingVersion" ])) + ); + + finalPackage = mkDerivationSimple overrideAttrs args; + in + finalPackage; + + mkDerivationSimple = + overrideAttrs: + { + # Scope-based dependency attributes (EEP 0041) + commands ? _: { }, + libraries ? _: { }, + propagatedCommands ? _: { }, + propagatedLibraries ? _: { }, + depsBuildBuild ? _: { }, + depsBuildTarget ? _: { }, + depsHostHost ? _: { }, + depsTargetTarget ? _: { }, + + # CC attribute — per-package compiler selection + cc ? "__default__", + + # Standard mkDerivation attributes + configureFlags ? [ ], + configurePlatforms ? defaultConfigurePlatforms, + doCheck ? doCheckByDefault, + doInstallCheck ? doCheckByDefault, + strictDeps ? defaultStrictDeps, + enableParallelBuilding ? enableParallelBuildingByDefault, + separateDebugInfo ? false, + outputs ? [ "out" ], + hardeningEnable ? [ ], + hardeningDisable ? [ ], + patches ? [ ], + __contentAddressed ? (!attrs ? outputHash) && contentAddressedByDefault, + __structuredAttrs ? true, + + cmakeFlags ? [ ], + mesonFlags ? [ ], + meta ? { }, + passthru ? { }, + pos ? ( + if attrs.meta.description or null != null then + unsafeGetAttrPos "description" attrs.meta + else if attrs.version or null != null then + unsafeGetAttrPos "version" attrs + else + unsafeGetAttrPos "name" attrs + ), + env ? { }, + + ... + }@attrs: + let + # Resolve the CC + # Note: `cc` here is attrs.cc (the per-package override), while + # `defaultCC` is the module-level default compiler (stdenv.cc). + resolvedCC = + if cc == "__default__" then + defaultCC + else if cc == null then + null + else if isFunction cc then + cc scopes.buildHost + else + cc; + + actualCC = resolvedCC; + + hasCC = actualCC != null; + + defaultHardeningFlags = + if hasCC then actualCC.defaultHardeningFlags or knownHardeningFlags else [ ]; + + # Flatten scope-based dependencies + commandsAttrs = commands scopes.buildHost; + # Merge CC into commands (CC takes lowest priority — user commands win) + mergedCommandsAttrs = (if hasCC then { cc = actualCC; } else { }) // commandsAttrs; + flatCommands = + map getDev (attrValues (filterAttrs (_: v: v != null) mergedCommandsAttrs)) + ++ optional separateDebugInfo' ../setup-hooks/separate-debug-info.sh + ++ optional isWindows ../setup-hooks/win-dll-link.sh; + + librariesAttrs = libraries scopes.hostTarget; + flatLibraries = map getDev (attrValues (filterAttrs (_: v: v != null) librariesAttrs)); + + propagatedCommandsAttrs = propagatedCommands scopes.buildHost; + flatPropagatedCommands = map getDev ( + attrValues (filterAttrs (_: v: v != null) propagatedCommandsAttrs) + ); + + propagatedLibrariesAttrs = propagatedLibraries scopes.hostTarget; + flatPropagatedLibraries = map getDev ( + attrValues (filterAttrs (_: v: v != null) propagatedLibrariesAttrs) + ); + + flatDepsBuildBuild = flattenDeps scopes.buildBuild depsBuildBuild; + flatDepsBuildTarget = flattenDeps scopes.buildTarget depsBuildTarget; + flatDepsHostHost = flattenDeps scopes.hostHost depsHostHost; + flatDepsTargetTarget = flattenDeps scopes.targetTarget depsTargetTarget; + + doCheck' = doCheck && canExecuteHostOnBuild; + doInstallCheck' = doInstallCheck && canExecuteHostOnBuild; + + separateDebugInfo' = separateDebugInfo && isLinux; + outputs' = if separateDebugInfo' then outputs ++ [ "debug" ] else outputs; + + attrsToRemove = [ + "commands" + "libraries" + "propagatedCommands" + "propagatedLibraries" + "depsBuildBuild" + "depsBuildTarget" + "depsHostHost" + "depsTargetTarget" + "cc" + "meta" + "passthru" + "pos" + "env" + "cmakeFlags" + "mesonFlags" + "configureFlags" + "configurePlatforms" + "doCheck" + "doInstallCheck" + "strictDeps" + "enableParallelBuilding" + "separateDebugInfo" + "outputs" + "hardeningEnable" + "hardeningDisable" + "patches" + "__contentAddressed" + "__structuredAttrs" + ]; + + derivationArg = removeAttrs attrs attrsToRemove // { + ${if (attrs ? name || (attrs ? pname && attrs ? version)) then "name" else null} = + let + hostSuffix = optionalString (hostSuffixNecessary && (!(attrs ? outputHash))) stdenvHostSuffix; + staticMarker = stdenvStaticMarker; + in + sanitizeDerivationName ( + if attrs ? name then + attrs.name + hostSuffix + else + assert + (attrs ? version && attrs.version != null) || throw "The `version` attribute cannot be null."; + "${attrs.pname}${staticMarker}${hostSuffix}-${attrs.version}" + ); + + builder = attrs.realBuilder or "${nushell}/bin/nu"; + args = attrs.args or defaultBuilderArgs; + inherit stdenv; + system = buildPlatformSystem; + __ignoreNulls = true; + __structuredAttrs = true; + inherit strictDeps; + + # Map scope-based deps to the standard derivation dependency slots + depsBuildBuild = flatDepsBuildBuild; + nativeBuildInputs = flatCommands; + depsBuildTarget = flatDepsBuildTarget; + depsHostHost = flatDepsHostHost; + buildInputs = flatLibraries; + depsTargetTarget = flatDepsTargetTarget; + + depsBuildBuildPropagated = [ ]; + propagatedNativeBuildInputs = flatPropagatedCommands; + depsBuildTargetPropagated = [ ]; + depsHostHostPropagated = [ ]; + propagatedBuildInputs = flatPropagatedLibraries; + depsTargetTargetPropagated = [ ]; + + configureFlags = + configureFlags + ++ ( + if configurePlatforms == defaultConfigurePlatforms then + defaultConfigurePlatformsFlags + else + optional (lib.elem "build" configurePlatforms) buildPlatformConfigureFlag + ++ optional (lib.elem "host" configurePlatforms) hostPlatformConfigureFlag + ++ optional (lib.elem "target" configurePlatforms) targetPlatformConfigureFlag + ); + + inherit patches; + + doCheck = doCheck'; + doInstallCheck = doInstallCheck'; + outputs = outputs'; + + ${if __contentAddressed then "__contentAddressed" else null} = __contentAddressed; + ${if __contentAddressed then "outputHashAlgo" else null} = attrs.outputHashAlgo or "sha256"; + ${if __contentAddressed then "outputHashMode" else null} = attrs.outputHashMode or "recursive"; + + ${if enableParallelBuilding then "enableParallelBuilding" else null} = enableParallelBuilding; + ${if enableParallelBuilding then "enableParallelChecking" else null} = + attrs.enableParallelChecking or true; + ${if enableParallelBuilding then "enableParallelInstalling" else null} = + attrs.enableParallelInstalling or true; + + ${ + if (hardeningDisable != [ ] || hardeningEnable != [ ] || isMusl) then + "NIX_HARDENING_ENABLE" + else + null + } = + lib.concatStringsSep " " ( + if lib.elem "all" hardeningDisable then + [ ] + else + filter ( + flag: + !(lib.elem flag hardeningDisable) + && (flag == "fortify3" -> !lib.elem "fortify" hardeningDisable) + && (flag == "strictflexarrays3" -> !lib.elem "strictflexarrays1" hardeningDisable) + && (flag == "libcxxhardeningextensive" -> !lib.elem "libcxxhardeningfast" hardeningDisable) + ) (defaultHardeningFlags ++ hardeningEnable) + ); + + ${if requiredSystemFeaturesShouldBeSet then "requiredSystemFeatures" else null} = + attrs.requiredSystemFeatures or [ ] ++ gccArchFeature; + + # Darwin-specific + ${if buildIsDarwin then "__darwinAllowLocalNetworking" else null} = + attrs.__darwinAllowLocalNetworking or false; + ${if buildIsDarwin then "__sandboxProfile" else null} = + let + allDeps = concatLists [ + flatDepsBuildBuild + flatCommands + flatDepsBuildTarget + flatDepsHostHost + flatLibraries + flatDepsTargetTarget + ]; + allPropDeps = concatLists [ + flatPropagatedCommands + flatPropagatedLibraries + ]; + computedSandboxProfile = lib.concatMap (input: input.__propagatedSandboxProfile or [ ]) ( + extraNativeBuildInputs ++ extraBuildInputs ++ allDeps + ); + computedPropagatedSandboxProfile = lib.concatMap ( + input: input.__propagatedSandboxProfile or [ ] + ) allPropDeps; + profiles = [ + extraSandboxProfile + ] + ++ computedSandboxProfile + ++ computedPropagatedSandboxProfile + ++ [ + (attrs.propagatedSandboxProfile or "") + (attrs.sandboxProfile or "") + ]; + in + lib.concatStringsSep "\n" (filter (x: x != "") (lib.unique profiles)); + ${if buildIsDarwin then "__impureHostDeps" else null} = + let + allDeps = concatLists [ + flatDepsBuildBuild + flatCommands + flatDepsBuildTarget + flatDepsHostHost + flatLibraries + flatDepsTargetTarget + ]; + allPropDeps = concatLists [ + flatPropagatedCommands + flatPropagatedLibraries + ]; + in + lib.unique ( + lib.concatMap (input: input.__propagatedImpureHostDeps or [ ]) ( + extraNativeBuildInputs ++ extraBuildInputs ++ allDeps + ) + ) + ++ lib.unique (lib.concatMap (input: input.__propagatedImpureHostDeps or [ ]) allPropDeps) + ++ (attrs.__propagatedImpureHostDeps or [ ]) + ++ (attrs.__impureHostDeps or [ ]) + ++ __extraImpureHostDeps + ++ [ + "/dev/zero" + "/dev/random" + "/dev/urandom" + "/bin/sh" + ]; + + # Windows/Cygwin + ${if isWindows || isCygwin then "allowedImpureDLLs" else null} = + (attrs.allowedImpureDLLs or [ ]) ++ optionals isCygwin [ "KERNEL32.dll" ]; + + # Structured attrs output checks + ${if __structuredAttrs then "outputChecks" else null} = + let + attrsOutputChecks = makeOutputChecks attrs; + attrsOutputChecksFiltered = filterAttrs (_: v: v != null) attrsOutputChecks; + in + if + !attrs ? outputs + && !attrs ? outputChecks + && (attrsOutputChecks == { } || attrsOutputChecksFiltered == { }) + then + if separateDebugInfo' then + { + out = { }; + debug = { }; + } + else + { out = { }; } + else + lib.listToAttrs ( + map (name: { + inherit name; + value = + let + raw = lib.zipAttrsWith (_: concatLists) [ + attrsOutputChecksFiltered + (makeOutputChecks (attrs.outputChecks.${name} or { })) + ]; + in + if separateDebugInfo' && name == "debug" then + removeAttrs raw [ + "allowedReferences" + "allowedRequisites" + "disallowedReferences" + "disallowedRequisites" + ] + else + raw; + }) outputs' + ); + + cmakeFlags = makeCMakeFlags attrs; + mesonFlags = makeMesonFlags attrs; + }; + + env' = + if attrs ? meta.mainProgram then env // { NIX_MAIN_PROGRAM = attrs.meta.mainProgram; } else env; + + checkedEnv = + let + overlappingArgs = intersectAttrs env' derivationArg; + in + assert + (isAttrs env && !isDerivation env) + || throw "`env` must be an attribute set of environment variables."; + assert + (overlappingArgs == { }) + || throw ( + let + errors = lib.concatMapStringsSep "\n" ( + name: + " - ${name}: in `env`: ${toPretty { } env'.${name}}; in derivation arguments: ${ + toPretty { } derivationArg.${name} + }" + ) (lib.attrNames overlappingArgs); + in + "The `env` attribute set cannot contain any attributes passed to derivation. The following attributes are overlapping:\n${errors}" + ); + mapAttrs ( + n: v: + assert + (isString v || isBool v || isInt v || isDerivation v) + || throw "The `env` attribute set can only contain derivation, string, boolean or integer attributes. The `${n}` attribute is of type ${typeOf v}."; + v + ) env'; + + validity = assertValidity { inherit meta attrs; }; + + meta = commonMeta { + inherit validity attrs pos; + references = flatCommands ++ flatLibraries ++ flatPropagatedCommands ++ flatPropagatedLibraries; + }; + + # Expose the commands/libraries attrsets on finalPackage for introspection + # and for use in build phases via finalAttrs.commands.foo + commandsPassthru = { + inherit commandsAttrs librariesAttrs; + }; + + attrsToRemoveLast = [ + "outputHashAlgo" + "outputHash" + "outputHashMode" + "allowedReferences" + "allowedRequisites" + "disallowedReferences" + "disallowedRequisites" + "outputChecks" + ]; + + in + extendDerivation validity.handled ( + { + inputDerivation = derivation ( + removeAttrs derivationArg attrsToRemoveLast + // { + name = "inputDerivation" + optionalString (derivationArg ? name) "-${derivationArg.name}"; + outputs = [ "out" ]; + requiredSystemFeatures = [ ]; + _derivation_original_builder = derivationArg.builder; + _derivation_original_args = derivationArg.args; + builder = stdenvShell; + args = [ + "-c" + '' + out="${builtins.placeholder "out"}" + if [ -e "$NIX_ATTRS_SH_FILE" ]; then . "$NIX_ATTRS_SH_FILE"; fi + declare -p > $out + for var in $passAsFile; do + pathVar="''${var}Path" + printf "%s" "$(< "''${!pathVar}")" >> $out + done + '' + ]; + } + ); + + inherit passthru overrideAttrs; + inherit meta; + # Expose dependency attrsets for introspection + commands = mergedCommandsAttrs; + libraries = librariesAttrs; + } + // passthru + ) (derivation (derivationArg // checkedEnv)); +in +{ + inherit mkEkaPackage; +} diff --git a/stdenv/nushell-builder/hooks/compress-man-pages.nu b/stdenv/nushell-builder/hooks/compress-man-pages.nu new file mode 100644 index 00000000..8586a272 --- /dev/null +++ b/stdenv/nushell-builder/hooks/compress-man-pages.nu @@ -0,0 +1,31 @@ +# Compress man pages — nushell equivalent of setup-hooks/compress-man-pages.sh + +export def compressManPages [] { + let attrs = $env.__attrs + if (($attrs | get -o dontGzipMan | default false) == true) { return } + + let prefix = $env.prefix + let manDir = $"($prefix)/share/man" + + if not ($manDir | path exists) { return } + + # Find and gzip uncompressed man pages + let files = (do { ^find $manDir -type f -not -name "*.gz" -not -name "*.bz2" -not -name "*.xz" } | complete | get stdout) + + $files | lines | where {|f| $f != ""} | each {|f| + try { ^gzip -9nf $f } catch { } + } + + # Fix symlinks pointing to uncompressed files + let links = (do { ^find $manDir -type l } | complete | get stdout) + + $links | lines | where {|l| $l != ""} | each {|link| + let target = (do { ^readlink $link } | complete | get stdout | str trim) + if not ($target | str ends-with ".gz") { + # Remove old symlink and create new one pointing to .gz version + ^rm $link + ^ln -s $"($target).gz" $"($link).gz" + } + } + null +} diff --git a/stdenv/nushell-builder/hooks/move-docs.nu b/stdenv/nushell-builder/hooks/move-docs.nu new file mode 100644 index 00000000..8d84adf1 --- /dev/null +++ b/stdenv/nushell-builder/hooks/move-docs.nu @@ -0,0 +1,24 @@ +# Move docs — nushell equivalent of setup-hooks/move-docs.sh +# +# Moves $prefix/{man,doc,info} to $prefix/share/{man,doc,info} + +export def moveDocs [] { + let prefix = $env.prefix + let forceShare = ($env.__attrs | get -o forceShare | default ["man" "doc" "info"]) + + for dir in $forceShare { + let src = $"($prefix)/($dir)" + let dest = $"($prefix)/share/($dir)" + + if ($src | path exists) and ($src | path type) == "dir" { + mkdir ($dest | path dirname) + if ($dest | path exists) { + # Merge into existing directory + ^cp -rn $"($src)/." $dest + ^rm -rf $src + } else { + ^mv $src $dest + } + } + } +} diff --git a/stdenv/nushell-builder/hooks/move-lib64.nu b/stdenv/nushell-builder/hooks/move-lib64.nu new file mode 100644 index 00000000..56a870e1 --- /dev/null +++ b/stdenv/nushell-builder/hooks/move-lib64.nu @@ -0,0 +1,22 @@ +# Move lib64 — nushell equivalent of setup-hooks/move-lib64.sh +# +# Consolidates lib64/ into lib/ with a symlink. Different bitnesses get +# separate store paths in Nix, so lib64 is unnecessary. + +export def moveLib64 [] { + let attrs = $env.__attrs + if (($attrs | get -o dontMoveLib64 | default false) == true) { return } + + let prefix = $env.prefix + let lib64 = $"($prefix)/lib64" + + if not ($lib64 | path exists) { return } + if ($lib64 | path type) == "symlink" { return } + + let lib = $"($prefix)/lib" + mkdir $lib + + ^cp -rn $"($lib64)/." $lib + ^rm -rf $lib64 + ^ln -s lib $lib64 +} diff --git a/stdenv/nushell-builder/hooks/move-sbin.nu b/stdenv/nushell-builder/hooks/move-sbin.nu new file mode 100644 index 00000000..5264fc25 --- /dev/null +++ b/stdenv/nushell-builder/hooks/move-sbin.nu @@ -0,0 +1,21 @@ +# Move sbin — nushell equivalent of setup-hooks/move-sbin.sh +# +# Consolidates sbin/ into bin/ with a symlink. + +export def moveSbin [] { + let attrs = $env.__attrs + if (($attrs | get -o dontMoveSbin | default false) == true) { return } + + let prefix = $env.prefix + let sbin = $"($prefix)/sbin" + + if not ($sbin | path exists) { return } + if ($sbin | path type) == "symlink" { return } + + let bin = $"($prefix)/bin" + mkdir $bin + + ^cp -rn $"($sbin)/." $bin + ^rm -rf $sbin + ^ln -s bin $sbin +} diff --git a/stdenv/nushell-builder/hooks/multiple-outputs.nu b/stdenv/nushell-builder/hooks/multiple-outputs.nu new file mode 100644 index 00000000..f131473d --- /dev/null +++ b/stdenv/nushell-builder/hooks/multiple-outputs.nu @@ -0,0 +1,48 @@ +# Multiple outputs — nushell equivalent of setup-hooks/multiple-outputs.sh +# +# Routes files to appropriate outputs (dev, lib, bin, man, etc.) + +export def multiOutputSetup [] { + let outputs = ($env.__attrs.outputs | columns) + + # Set up output variable defaults + # dev output: includes, pkgconfig, cmake configs + let outputDev = if "dev" in $outputs { "dev" } else { "out" } + let outputBin = if "bin" in $outputs { "bin" } else { "out" } + let outputLib = if "lib" in $outputs { "lib" } else { "out" } + let outputDoc = if "doc" in $outputs { "doc" } else if "out" in $outputs { "out" } else { "out" } + let outputMan = if "man" in $outputs { "man" } else { $outputDoc } + let outputInfo = if "info" in $outputs { "info" } else { $outputDoc } + let outputInclude = if "include" in $outputs { "include" } else { $outputDev } + + load-env { + outputDev: $outputDev + outputBin: $outputBin + outputLib: $outputLib + outputDoc: $outputDoc + outputMan: $outputMan + outputInfo: $outputInfo + outputInclude: $outputInclude + } +} + +# Move files matching a pattern from one output to another +export def moveToOutput [pattern: string, targetOutput: string] { + let src = $env.out + let dest = (getOutputPath $targetOutput) + + if $src == $dest { return } + + let files = (glob $"($src)/($pattern)") + for file in $files { + let rel = ($file | str replace $src "") + let destFile = $"($dest)($rel)" + let destDir = ($destFile | path dirname) + mkdir $destDir + ^mv $file $destFile + } +} + +def getOutputPath [name: string]: nothing -> string { + $env.__attrs.outputs | get $name +} diff --git a/stdenv/nushell-builder/hooks/patch-shebangs.nu b/stdenv/nushell-builder/hooks/patch-shebangs.nu new file mode 100644 index 00000000..9724a748 --- /dev/null +++ b/stdenv/nushell-builder/hooks/patch-shebangs.nu @@ -0,0 +1,85 @@ +# Patch shebangs — nushell equivalent of setup-hooks/patch-shebangs.sh +# +# Rewrites #! interpreter paths in scripts to absolute store paths. + +export def patchShebangsAuto [outputPath: string] { + let attrs = $env.__attrs + if (($attrs | get -o dontPatchShebangs | default false) == true) { return } + + patchShebangs $outputPath +} + +export def patchShebangs [dir: string] { + if not ($dir | path exists) { return } + + let path = ($env | get -o PATH | default "") + let hostPath = ($env | get -o HOST_PATH | default "") + + # Find all regular executable files + let files = (do { ^find $dir -type f -executable -print0 } | complete | get stdout) + if ($files | str trim) == "" { return } + + $files | split row "\u{0}" | where {|f| $f != ""} | each {|f| + patchShebang $f $path $hostPath + } + null +} + +def patchShebang [file: string, path: string, hostPath: string] { + # Read first line and check for shebang + let firstLine = try { open $file --raw | lines | first } catch { return } + if not ($firstLine | str starts-with "#!") { return } + + let shebang = ($firstLine | str substring 2.. | str trim) + if $shebang == "" { return } + + # Parse the shebang + let parts = ($shebang | split row " " | where {|p| $p != ""}) + let interpreter = ($parts | first) + let rest = ($parts | skip 1) + + # Skip if already a store path + let nixStore = ($env | get -o NIX_STORE | default "/nix/store") + if ($interpreter | str starts-with $nixStore) { return } + + # Handle /usr/bin/env + mut newInterpreter = "" + if $interpreter == "/usr/bin/env" and ($rest | length) > 0 { + let cmd = ($rest | first) + $newInterpreter = (findOnPath $cmd $path $hostPath) + if $newInterpreter == "" { return } + # Rewrite to direct interpreter path + let newShebang = $"#!($newInterpreter) ($rest | skip 1 | str join ' ')" | str trim + rewriteShebang $file $firstLine $newShebang + } else { + # Direct interpreter path + let cmd = ($interpreter | path basename) + $newInterpreter = (findOnPath $cmd $path $hostPath) + if $newInterpreter == "" { return } + let newShebang = $"#!($newInterpreter) ($rest | str join ' ')" | str trim + rewriteShebang $file $firstLine $newShebang + } +} + +def findOnPath [cmd: string, path: string, hostPath: string]: nothing -> string { + # Search PATH first, then HOST_PATH + for searchPath in [$path $hostPath] { + if $searchPath == "" { continue } + for dir in ($searchPath | split row ":") { + let candidate = $"($dir)/($cmd)" + if ($candidate | path exists) { + return $candidate + } + } + } + "" +} + +def rewriteShebang [file: string, oldLine: string, newLine: string] { + if $oldLine == $newLine { return } + print -e $"patching shebang of ($file): ($oldLine) -> ($newLine)" + + let content = (open $file --raw) + let newContent = ($content | str replace $oldLine $newLine) + $newContent | save -f $file +} diff --git a/stdenv/nushell-builder/hooks/propagated-deps.nu b/stdenv/nushell-builder/hooks/propagated-deps.nu new file mode 100644 index 00000000..088addd2 --- /dev/null +++ b/stdenv/nushell-builder/hooks/propagated-deps.nu @@ -0,0 +1,33 @@ +# Record propagated dependencies — nushell equivalent of recordPropagatedDependencies() +# +# Writes nix-support/propagated-* files in the standard format so downstream +# bash-based stdenv.mkDerivation packages can consume them. + +export def recordPropagatedDeps [] { + let attrs = $env.__attrs + + # Map from attr name to nix-support file name + let depMap = [ + [attr file]; + [depsBuildBuildPropagated propagated-build-build-deps] + [propagatedNativeBuildInputs propagated-native-build-inputs] + [depsBuildTargetPropagated propagated-build-target-deps] + [depsHostHostPropagated propagated-host-host-deps] + [propagatedBuildInputs propagated-build-inputs] + [depsTargetTargetPropagated propagated-target-target-deps] + ] + + # Determine dev output + let outputDev = ($env | get -o outputDev | default "out") + let devPath = ($attrs.outputs | get $outputDev) + + for row in $depMap { + let deps = ($attrs | get -o $row.attr | default []) + if ($deps | length) > 0 { + let supportDir = $"($devPath)/nix-support" + mkdir $supportDir + let content = ($deps | str join " ") + $content | save -f $"($supportDir)/($row.file)" + } + } +} diff --git a/stdenv/nushell-builder/hooks/strip.nu b/stdenv/nushell-builder/hooks/strip.nu new file mode 100644 index 00000000..3a718b5d --- /dev/null +++ b/stdenv/nushell-builder/hooks/strip.nu @@ -0,0 +1,65 @@ +# Strip binaries and libraries — nushell equivalent of setup-hooks/strip.sh + +export def doStrip [outputPath: string] { + let attrs = $env.__attrs + + if (($attrs | get -o dontStrip | default false) == true) { return } + + let strip = ($env | get -o STRIP | default "strip") + let ranlib = ($env | get -o RANLIB | default "ranlib") + let cores = ($env | get -o NIX_BUILD_CORES | default "1") + + let stripDebugFlags = ($attrs | get -o stripDebugFlags | default ["-S" "-p"]) + let stripAllFlags = ($attrs | get -o stripAllFlags | default ["-s" "-p"]) + + # Directories to strip debug info from (libraries) + let stripDebugDirs = ($attrs | get -o stripDebugList | default ["lib" "lib32" "lib64" "libexec" "bin" "sbin"]) + # Directories to fully strip (executables) + let stripAllDirs = ($attrs | get -o stripAllList | default []) + + # Strip debug info from libraries + for dir in $stripDebugDirs { + let fullDir = $"($outputPath)/($dir)" + if ($fullDir | path exists) { + stripDir $fullDir $strip $stripDebugFlags $cores + } + } + + # Fully strip executables + for dir in $stripAllDirs { + let fullDir = $"($outputPath)/($dir)" + if ($fullDir | path exists) { + stripDir $fullDir $strip $stripAllFlags $cores + } + } + + # Restore archive indexes + let libDir = $"($outputPath)/lib" + if ($libDir | path exists) { + glob $"($libDir)/**/*.a" | each {|f| + do { ^$ranlib $f } | complete | ignore + } + } +} + +def stripDir [dir: string, strip: string, flags: list, cores: string] { + # Find ELF files and strip them + let files = (do { + ^find $dir -type f -not -path "*/lib/debug/*" -print0 + } | complete | get stdout) + + if ($files | str trim) == "" { return } + + # Process files + $files | split row "\u{0}" | where {|f| $f != ""} | each {|f| + # Check if file is ELF by reading magic bytes + let isElf = try { + let bytes = (open $f --raw | bytes at 0..4) + ($bytes | encode hex) == "7f454c46" # \x7fELF + } catch { false } + if $isElf { + try { ^$strip ...$flags $f } catch { } + } + } + null +} diff --git a/stdenv/nushell-builder/setup.nu b/stdenv/nushell-builder/setup.nu new file mode 100644 index 00000000..b2e4a9a9 --- /dev/null +++ b/stdenv/nushell-builder/setup.nu @@ -0,0 +1,746 @@ +# mkEkaPackage nushell builder — replaces source-stdenv.sh + setup.sh + default-builder.sh +# +# Entry point for all mkEkaPackage derivations. Reads structured attrs from +# $NIX_ATTRS_JSON_FILE, sets up the build environment, activates dependencies, +# detects nushell hooks from nativeBuildInputs, and runs the standard phase +# sequence. + +use hooks/strip.nu +use hooks/patch-shebangs.nu +use hooks/multiple-outputs.nu +use hooks/compress-man-pages.nu +use hooks/move-docs.nu +use hooks/move-lib64.nu +use hooks/move-sbin.nu +use hooks/propagated-deps.nu + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +# Log a phase start in Nix structured-log format +def note [kind: string, msg: string = ""] { + let ev = if $kind == "phase" { + {action: setPhase, phase: $msg} + } else { + {action: msg, level: 3, msg: $"($kind): ($msg)"} + } + print -e $"@nix ($ev | to json -r)" +} + +# Run all hooks in a named hook array (e.g. __preConfigureHooks). +# Hook arrays are stored as lists of closures in $env. +def --env runHook [name: string] { + let hookVar = $"__($name)Hooks" + let hooks = ($env | get -o $hookVar | default []) + for hook in $hooks { + do $hook + } +} + +# Add a path to a colon-separated env var if the path exists +def --env addToSearchPath [varName: string, dir: string] { + if ($dir | path exists) { + let current = ($env | get -o $varName | default "") + if $current == "" { + load-env {$varName: $dir} + } else { + load-env {$varName: $"($current):($dir)"} + } + } +} + +# Get all output names from the attrs +def getAllOutputNames [] { + $env.__attrs.outputs | columns +} + +# Get the store path for a given output name +def getOutput [name: string] { + $env.__attrs.outputs | get $name +} + +# Get an attribute from attrs with a default +def attr [name: string, default: any = null] { + $env.__attrs | get -o $name | default $default +} + +# Check if a flag attribute is truthy +def attrBool [name: string, default: bool = false] { + let val = (attr $name) + if $val == null { + $default + } else if ($val | describe) == "bool" { + $val + } else if ($val | describe) == "int" { + $val != 0 + } else if ($val | describe) == "string" { + $val != "" and $val != "0" + } else { + $default + } +} + +# Evaluate a nushell code string (for inline phase overrides) +def --env evalNu [code: string] { + let nuBin = ($nu.current-exe) + ^$nuBin --no-config-file -c $code +} + +# Execute an external command with logging +def --env x [cmd: string, ...args: string] { + print -e $"+ ($cmd) ($args | str join ' ')" + ^$cmd ...$args +} + +# --------------------------------------------------------------------------- +# Environment setup +# --------------------------------------------------------------------------- + +def --env setupEnvironment [] { + let attrs = $env.__attrs + + # Export outputs as env vars + for col in ($attrs.outputs | columns) { + load-env {$col: ($attrs.outputs | get $col)} + } + + # Set prefix to primary output + load-env {prefix: $env.out} + + # Reproducibility + load-env { + SOURCE_DATE_EPOCH: "315532800" + TZ: "UTC" + LC_ALL: "C.UTF-8" + ZERO_AR_DATE: "1" + PERL_HASH_SEED: "0" + PYTHONHASHSEED: "0" + KBUILD_BUILD_TIMESTAMP: "@315532800" + KBUILD_BUILD_USER: "nixbld" + KBUILD_BUILD_HOST: "localhost" + } + + # Writable HOME + let home = $"($env.NIX_BUILD_TOP)/homeless-shelter" + mkdir $home + load-env { + HOME: $home + XDG_CACHE_HOME: $"($home)/.cache" + XDG_DATA_HOME: $"($home)/.local/share" + XDG_CONFIG_HOME: $"($home)/.config" + TMPDIR: $env.NIX_BUILD_TOP + } + + # NIX_BUILD_CORES + let cores = ($env | get -o NIX_BUILD_CORES | default "1") + let coresInt = if ($cores | into int) <= 0 { + try { ^nproc | str trim | into int } catch { 1 } + } else { + $cores | into int + } + load-env {NIX_BUILD_CORES: ($coresInt | into string)} + + # User-specified env attributes + let userEnv = ($attrs | get -o env | default {}) + if ($userEnv | describe) == "record" and ($userEnv | columns | length) > 0 { + # Convert all values to strings for the environment + let envRecord = ($userEnv | items {|k, v| + {$k: (if ($v | describe) == "bool" { + if $v { "1" } else { "" } + } else { + $v | into string + })} + } | reduce -f {} {|it, acc| $acc | merge $it}) + load-env $envRecord + } + + # Initial PATH from stdenv (coreutils, findutils, etc.) + # Parse the initialPath from the stdenv setup file + let stdenvPath = (attr stdenv) + if $stdenvPath != null { + let setupFile = $"($stdenvPath)/setup" + if ($setupFile | path exists) { + let setupContent = (open --raw $setupFile) + let initialPathLine = (try { $setupContent | lines | where {|l| $l | str starts-with "initialPath="} | first } catch { null }) + if $initialPathLine != null { + let initialPath = ($initialPathLine | str replace 'initialPath="' '' | str replace '"' '' | str trim) + let pathEntries = ($initialPath | split row " " | each {|p| $"($p)/bin"} | where {|p| $p | path exists}) + let existingPath = ($env | get -o PATH | default "") + let newPath = if $existingPath != "" { + $"($existingPath):($pathEntries | str join ':')" + } else { + ($pathEntries | str join ":") + } + load-env {PATH: $newPath} + } + } + } + + # SSL certificates + let certFile = ($env | get -o NIX_SSL_CERT_FILE | default "") + if $certFile != "" { + load-env { + SSL_CERT_FILE: $certFile + GIT_SSL_CAINFO: $certFile + CURL_CA_BUNDLE: $certFile + } + } +} + +# --------------------------------------------------------------------------- +# Dependency activation +# --------------------------------------------------------------------------- + +def --env activateDependencies [] { + let attrs = $env.__attrs + let strictDeps = (attrBool strictDeps true) + + # Initialize hook arrays + load-env { + __preUnpackHooks: [] + __postUnpackHooks: [] + __prePatchHooks: [] + __postPatchHooks: [] + __preConfigureHooks: [] + __postConfigureHooks: [] + __preBuildHooks: [] + __postBuildHooks: [] + __preCheckHooks: [] + __postCheckHooks: [] + __preInstallHooks: [] + __postInstallHooks: [] + __preFixupHooks: [] + __postFixupHooks: [] + __fixupOutputHooks: [] + __preInstallCheckHooks: [] + __postInstallCheckHooks: [] + } + + # Build PATH from nativeBuildInputs (commands) + let nativeBuildInputs = ($attrs | get -o nativeBuildInputs | default []) + let buildInputs = ($attrs | get -o buildInputs | default []) + let depsBuildBuild = ($attrs | get -o depsBuildBuild | default []) + + # nativeBuildInputs always go on PATH + let pathDirs = ($nativeBuildInputs | each {|dep| + let binDir = $"($dep)/bin" + if ($binDir | path exists) { $binDir } else { null } + } | compact) + + # depsBuildBuild also go on PATH + let pathDirsBB = ($depsBuildBuild | each {|dep| + let binDir = $"($dep)/bin" + if ($binDir | path exists) { $binDir } else { null } + } | compact) + + # buildInputs only go on PATH if not strictDeps + let pathDirsBI = if not $strictDeps { + $buildInputs | each {|dep| + let binDir = $"($dep)/bin" + if ($binDir | path exists) { $binDir } else { null } + } | compact + } else { [] } + + let allPathDirs = ($pathDirsBB ++ $pathDirs ++ $pathDirsBI) + let existingPath = ($env | get -o PATH | default "") + let newPath = if ($allPathDirs | length) > 0 { + let joined = ($allPathDirs | str join ":") + if $existingPath != "" { $"($joined):($existingPath)" } else { $joined } + } else { + $existingPath + } + load-env {PATH: $newPath} + + # Build HOST_PATH (for runtime shebang patching) + let hostPathDirs = ($buildInputs | each {|dep| + let binDir = $"($dep)/bin" + if ($binDir | path exists) { $binDir } else { null } + } | compact) + if ($hostPathDirs | length) > 0 { + load-env {HOST_PATH: ($hostPathDirs | str join ":")} + } + + # Build search paths from all deps (both native and host) + let allDeps = ($nativeBuildInputs ++ $buildInputs ++ $depsBuildBuild) + + for dep in $allDeps { + addToSearchPath PKG_CONFIG_PATH $"($dep)/lib/pkgconfig" + addToSearchPath PKG_CONFIG_PATH $"($dep)/share/pkgconfig" + addToSearchPath CMAKE_PREFIX_PATH $dep + addToSearchPath ACLOCAL_PATH $"($dep)/share/aclocal" + addToSearchPath XDG_DATA_DIRS $"($dep)/share" + addToSearchPath PERL5LIB $"($dep)/lib/perl5/site_perl" + } + + # Also add propagated deps from all direct deps + for dep in $allDeps { + propagateSearchPaths $dep + } + + # Collect nushell hook paths (.nu files in nativeBuildInputs) + # These are paths like cmake.nushellHook that point to .nu files. + # They will be executed during phases to override default behavior. + let nuHooks = ($nativeBuildInputs | where {|dep| + ($dep | str ends-with ".nu") and ($dep | path exists) + }) + load-env {__nuHooks: $nuHooks} +} + +# Walk a dependency's nix-support/ for propagated deps and add their paths +def --env propagateSearchPaths [dep: string] { + let supportDir = $"($dep)/nix-support" + if not ($supportDir | path exists) { return } + + # Read propagated deps and add their paths + for file in ["propagated-native-build-inputs" "propagated-build-inputs"] { + let filePath = $"($supportDir)/($file)" + if ($filePath | path exists) { + let deps = (open --raw $filePath | str trim | split row " " | where {|d| $d != ""}) + for pdep in $deps { + if ($pdep | path exists) { + addToSearchPath PKG_CONFIG_PATH $"($pdep)/lib/pkgconfig" + addToSearchPath PKG_CONFIG_PATH $"($pdep)/share/pkgconfig" + addToSearchPath CMAKE_PREFIX_PATH $pdep + addToSearchPath XDG_DATA_DIRS $"($pdep)/share" + } + } + } + } +} + +# --------------------------------------------------------------------------- +# Phase implementations +# --------------------------------------------------------------------------- + +def --env unpackPhase [] { + runHook preUnpack + + let src = (attr src) + let srcs = (attr srcs []) + let srcList = if ($srcs | length) > 0 { $srcs } else if $src != null { [$src] } else { + print -e "error: variable src or srcs should point to the source" + exit 1 + } + + # Record dirs before unpacking + let dirsBefore = (ls | where type == dir | get name) + + # Unpack each source + for s in $srcList { + unpackFile $s + } + + # Determine source root + mut sourceRoot = (attr sourceRoot "") + if $sourceRoot == "" { + let dirsAfter = (ls | where type == dir | get name) + let newDirs = ($dirsAfter | where {|d| $d not-in $dirsBefore}) + if ($newDirs | length) == 1 { + $sourceRoot = ($newDirs | first) + } else if ($newDirs | length) > 1 { + print -e "unpacker produced multiple directories" + exit 1 + } else { + print -e "unpacker appears to have produced no directories" + exit 1 + } + } + + print -e $"source root is ($sourceRoot)" + + # Make sources writable + if not (attrBool dontMakeSourcesWritable) { + ^chmod -R u+w -- $sourceRoot + } + + load-env {sourceRoot: $sourceRoot} + + runHook postUnpack + + # Change to source directory + cd $env.sourceRoot +} + +# Unpack a single source file +def unpackFile [file: string] { + if ($file | path type) == "dir" { + let base = ($file | path basename) + ^cp -rT $file $base + } else { + let name = ($file | path basename) + # Detect archive type and extract + if ($name | str ends-with ".tar.gz") or ($name | str ends-with ".tgz") { + ^tar xzf $file + } else if ($name | str ends-with ".tar.bz2") or ($name | str ends-with ".tbz2") { + ^tar xjf $file + } else if ($name | str ends-with ".tar.xz") or ($name | str ends-with ".txz") { + ^tar xJf $file + } else if ($name | str ends-with ".tar.zst") or ($name | str ends-with ".tar.zstd") { + ^tar --use-compress-program=unzstd -xf $file + } else if ($name | str ends-with ".tar.lz") { + ^tar --use-compress-program=lzip -xf $file + } else if ($name | str ends-with ".tar") { + ^tar xf $file + } else if ($name | str ends-with ".zip") { + ^unzip -qq $file + } else { + print -e $"don't know how to unpack ($file)" + exit 1 + } + } +} + +def --env patchPhase [] { + runHook prePatch + + let patches = (attr patches []) + let patchFlags = (attr patchFlags ["-p1"]) + + for p in $patches { + print -e $"applying patch ($p)" + let name = ($p | path basename) + if ($name | str ends-with ".gz") { + ^gzip -d -c $p | ^patch ...$patchFlags + } else if ($name | str ends-with ".bz2") { + ^bzip2 -d -c $p | ^patch ...$patchFlags + } else if ($name | str ends-with ".xz") { + ^xz -d -c $p | ^patch ...$patchFlags + } else { + ^patch ...$patchFlags -i $p + } + } + + runHook postPatch +} + +def --env configurePhase [] { + runHook preConfigure + + # Check for string override from attrs + let phaseStr = (attr configurePhase) + if $phaseStr != null and ($phaseStr | describe) == "string" { + evalNu $phaseStr + runHook postConfigure + return + } + + # Default: autotools-style configure + let configureScript = (attr configureScript "./configure") + if not ($configureScript | path exists) { + print -e "no configure script, doing nothing" + runHook postConfigure + return + } + + let configureFlags = (attr configureFlags []) + let prefix = $env.out + + mut flags = [] + if not (attrBool dontAddPrefix) { + $flags = ($flags | append $"--prefix=($prefix)") + } + $flags = ($flags | append $configureFlags) + + x $configureScript ...$flags + + runHook postConfigure +} + +def --env buildPhase [] { + runHook preBuild + + # Check for phase override + let override = ($env | get -o __buildPhase) + if $override != null { + do $override + runHook postBuild + return + } + + # Check for string override from attrs + let phaseStr = (attr buildPhase) + if $phaseStr != null and ($phaseStr | describe) == "string" { + evalNu $phaseStr + runHook postBuild + return + } + + # Default: make + let hasMakefile = (("Makefile" | path exists) or ("makefile" | path exists) or ("GNUmakefile" | path exists)) + let makeFlags = (attr makeFlags []) + + if not $hasMakefile and ($makeFlags | length) == 0 { + print -e "no Makefile or custom buildPhase, doing nothing" + runHook postBuild + return + } + + let buildFlags = (attr buildFlags []) + let cores = $env.NIX_BUILD_CORES + let parallelFlag = if (attrBool enableParallelBuilding true) { [$"-j($cores)"] } else { [] } + + x make ...$parallelFlag ...$makeFlags ...$buildFlags + + runHook postBuild +} + +def --env checkPhase [] { + runHook preCheck + + # Check for phase override + let override = ($env | get -o __checkPhase) + if $override != null { + do $override + runHook postCheck + return + } + + # Check for string override from attrs + let phaseStr = (attr checkPhase) + if $phaseStr != null and ($phaseStr | describe) == "string" { + evalNu $phaseStr + runHook postCheck + return + } + + # Default: make check or make test + let hasMakefile = (("Makefile" | path exists) or ("makefile" | path exists) or ("GNUmakefile" | path exists)) + if not $hasMakefile { + print -e "no Makefile or custom checkPhase, doing nothing" + runHook postCheck + return + } + + let checkTarget = (attr checkTarget) + let target = if $checkTarget != null { + $checkTarget + } else if (do { ^make -n check out+err>| complete | get exit_code } == 0) { + "check" + } else if (do { ^make -n test out+err>| complete | get exit_code } == 0) { + "test" + } else { + null + } + + if $target == null { + print -e "no check/test target in Makefile, doing nothing" + runHook postCheck + return + } + + let checkFlags = (attr checkFlags []) + let makeFlags = (attr makeFlags []) + let cores = $env.NIX_BUILD_CORES + let parallelFlag = if (attrBool enableParallelChecking true) { [$"-j($cores)"] } else { [] } + + x make $target ...$parallelFlag ...$makeFlags ...$checkFlags + + runHook postCheck +} + +def --env installPhase [] { + runHook preInstall + + # Check for phase override + let override = ($env | get -o __installPhase) + if $override != null { + do $override + runHook postInstall + return + } + + # Check for string override from attrs + let phaseStr = (attr installPhase) + if $phaseStr != null and ($phaseStr | describe) == "string" { + evalNu $phaseStr + runHook postInstall + return + } + + # Default: make install + let hasMakefile = (("Makefile" | path exists) or ("makefile" | path exists) or ("GNUmakefile" | path exists)) + let makeFlags = (attr makeFlags []) + + if not $hasMakefile and ($makeFlags | length) == 0 { + print -e "no Makefile or custom installPhase, doing nothing" + runHook postInstall + return + } + + let prefix = $env.out + mkdir $prefix + + let installFlags = (attr installFlags []) + let installTargets = (attr installTargets ["install"]) + let cores = $env.NIX_BUILD_CORES + let parallelFlag = if (attrBool enableParallelInstalling true) { [$"-j($cores)"] } else { [] } + + x make ...$installTargets ...$parallelFlag ...$makeFlags ...$installFlags + + runHook postInstall +} + +def --env fixupPhase [] { + # Ensure all output directories exist (even if empty) + for outputName in (getAllOutputNames) { + let outputPath = (getOutput $outputName) + if not ($outputPath | path exists) { + mkdir $outputPath + } + } + + # Make everything writable for strip et al. + for outputName in (getAllOutputNames) { + let outputPath = (getOutput $outputName) + if ($outputPath | path exists) { + ^chmod -R u+w -- $outputPath + } + } + + runHook preFixup + + # Run fixup hooks on each output + for outputName in (getAllOutputNames) { + let outputPath = (getOutput $outputName) + if ($outputPath | path exists) { + load-env {prefix: $outputPath} + # Run built-in fixup operations + move-lib64 moveLib64 + move-sbin moveSbin + move-docs moveDocs + compress-man-pages compressManPages + patch-shebangs patchShebangsAuto $outputPath + strip doStrip $outputPath + # Run user-registered fixup hooks + runHook fixupOutput + } + } + + # Record propagated dependencies + propagated-deps recordPropagatedDeps + + runHook postFixup +} + +def --env installCheckPhase [] { + runHook preInstallCheck + + # Check for phase override + let override = ($env | get -o __installCheckPhase) + if $override != null { + do $override + } else { + let phaseStr = (attr installCheckPhase) + if $phaseStr != null and ($phaseStr | describe) == "string" { + evalNu $phaseStr + } + } + + runHook postInstallCheck +} + +# --------------------------------------------------------------------------- +# Phase runner +# --------------------------------------------------------------------------- + +def --env runPhase [phase: string] { + # Skip conditions + let skipMap = { + unpackPhase: dontUnpack + patchPhase: dontPatch + configurePhase: dontConfigure + buildPhase: dontBuild + installPhase: dontInstall + fixupPhase: dontFixup + } + + # Phases that require an opt-in + let optInMap = { + checkPhase: doCheck + installCheckPhase: doInstallCheck + } + + # Check skip + let skipAttr = ($skipMap | get -o $phase) + if $skipAttr != null and (attrBool $skipAttr) { + return + } + + # Check opt-in + let optInAttr = ($optInMap | get -o $phase) + if $optInAttr != null and not (attrBool $optInAttr) { + return + } + + note "phase" $phase + print -e $"Running phase: ($phase)" + + let start = (date now) + + # Execute the phase + match $phase { + "unpackPhase" => { unpackPhase } + "patchPhase" => { patchPhase } + "configurePhase" => { configurePhase } + "buildPhase" => { buildPhase } + "checkPhase" => { checkPhase } + "installPhase" => { installPhase } + "fixupPhase" => { fixupPhase } + "installCheckPhase" => { installCheckPhase } + _ => { + # Custom phase — try to eval as nushell code + let phaseStr = (attr $phase) + if $phaseStr != null and ($phaseStr | describe) == "string" { + evalNu $phaseStr + } else { + print -e $"unknown phase ($phase), skipping" + } + } + } + + let elapsed = ((date now) - $start) + print -e $"Phase ($phase) completed in ($elapsed)" +} + +# --------------------------------------------------------------------------- +# Main entry point +# --------------------------------------------------------------------------- + +def --env main [] { + # Parse structured attrs + let attrs = (open $env.NIX_ATTRS_JSON_FILE) + load-env {__attrs: $attrs} + + print -e "nushell builder starting" + + # Set up environment + setupEnvironment + + # Activate dependencies and build search paths + activateDependencies + + # Define phase sequence + let defaultPhases = [ + "unpackPhase" + "patchPhase" + "configurePhase" + "buildPhase" + "checkPhase" + "installPhase" + "fixupPhase" + "installCheckPhase" + ] + + let phases = (attr phases $defaultPhases) + + # Run all phases + for phase in $phases { + runPhase $phase + } + + print -e "nushell builder finished" +} + +# Run +main diff --git a/stdenv/splice.nix b/stdenv/splice.nix index 9df78f36..48155ae8 100644 --- a/stdenv/splice.nix +++ b/stdenv/splice.nix @@ -185,6 +185,35 @@ in pkgs = if actuallySplice then splicedPackages // { recurseForDerivations = false; } else pkgs; + # mkEkaPackage — scope-based dependency declaration (EEP 0041) + # Uses nushell as native builder instead of bash. + mkEkaPackage = { + inherit (pkgs) stdenv nushell; + cc = pkgs.stdenv.cc; + scopes = { + buildBuild = pkgs.pkgsBuildBuild; + buildHost = pkgs.pkgsBuildHost; + buildTarget = pkgs.pkgsBuildTarget; + hostHost = pkgs.pkgsHostHost; + hostTarget = pkgs.pkgsHostTarget; + targetTarget = pkgs.pkgsTargetTarget; + }; + + __functor = + self: fnOrAttrs: + (import ./generic/make-eka-package.nix { + inherit lib; + inherit (pkgs) config; + inherit (self) + stdenv + cc + scopes + nushell + ; + }).mkEkaPackage + fnOrAttrs; + }; + # prefill 2 fields of the function for convenience makeScopeWithSplicing = lib.makeScopeWithSplicing splicePackages pkgs.newScope; makeScopeWithSplicing' = lib.makeScopeWithSplicing' { diff --git a/top-level.nix b/top-level.nix index 8c27b2e6..a06223fd 100644 --- a/top-level.nix +++ b/top-level.nix @@ -50,6 +50,9 @@ with final; inherit (stdenv) hostPlatform; }; + # mkEkaPackage (nushell builder) test variant of jq + jq_nu = callPackage ./pkgs/jq/nushell.nix { }; + nix-update-script = callPackage ./pkgs/nix-update-script { }; nixos = null;