Skip to content
This repository was archived by the owner on Dec 27, 2025. It is now read-only.

Commit 04c9f1c

Browse files
authored
Merge pull request #2 from natsukium/feat/deterministic
refactor: improve nix packaging with deterministic dependency vendoring
2 parents cbaefbd + 73fd886 commit 04c9f1c

3 files changed

Lines changed: 155 additions & 25 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
- uses: denoland/setup-deno@v2
1919
with:
2020
deno-version: ${{ env.DENO_VERSION }}
21+
- uses: nixbuild/nix-quick-install-action@v34
2122
- run: deno fmt --check
2223
- run: deno lint
2324
- run: deno task check

deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"coverage": "deno coverage .coverage --include='src/'",
2828
"verify": "deno fmt && deno lint && deno task check && deno task test",
2929
"probitas": "deno run -A --unstable-kv --lock ./deno.lock ./mod.ts",
30-
"update-lock": "rm deno.lock && deno cache -r mod.ts jsr:@probitas/probitas jsr:@probitas/probitas@^0",
30+
"update-nix-hash": "nix run .#probitas.updateDepsHash",
31+
"update-lock": "rm deno.lock && deno cache -r mod.ts jsr:@probitas/probitas jsr:@probitas/probitas@^0 && deno task update-nix-hash",
3132
"update-version": "deno run -A .scripts/update_version.ts"
3233
},
3334
"imports": {

flake.nix

Lines changed: 152 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,171 @@
77
};
88

99
outputs = { self, nixpkgs, flake-utils }:
10-
let
11-
# Overlay that adds probitas to pkgs
12-
overlay = final: prev: {
13-
probitas = prev.writeShellApplication {
14-
name = "probitas";
15-
runtimeInputs = [ prev.deno ];
16-
text = ''
17-
export DENO_NO_UPDATE_CHECK=1
18-
exec deno run -A \
19-
--unstable-kv \
20-
--config=${self}/deno.json \
21-
--frozen --lock=${self}/deno.lock \
22-
${self}/mod.ts "$@"
23-
'';
24-
};
25-
};
26-
in
2710
{
2811
# Overlay for easy integration
29-
overlays.default = overlay;
12+
overlays.default = final: prev: {
13+
inherit (self.packages.${final.stdenv.hostPlatform.system}) probitas;
14+
};
3015
}
3116
//
3217
flake-utils.lib.eachDefaultSystem (system:
3318
let
34-
pkgs = import nixpkgs {
35-
inherit system;
36-
overlays = [ overlay ];
19+
pkgs = import nixpkgs { inherit system; };
20+
21+
# Map Nix platform to npm's os/cpu values (Node.js process.platform/process.arch)
22+
npmPlatform = {
23+
os =
24+
if pkgs.stdenv.hostPlatform.isDarwin then "darwin"
25+
else if pkgs.stdenv.hostPlatform.isLinux then "linux"
26+
else null;
27+
cpu =
28+
if pkgs.stdenv.hostPlatform.isAarch64 then "arm64"
29+
else if pkgs.stdenv.hostPlatform.isx86_64 then "x64"
30+
else null;
31+
};
32+
33+
# Use `deno cache --vendor` for deterministic output instead of $DENO_DIR cache.
34+
# The cache approach (`deno install` alone) is non-deterministic due to:
35+
# - JSR cache metadata with timestamps (normalizing breaks cache lookup)
36+
# - SQLite databases with non-deterministic page ordering (deleting causes re-downloads)
37+
deps = pkgs.stdenvNoCC.mkDerivation {
38+
name = "probitas-deps";
39+
src = pkgs.lib.cleanSource ./.;
40+
nativeBuildInputs = with pkgs; [
41+
deno
42+
jq
43+
writableTmpDirAsHomeHook
44+
];
45+
# Remove os/cpu fields from deno.lock for cross-platform deterministic hash
46+
postPatch = ''
47+
jq '
48+
if .npm then
49+
.npm |= map_values(del(.os, .cpu))
50+
else .
51+
end
52+
' deno.lock > deno.lock.tmp
53+
mv deno.lock.tmp deno.lock
54+
'';
55+
installPhase = ''
56+
runHook preInstall
57+
58+
mkdir -p $out
59+
# vendor dependencies for deterministic output
60+
deno cache --vendor --frozen mod.ts jsr:@probitas/probitas@^0
61+
62+
cp -r vendor node_modules $out/
63+
64+
runHook postInstall
65+
'';
66+
outputHashAlgo = "sha256";
67+
outputHashMode = "recursive";
68+
outputHash = "sha256-aMxoVbu2g1jb/2ZzGUjSk2V2z43jqSma5EaA7JFyFOI=";
3769
};
3870
in
3971
{
4072
packages = {
41-
inherit (pkgs) probitas;
42-
default = pkgs.probitas;
73+
default = self.packages.${system}.probitas;
74+
probitas = pkgs.stdenvNoCC.mkDerivation (finalAttrs: {
75+
pname = "probitas";
76+
version = self.shortRev or self.dirtyShortRev or "dev";
77+
src = pkgs.lib.cleanSource ./.;
78+
79+
postPatch = ''
80+
substituteInPlace deno.json \
81+
--replace-fail '"version": "0.0.0"' '"version": "${finalAttrs.version}"'
82+
'';
83+
84+
nativeBuildInputs = with pkgs; [
85+
deno
86+
jq
87+
makeBinaryWrapper
88+
] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [
89+
autoPatchelfHook
90+
];
91+
92+
installPhase = ''
93+
runHook preInstall
94+
95+
mkdir -p $out/share/probitas
96+
cp -r src assets $out/share/probitas/
97+
cp mod.ts deno.json deno.lock $out/share/probitas/
98+
99+
cp -r ${deps}/{vendor,node_modules} $out/share/probitas/
100+
# Make node_modules writable to allow removal of platform-specific packages
101+
chmod -R +w $out/share/probitas/node_modules
102+
103+
# Remove npm packages with os/cpu constraints that don't match current platform
104+
# Uses deno.lock as the source of truth for platform-specific packages
105+
incompatiblePackages=$(jq -r --arg os "${npmPlatform.os}" --arg cpu "${npmPlatform.cpu}" '
106+
.npm // {} | to_entries[] |
107+
select(
108+
(.value.os != null and (.value.os | index($os) | not)) or
109+
(.value.cpu != null and (.value.cpu | index($cpu) | not))
110+
) |
111+
.key | gsub("/"; "+")
112+
' "deno.lock")
113+
114+
for pkg in $incompatiblePackages; do
115+
rm -rf "$out/share/probitas/node_modules/.deno/$pkg"
116+
done
117+
118+
# Clean up broken symlinks left after removing platform-specific packages
119+
find "$out/share/probitas/node_modules" -xtype l -delete
120+
121+
makeWrapper ${pkgs.lib.getExe pkgs.deno} $out/bin/probitas \
122+
--set DENO_NO_UPDATE_CHECK 1 \
123+
--add-flags "run -A" \
124+
--add-flags "--unstable-kv" \
125+
--add-flags "--vendor" \
126+
--add-flags "--frozen" \
127+
--add-flags "--config=$out/share/probitas/deno.json" \
128+
--add-flags "--lock=$out/share/probitas/deno.lock" \
129+
--add-flags "$out/share/probitas/mod.ts"
130+
131+
runHook postInstall
132+
'';
133+
134+
doInstallCheck = true;
135+
nativeInstallCheckInputs = [ pkgs.versionCheckHook ];
136+
137+
passthru = {
138+
inherit deps;
139+
updateDepsHash = pkgs.writeShellScriptBin "update-probitas-deps" ''
140+
set -euo pipefail
141+
cd "$(${pkgs.lib.getExe pkgs.git} rev-parse --show-toplevel)"
142+
143+
fakehash="${pkgs.lib.fakeHash}"
144+
curhash=$(nix eval .#probitas.deps.outputHash --raw)
145+
146+
# Replace current hash with fakeHash
147+
${pkgs.gnused}/bin/sed -i "s|\"$curhash\"|\"$fakehash\"|" flake.nix
148+
149+
# Build with fakeHash to get the correct hash from error output
150+
set +e
151+
newhash=$(
152+
nix build .#probitas.deps --no-link --log-format internal-json 2>&1 >/dev/null \
153+
| ${pkgs.gnugrep}/bin/grep "$fakehash" \
154+
| ${pkgs.gnugrep}/bin/grep -oP 'sha256-[A-Za-z0-9+/=]+' \
155+
| tail -1
156+
)
157+
set -e
158+
159+
if [[ -n "$newhash" ]]; then
160+
${pkgs.gnused}/bin/sed -i "s|\"$fakehash\"|\"$newhash\"|" flake.nix
161+
echo "Updated deps hash to: $newhash"
162+
else
163+
${pkgs.gnused}/bin/sed -i "s|\"$fakehash\"|\"$curhash\"|" flake.nix
164+
echo "Failed to get hash, restored original"
165+
fi
166+
'';
167+
};
168+
169+
meta.mainProgram = "probitas";
170+
});
43171
};
44172

45173
apps.default = flake-utils.lib.mkApp {
46-
drv = pkgs.probitas;
174+
drv = self.packages.${system}.default;
47175
};
48176

49177
devShells.default = pkgs.mkShell {

0 commit comments

Comments
 (0)