From a9625863819c645e3db98957e1bccf532c573c73 Mon Sep 17 00:00:00 2001 From: Quixos Development Agent 01f26965-84fa-4a79-8069-725682c13065 <01f26965-84fa-4a79-8069-725682c13065@agents.gitea.internal.quixos.org> Date: Thu, 29 Oct 2026 00:38:18 +0000 Subject: [PATCH] Retire staged Yarn dependencies during Nix installation (#473) --- PORTABLE_ARTIFACTS.md | 6 +++++ test/yarn-dependencies/yarn-project.nix | 31 +++++++++++++++++++++++++ yarn-dependencies.nix | 10 +++++++- yarn-dependencies.test.nix | 24 +++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 test/yarn-dependencies/yarn-project.nix create mode 100644 yarn-dependencies.test.nix diff --git a/PORTABLE_ARTIFACTS.md b/PORTABLE_ARTIFACTS.md index 7348087..2e194bf 100644 --- a/PORTABLE_ARTIFACTS.md +++ b/PORTABLE_ARTIFACTS.md @@ -69,6 +69,12 @@ describes its install-state file as an optimization cache in its This helper output is a dependency directory for a build, not a standalone Package or a runnable Yarn project. Its `devShell` supplies freestanding Yarn for editable projects; do not run Yarn against the readonly dependency output. +Installation discards its disposable archive cache and streams dependencies into +the output while retiring staged files, including across separate build/store +mounts. It preserves package layout, native results, hidden data and executable +links without holding two complete dependency trees. The maintained +`camino-yarn-dependencies` check covers transfer and runtime layout; consuming +Studio and Package-builder checks retain real locked-installation coverage. Use the shared `installDependencies` assembly for consumers: authored libraries are copied into the consumer's `node_modules`, where Node can find their diff --git a/test/yarn-dependencies/yarn-project.nix b/test/yarn-dependencies/yarn-project.nix new file mode 100644 index 0000000..da0c784 --- /dev/null +++ b/test/yarn-dependencies/yarn-project.nix @@ -0,0 +1,31 @@ +# Tiny generated-project interface fixture. Real offline Yarn installation +# remains covered by the consuming Studio and Package-builder checks. +{ stdenvNoCC, nodejs }: +{ src, overrideAttrs ? null }: +let + project = stdenvNoCC.mkDerivation { + name = "yarn-dependencies-transfer-fixture"; + inherit src; + configurePhase = '' + mkdir -p node_modules/fixture node_modules/.bin .yarn/cache + printf '%s\n' '{"main":"index.cjs"}' > node_modules/fixture/package.json + printf '%s\n' 'module.exports = {value: "retained dependency"};' > node_modules/fixture/index.cjs + ln node_modules/fixture/index.cjs node_modules/fixture/twin.cjs + ln -s fixture node_modules/alias + printf '%s\n' '#!${nodejs}/bin/node' 'console.log("linked executable");' > node_modules/fixture/cli.cjs + chmod +x node_modules/fixture/cli.cjs + ln -s ../fixture/cli.cjs node_modules/.bin/fixture + printf '%s\n' 'hidden dependency data' > node_modules/.fixture-data + printf '%s\n' 'disposable linker state' > node_modules/.yarn-state.yml + printf '%s\n' 'disposable archive' > .yarn/cache/fixture.zip + ''; + postInstall = '' + test ! -e node_modules + test ! -e .yarn/cache + test ! -e "$out/.yarn-state.yml" + test ! -e "$out/node_modules" + test "$(stat -c %i "$out/fixture/index.cjs")" = "$(stat -c %i "$out/fixture/twin.cjs")" + ''; + }; +in +if overrideAttrs == null then project else project.overrideAttrs overrideAttrs diff --git a/yarn-dependencies.nix b/yarn-dependencies.nix index 6ea4567..f3a389b 100644 --- a/yarn-dependencies.nix +++ b/yarn-dependencies.nix @@ -9,10 +9,18 @@ let overrideAttrs = _: { buildPhase = "runHook preBuild; runHook postBuild"; installPhase = '' + set -o pipefail runHook preInstall test -d node_modules + test ! -L node_modules mkdir -p "$out" - cp -a node_modules/. "$out/" + # Installation is finished: retire its disposable archive cache, then + # stream dependencies across the build/store mount boundary without + # retaining a second complete tree. This is the same installation + # policy used by Central's dependency derivations. + rm -rf .yarn/cache + ${pkgs.gnutar}/bin/tar --create --file=- --remove-files node_modules \ + | ${pkgs.gnutar}/bin/tar --extract --file=- --strip-components=1 --directory="$out" # Node resolves installed packages directly. This generated index is for # Yarn's linker, not a runtime input; do not export build-session state. rm -f "$out/.yarn-state.yml" diff --git a/yarn-dependencies.test.nix b/yarn-dependencies.test.nix new file mode 100644 index 0000000..8c84d4a --- /dev/null +++ b/yarn-dependencies.test.nix @@ -0,0 +1,24 @@ +{ pkgs }: +let + dependencies = import ./yarn-dependencies.nix { + inherit pkgs; + src = ./test/yarn-dependencies; + }; +in +pkgs.runCommand "quixos-yarn-dependencies-check" { nativeBuildInputs = [ pkgs.nodejs_24 ]; } '' + node - ${dependencies} <<'JS' + const assert = require('node:assert/strict'); + const fs = require('node:fs'); + const path = require('node:path'); + const {execFileSync} = require('node:child_process'); + const root = process.argv[2]; + assert.deepEqual(require(path.join(root, 'fixture')), {value: 'retained dependency'}); + assert.deepEqual(require(path.join(root, 'alias')), {value: 'retained dependency'}); + assert.equal(fs.readlinkSync(path.join(root, 'alias')), 'fixture'); + assert.equal(fs.readlinkSync(path.join(root, '.bin/fixture')), '../fixture/cli.cjs'); + assert.equal(execFileSync(path.join(root, '.bin/fixture'), {encoding: 'utf8'}), 'linked executable\n'); + assert.equal(fs.readFileSync(path.join(root, '.fixture-data'), 'utf8'), 'hidden dependency data\n'); + assert.equal(fs.existsSync(path.join(root, '.yarn-state.yml')), false); + JS + touch "$out" +''