Resolve the nix-flake-packaging spec's open verification item by observation rather than inference, and act on the unfavourable answer. `resolvePortableHookCommand` in axi-sdk-js returns the bare binary name only when a PATH entry realpath-matches the entrypoint. npm symlinks its bin entry straight at dist/main.js and satisfies that; Nix installs a generated wrapper script whose realpath is the wrapper, so the absolute store path is recorded instead. Verified by probing the SDK with both install shapes and by driving the flake-built binary against a temporary HOME. The path is content-addressed, so it moves on every rebuild and is eventually collected, and a session-start hook that cannot execute fails silently. The mitigation is documentation, per the decision recorded when the item was opened: the setup help text now says to re-run `setup hooks` after an upgrade. How the setup command constructs the hook is deliberately unchanged. Preferring the bare name is the right answer for every wrapper-based install, not a Nix special case, so it belongs in a successor task with its own ADR alongside the related `isManagedHook` substring defect. package.nix's postUnpack rename therefore stays; its comment no longer promises this task will remove it.
189 lines
7.9 KiB
Nix
189 lines
7.9 KiB
Nix
{
|
|
lib,
|
|
buildNpmPackage,
|
|
importNpmLock,
|
|
makeWrapper,
|
|
nodejs,
|
|
git,
|
|
tea,
|
|
which,
|
|
}:
|
|
|
|
let
|
|
# Where the install check finds the dev dependencies that `npmInstallHook`
|
|
# prunes out of the build tree. Named once: it is a contract between
|
|
# `preInstall`, which writes it, and `installCheckPhase`, which reads it.
|
|
devNodeModules = "$NIX_BUILD_TOP/node_modules-dev";
|
|
|
|
# The manifest is the canonical version: the release flow bumps it, and
|
|
# reading it here means a store path and a released version cannot disagree.
|
|
manifest = lib.importJSON ./package.json;
|
|
|
|
# An explicit allowlist of what the build and its tests actually read. The
|
|
# repository's highest-churn directories — .claude, bench, prose docs — are
|
|
# all build-irrelevant, so a whole-repository source would let writing an ADR
|
|
# invalidate the derivation and force a rebuild with a full test run.
|
|
#
|
|
# Adding a build-relevant top-level file means adding it here too; the build
|
|
# otherwise fails on a missing file.
|
|
src = lib.fileset.toSource {
|
|
root = ./.;
|
|
fileset = lib.fileset.unions [
|
|
./src
|
|
# The fast tier only. `test/e2e` needs a live Gitea host and is excluded
|
|
# from the runner config, so admitting it would let end-to-end churn
|
|
# invalidate the derivation — the very cost this allowlist exists to
|
|
# avoid. `test/packaging` stays: task 0038 drives it against the
|
|
# installed binary.
|
|
(lib.fileset.difference ./test ./test/e2e)
|
|
./skills
|
|
./package.json
|
|
./package-lock.json
|
|
./tsconfig.json
|
|
./tsconfig.build.json
|
|
./vitest.config.ts
|
|
./vitest.packaging.config.ts
|
|
];
|
|
};
|
|
in
|
|
buildNpmPackage {
|
|
pname = "gitea-axi";
|
|
inherit (manifest) version;
|
|
inherit src nodejs;
|
|
|
|
# Each dependency's fetch is derived from the integrity fields already in the
|
|
# lockfile, so a lockfile change needs no edit here. A single fixed-output
|
|
# hash would break on every dependency bump and be repaired by copying a hash
|
|
# out of an error message — a permanent recurring tax.
|
|
npmDeps = importNpmLock { npmRoot = src; };
|
|
inherit (importNpmLock) npmConfigHook;
|
|
|
|
nativeBuildInputs = [ makeWrapper ];
|
|
|
|
# The builder would otherwise unpack to a generic `source` directory, which no
|
|
# real installation resembles: under npm the tree lives at
|
|
# `node_modules/gitea-axi`, under Nix at `…-gitea-axi-<version>/…`. The fast
|
|
# tier's `setup hooks` test is sensitive to the difference, because the SDK
|
|
# records the entrypoint's absolute path and recognises its own managed hook by
|
|
# finding "gitea-axi" within it. Naming the tree makes the build representative
|
|
# rather than an environment no operator ever has.
|
|
#
|
|
# This coupling is a defect, not a property worth preserving. Task 0042
|
|
# verified the resolution behaviour and documented the mitigation, but left
|
|
# the hook's dependence on the entrypoint path in place: removing it needs its
|
|
# own ADR, since a stable search-path name is the right answer for every
|
|
# installation method and not a Nix special case. This rename goes away with
|
|
# that task, not before.
|
|
postUnpack = ''
|
|
mv "$sourceRoot" gitea-axi
|
|
export sourceRoot=gitea-axi
|
|
'';
|
|
|
|
# The fast tier only. The live end-to-end and benchmark smoke tiers need a
|
|
# live Gitea host. Two of these test files invoke `git` directly and one
|
|
# resolves it with `which`; `tea` is already stubbed within this tier.
|
|
doCheck = true;
|
|
nativeCheckInputs = [
|
|
git
|
|
which
|
|
];
|
|
|
|
# `buildNpmPackage` wires config, build and install hooks but no check hook, so
|
|
# `doCheck` alone is inert and the phase has to be spelled out. `git init` and
|
|
# `git commit` in the fast tier also need a writable HOME, which the sandbox
|
|
# otherwise points at a non-existent directory.
|
|
checkPhase = ''
|
|
runHook preCheck
|
|
|
|
export HOME=$(mktemp -d)
|
|
npm run test
|
|
|
|
# vitest leaves a run cache under node_modules/.vite whose results.json
|
|
# records durations and timestamps. `npmInstallHook` copies node_modules
|
|
# into $out wholesale, so leaving it there both ships a stray cache in the
|
|
# closure and makes the output non-reproducible — `nix build --rebuild`
|
|
# reports the derivation "may not be deterministic" on that one file.
|
|
rm -rf node_modules/.vite
|
|
|
|
runHook postCheck
|
|
'';
|
|
|
|
# `npmInstallHook` prunes dev dependencies out of the build tree's
|
|
# node_modules on its way to assembling $out, which would take vitest with it
|
|
# and leave the install check with nothing to run. Snapshot the tree first —
|
|
# as hardlinks, so it costs neither time nor space, and so the prune's
|
|
# deletions do not follow through to the copy.
|
|
preInstall = ''
|
|
cp -al node_modules "${devNodeModules}"
|
|
'';
|
|
|
|
# ADR 0018: append, never prepend. The operator's own `tea` owns the
|
|
# credential store it refreshes in place, so the closure's copy is a
|
|
# fresh-machine fallback rather than an override.
|
|
postInstall = ''
|
|
wrapProgram $out/bin/gitea-axi \
|
|
--suffix PATH : ${lib.makeBinPath [ git tea ]}
|
|
'';
|
|
|
|
# Drive the binary that was just installed through the shared installed-binary
|
|
# tier, which the npm distribution path drives too — so the two cannot drift
|
|
# apart in what they guarantee about an installed gitea-axi.
|
|
#
|
|
# This guards a class of failure `checkPhase` structurally cannot reach,
|
|
# because it runs against the source tree rather than an installation. The
|
|
# one that bites here is Skill resolution: `setup` locates the bundled Agent
|
|
# Skill relative to its own module location, so the built output's position
|
|
# relative to that Skill is load-bearing — an arrangement that exists only
|
|
# once installed. A probe moving the installed `skills` aside does fail this
|
|
# phase.
|
|
#
|
|
# The tier's executable-bit assertion carries less weight under Nix than
|
|
# under npm, and deliberately so: `nodejsInstallExecutables` generates a
|
|
# wrapper invoking `node <path>` rather than symlinking the entrypoint, so
|
|
# the bit that matters is the one on `$out/bin/gitea-axi`, which makeWrapper
|
|
# always sets. That assertion earns its keep on the npm path, where npm sets
|
|
# the bit from the manifest's `bin` entry and `tsc` does not. Sharing one
|
|
# tier means neither path picks which guarantees it feels like offering.
|
|
#
|
|
# `installCheckPhase` runs after `fixupPhase`, so the binary named here is the
|
|
# wrapped one an operator would actually get. Naming it is all this phase
|
|
# does: the assertions live in the tier, not in shell script here.
|
|
doInstallCheck = true;
|
|
installCheckPhase = ''
|
|
runHook preInstallCheck
|
|
|
|
# Restore by copying, not moving, so the snapshot survives for a replayed
|
|
# phase — `--keep-failed` debugging, or `genericBuild` re-entered by hand.
|
|
rm -rf node_modules
|
|
cp -al "${devNodeModules}" node_modules
|
|
|
|
export HOME=$(mktemp -d)
|
|
GITEA_AXI_INSTALLED_BIN=$out/bin/gitea-axi npm run test:installed
|
|
|
|
runHook postInstallCheck
|
|
'';
|
|
|
|
# The Node the package is built against, published as a declared interface
|
|
# rather than left to be read off the build environment. The flake's dev shell
|
|
# consumes exactly this, so the two cannot drift onto different majors — and
|
|
# this attribute is why that holds, so removing it breaks the shell.
|
|
passthru = { inherit nodejs; };
|
|
|
|
meta = {
|
|
inherit (manifest) description homepage;
|
|
|
|
# Looked up by SPDX identifier rather than hardcoded, for the same reason
|
|
# the version is read from the manifest: one canonical source, no second
|
|
# place to update on a relicence.
|
|
license = lib.licensesSpdx.${manifest.license};
|
|
|
|
mainProgram = "gitea-axi";
|
|
|
|
# Broader than the flake's `systems` list, deliberately. This describes what
|
|
# the package supports — everything, since it contains no compiled code —
|
|
# whereas that list encodes which systems the pinned nixpkgs can still
|
|
# evaluate. Consumed against 26.05, x86_64-darwin builds fine from here.
|
|
platforms = lib.platforms.linux ++ lib.platforms.darwin;
|
|
};
|
|
}
|