-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathper-system.nix
More file actions
832 lines (790 loc) · 35 KB
/
Copy pathper-system.nix
File metadata and controls
832 lines (790 loc) · 35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
# Per-system flake outputs (packages / checks / formatter).
#
# Kept out of flake.nix so the flake top-level can read as a manifest of
# inputs and output categories. Composition logic for workflow tools and
# lint plumbing lives here. Workflow tools (lint, update-mods, ...) are
# exposed under `packages.<system>.<name>` with `meta.mainProgram` set, so
# `nix run .#<name>` and `nix build .#<name>` both work without an `apps`
# entry (see AGENTS.md "Flake.nix style").
{
system,
ix,
nixpkgs,
paths,
rust-overlay,
}:
let
inherit (nixpkgs) lib;
pkgs = import nixpkgs {
inherit system;
overlays = [
rust-overlay.overlays.default
ix.overlay
];
};
fs = lib.fileset;
packageRegistry = import (paths.packagesRoot + "/registry.nix") {
inherit lib;
root = paths.packagesRoot;
};
# Each lint stage is one subcommand on a single binary so the spec keys
# off `lib.getExe lintStage` without registering four sibling packages.
# The Nu wrapper checks syntax at build time, so a typo in a stage shows
# up in the `lint` derivation build, not at `nix run` time.
lintStage = ix.writeNushellApplication pkgs {
name = "lint-stage";
meta.description = "One lint stage (nixfmt | statix | deadnix | ast-grep | ast-grep-test); driven by `lint`";
runtimeInputs = [
pkgs.ast-grep
pkgs.deadnix
pkgs.fd
pkgs.nixfmt
pkgs.statix
];
text = ''
def "main nixfmt" [] {
let nix_files = (fd --extension nix | lines)
nixfmt --check ...$nix_files
}
def "main statix" [] { statix check --ignore '.claude/worktrees' . }
def "main deadnix" [] { deadnix --fail --no-lambda-pattern-names . }
def "main ast-grep" [] { ast-grep scan --error . }
# Rule self-test: every fixture under ast-grep/nix/tests must flag its
# invalid cases and ignore its valid ones. Catches rules whose pattern
# silently stops matching (e.g. a bare `attr = val` that parses as an
# expression, not a binding). --skip-snapshot-tests keeps it to match
# presence/absence without baseline snapshot files.
def "main ast-grep-test" [] { ast-grep test --skip-snapshot-tests }
def main [] {
error make { msg: "specify a stage: nixfmt | statix | deadnix | ast-grep | ast-grep-test" }
}
'';
};
lintSpec = (pkgs.formats.json { }).generate "lint-dag.json" {
nodes = {
nixfmt.command = [
(lib.getExe lintStage)
"nixfmt"
];
statix.command = [
(lib.getExe lintStage)
"statix"
];
deadnix.command = [
(lib.getExe lintStage)
"deadnix"
];
"ast-grep".command = [
(lib.getExe lintStage)
"ast-grep"
];
"ast-grep-test".command = [
(lib.getExe lintStage)
"ast-grep-test"
];
};
};
lint = ix.writeNushellApplication pkgs {
name = "lint";
meta.description = "Run all Nix formatting and lint checks in parallel via dag-runner";
runtimeInputs = [ repoPackages.dag-runner ];
text = ''
def --wrapped main [...args] {
exec dag-runner ...$args ${lintSpec}
}
'';
};
# `check` is the full CI gate as one repo-owned command: check.yml runs
# `nix run .#check`, so the same two steps run in CI and locally from a single
# definition. It targets x86_64-linux explicitly because that is the system CI
# builds for; a linux runner can only pure-eval the cross-platform darwin
# images, and that cross-eval was most of what made the old single-threaded
# `nix flake check` slow. `nix` is taken from the ambient PATH on purpose
# (this is always invoked as `nix run .#check`, so the host's daemon-matched
# nix is already present); pinning a client nix here could mismatch the host
# Nix 2.34.x daemon.
#
# Step 1 (nix-fast-build) builds every `ciChecks.x86_64-linux` derivation: it
# evaluates with nix-eval-jobs (parallel) and streams each drv into a build
# pool as it resolves. --skip-cached drops paths already in a substituter (a
# warm run does almost no work), --no-nom keeps plain logs, --no-link leaves no
# result symlinks. It exits nonzero iff a build or eval fails: that is the gate.
# --eval-workers 16 with --eval-max-memory-size 6144 is a headroom guard rail
# (above nix-eval-jobs' 4 GiB default per worker, below the old 8 GiB), not a
# workaround: the per-crate check split (see the `checks` block below) keeps
# each worker's eval bounded by the largest single crate. Pinned by commit to
# nix-fast-build 1.5.0.
#
# Step 2 (nix-eval-jobs) is the schema/eval gate over the package outputs,
# broader than the `checks` set step 1 built. nix-eval-jobs is the same
# parallel evaluator nix-fast-build wraps; run eval-only over
# packages.x86_64-linux it spreads per-attribute eval across 16 workers and
# realizes IFD (the `site` import-npm-lock source) on demand. Each worker is a
# full evaluator that can grow to the 4 GiB-per-worker cap and the host runs
# many CI jobs at once, so 16 both bounds memory and already collapses the eval
# toward the slowest single attribute (warm store, eval-cache off: 342s at 1
# worker, 75s at 16, 70s at 32). The eval cache is off because it is keyed per
# commit (it never hits on a fresh CI commit) and parallel workers otherwise
# contend writing the same per-commit sqlite ("database is busy"). The
# resulting cold per-commit re-eval is tracked separately; a flake eval cache
# would amortize it. nix-eval-jobs
# reports a per-attribute eval failure as a JSON `error` line and still exits 0,
# so the gate is the error-line check; a startup or lock failure exits nonzero
# and aborts the run (Nushell propagates external failures like bash
# `set -o pipefail`). Pinned by commit to nix-eval-jobs v2.34.1, matching the
# host Nix 2.34.x.
check = ix.writeNushellApplication pkgs {
name = "check";
meta.description = "Run the full CI gate: build .#ciChecks.x86_64-linux and eval-validate .#packages.x86_64-linux";
text = ''
const fast_build = "github:Mic92/nix-fast-build/7f185e0ec37b65b4730f892e0de9a831b0610f3a"
const eval_jobs = "github:nix-community/nix-eval-jobs/65ebf5b7cd453a27af09cf02b1fc57b3568cc4b7"
def main [] {
# ca-derivations: the rust workspace units default to
# `contentAddressed = true` (lib/rust/cargo-unit.nix), so evaluating
# `.#ciChecks.x86_64-linux` resolves floating content-addressed drvs. The
# evaluator (nix-eval-jobs, which nix-fast-build wraps) needs the
# `ca-derivations` experimental feature, or it aborts with
# "experimental Nix feature 'ca-derivations' is disabled". The flake's
# nixConfig.extra-experimental-features carries it via
# accept-flake-config; `--option extra-experimental-features` here pins
# it for the build pool too so the gate is self-contained.
# --result-format json --result-file emits one record per attr per phase
# ({attr, type: EVAL|BUILD, duration, success, error, outputs}) into the
# cwd. blast-radius consumes this on a later PR via `--timings` to
# annotate the rebuilt-checks list with wall-clock seconds. The path is
# relative to the runner cwd; check.yml uploads it as an artifact.
^nix run $fast_build -- ...[
"--flake" ".#ciChecks.x86_64-linux"
"--eval-max-memory-size" "6144"
"--eval-workers" "16"
"--skip-cached"
"--no-nom"
"--no-link"
"--result-format" "json"
"--result-file" "check-results.json"
"--option" "accept-flake-config" "true"
"--option" "extra-experimental-features" "ca-derivations"
]
let tmp = (mktemp --directory --tmpdir "ix-check.XXXXXX")
let report = ($tmp | path join "flake-schema-eval.jsonl")
do --capture-errors {
^nix run $eval_jobs -- ...[
"--flake" ".#packages.x86_64-linux"
"--workers" "16"
"--gc-roots-dir" ($tmp | path join "flake-schema-eval-gc")
"--option" "accept-flake-config" "true"
"--option" "eval-cache" "false"
# See the ca-derivations note above: the package set also resolves
# content-addressed rust units, so this eval needs the feature too.
"--option" "extra-experimental-features" "ca-derivations"
]
} | tee { save --raw --force $report }
# nix-eval-jobs exits 0 even when an attribute fails to evaluate, so this
# error-line check is the gate; a nonzero exit already aborted above. The
# report is left in place on failure for inspection.
if (open --raw $report | lines | any {|line| $line | str contains '"error":' }) {
print --stderr "flake schema evaluation failed; see the error lines above"
exit 1
}
rm --recursive --force $tmp
}
'';
};
updateMods = ix.writePythonApplication pkgs {
name = "update-mods";
src = paths.tools.updateMods;
meta.description = "Regenerate Minecraft mod catalogs";
};
updateLoaders = ix.writePythonApplication pkgs {
name = "update-loaders";
src = paths.tools.updateLoaders;
meta.description = "Refresh Minecraft loader (Paper / Velocity / Fabric) catalogs from upstream";
};
ixShellSyncIgnored = ix.writePythonApplication pkgs {
name = "ix-shell-sync-ignored";
src = paths.tools.ixShellSyncIgnored;
runtimeInputs = [
pkgs.git
pkgs.gnutar
];
meta.description = "Copy git-ignored files into an ix shell workspace";
};
# Always-on instruction documents. Forcing either string evaluates the
# `agent-context` always-on cap assertion (see lib/agent-context/default.nix).
agentContextClaudeMd = pkgs.writeText "CLAUDE.md" ix.agentContext.alwaysDoc;
agentContextCodexMd = pkgs.writeText "AGENTS.md" ix.agentContext.alwaysDoc;
# One symlink-free directory holding every handwritten skill plus one
# generated skill per `disclosure: progressive` section, ready to copy into
# `.claude/skills`.
agentContextProgressiveSkills = ix.agentContext.mkProgressiveSkills { inherit pkgs; };
agentContextSkillCollisions = lib.intersectLists ix.skills.allSkills (
lib.attrNames agentContextProgressiveSkills
);
agentContextSkills =
assert lib.assertMsg (agentContextSkillCollisions == [ ])
"agent-context: progressive section names collide with handwritten skills: ${lib.concatStringsSep ", " agentContextSkillCollisions}";
ix.skills.mkSkillsDir {
inherit pkgs;
extraSkills = agentContextProgressiveSkills;
};
mcSource = ix.writeNushellApplication pkgs {
name = "mc-source";
text = builtins.readFile paths.tools.mcSource;
runtimeInputs = [
(pkgs.callPackage packageRegistry.byId.vineflower.path { })
];
meta.description = "Decompile a Minecraft server jar with Mojang mappings via Vineflower";
};
updateSounds = ix.writeNushellApplication pkgs {
name = "update-sounds";
text = builtins.readFile paths.tools.updateSounds;
meta.description = "Refresh the pinned Minecraft sound pack in packages/minecraft/sound";
};
benchFilesystem = import paths.bench.filesystem { inherit ix pkgs; };
# The indexbench CLI built for this system, fed to `mkBenchSuite` and the
# `apps.bench` perf job. Also surfaced as `packages.indexbench` through the
# registry; this binding just avoids re-resolving the package set here.
inherit (repoPackages) indexbench;
# The reproducible alloc-count bench binary from the shared workspace graph.
# It installs the counting allocator and prints an `@bench name=allocations`
# line, so its metric is deterministic and gateable as a flake check.
indexbenchAllocDemo = ix.cargoUnit.selectBinaryWithTests ix.rustWorkspace.units {
binary = "indexbench-alloc-demo";
packageName = "indexbench";
includeTestCases = false;
meta.mainProgram = "indexbench-alloc-demo";
};
# The repo's own demonstration suite: a trivial macro command, run through the
# framework end to end. `nix run .#bench` invokes this perf job. Consumers add
# their own suites the same way via `ix.mkBenchSuite`. The `allocCheck` wires
# the reproducible alloc-count bench into a flake check.
indexbenchSelfDemo = ix.mkBenchSuite pkgs {
name = "self-demo";
inherit indexbench;
macros = [
{
name = "true";
command = "true";
}
];
allocCheck = {
bench = lib.getExe indexbenchAllocDemo;
# The demo makes exactly 64 heap allocations by construction (see
# packages/indexbench/src/bin/alloc-demo.rs), so this budget is an exact,
# toolchain-stable constant; any added allocation trips the gate.
budgets.allocations = 64;
};
};
siteSrc = fs.toSource {
root = paths.site;
fileset = fs.intersection (fs.gitTracked paths.site) (
fs.unions [
(paths.site + "/package.json")
(paths.site + "/package-lock.json")
(paths.site + "/mdsvex.config.js")
(paths.site + "/svelte.config.js")
(paths.site + "/vite.config.ts")
(paths.site + "/vitest.config.ts")
(paths.site + "/tsconfig.json")
(paths.site + "/eslint.config.js")
(paths.site + "/src")
(paths.site + "/static")
]
);
};
siteBuild = ix.buildSvelteSite pkgs {
pname = "ix-site";
version = "0.1.0";
src = siteSrc;
distDir = "build";
serve = {
name = "ix-site";
routePrefix = "/index";
};
devServer = {
name = "ix-site-dev";
checkoutSubdir = "site";
};
};
# The local preview serves the same `/index` build that Pages deploys.
site = siteBuild.overrideAttrs (old: {
passthru = (old.passthru or { }) // {
preview = siteBuild.passthru.serve;
static = siteBuild.passthru.staticSite;
};
});
siteTests = ix.buildNpmVitest pkgs {
pname = "ix-site";
version = "0.1.0";
src = siteSrc;
preTest = ''
node node_modules/@sveltejs/kit/src/cli.js sync
'';
};
repoPackages = ix.packageSetFor pkgs;
# Cross-compiled standalone binaries, exposed as `packages.<host>.<bin>-<triple>`.
# Linux-only: the Apple (zig + macOS SDK) and musl cross toolchains run on a
# Linux build host; an aarch64-darwin Mac builds Darwin targets natively and
# cannot host the Linux→Darwin path, so gating here keeps darwin evaluation
# from pulling an unbuildable graph. Start with `dag-runner` (pure Rust, no C
# deps); widen `crossBinaries` as crates are confirmed cross-clean.
# macOS targets only for now: the zig + SDK toolchain produces a working
# linker out of the box. A musl target additionally needs a static musl
# linker wrapper (clang + mold against a musl sysroot); until that lands,
# `unitsFor` still accepts any triple but no musl package is exposed.
crossTargets = [
"aarch64-apple-darwin"
"x86_64-apple-darwin"
];
crossBinaries = [ "dag-runner" ];
crossWorkspace = ix.rustWorkspaceFor pkgs;
crossPackages = lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux (
lib.mergeAttrsList (
map (
target:
let
units = crossWorkspace.unitsFor { inherit target; };
in
lib.genAttrs' crossBinaries (
binary:
lib.nameValuePair "${binary}-${target}" (
units.binaries.${binary} or (throw "cross: workspace has no binary `${binary}` for ${target}")
)
)
) crossTargets
)
);
repoFlakePackages = lib.genAttrs' (packageRegistry.flakeEntriesFor system) (
entry:
lib.nameValuePair entry.flake.attrName (
lib.attrByPath entry.packageSet.attrPath
(throw "packages/${entry.relativePath}/package.nix: flake output `${entry.flake.attrName}` needs packageSet.attrPath")
repoPackages
)
);
rustPackageTestSets =
let
cargoUnit = ix.cargoUnitFor pkgs;
rustWorkspace = ix.rustWorkspaceFor pkgs;
# A crate with a `packageSet` is built through `repoPackages` and carries
# its own `passthru.tests`. A lib-only workspace crate has no `packageSet`
# and is not in `repoPackages`, so select its library straight from the
# shared unit graph (same path ix-vt's default.nix uses). The library unit
# key is the Cargo package name with dashes underscored.
packageTestsFor =
entry:
if entry.packageSet != null then
(lib.attrByPath entry.packageSet.attrPath
(throw "packages/${entry.relativePath}/package.nix: passthruTests needs packageSet.attrPath")
repoPackages
).passthru.tests or { }
else
(cargoUnit.selectLibraryWithTests rustWorkspace.units {
library = lib.replaceStrings [ "-" ] [ "_" ] entry.id;
packageName = entry.id;
}).passthru.tests or { };
# Two keyings of the same leaf test derivations:
#
# * `flat` keys each per-#[test] check as its own top-level name
# (`<prefix>-<target>-tests-<case>`). This is what the public `checks`
# output needs: the flake schema requires every `checks.<system>.<name>`
# to be a derivation, so a nested attrset there fails `nix flake check`.
#
# * `sharded` nests each package's checks under one `recurseForDerivations`
# attr (`<prefix>.<target>-tests-<case>`). This is what the memory-bounded
# CI evaluator (nix-fast-build / nix-eval-jobs / blast-radius) consumes
# through the separate `ciChecks` output.
#
# Why the sharded shape exists: nix-eval-jobs hands the root attrpath to one
# worker and forces its child names to recurse. With the flat set, that one
# worker forces every crate's per-#[test] manifest IFD at once and balloons
# to tens of GiB, which earlyoom kills on the shared CI host. The nested
# shape makes the root return cheap per-package names and forces each
# crate's manifests inside its own worker job, which restarts at the memory
# cap between packages (ENG-2201). The nested value must stay a thunk:
# filtering empties (e.g. `tests != {}`) would force every manifest during
# enumeration and reintroduce the balloon, so empty groups are left in.
flatPackageChecks = prefix: tests: lib.mapAttrs' (n: t: lib.nameValuePair "${prefix}-${n}" t) tests;
shardedPackageChecks = prefix: tests: {
${prefix} = tests // {
recurseForDerivations = true;
};
};
repoEntries = packageRegistry.passthruTestEntriesFor system;
moduleRustPackages = {
resource-monitor-stats-writer = cargoUnit.selectBinaryWithTests rustWorkspace.units {
binary = "resource-monitor-stats-writer";
};
};
# cargoAudit scans the single workspace Cargo.lock against the advisory DB,
# so it is one lockfile-scoped check (it rebuilds only on a Cargo.lock
# change, never on a source edit) rather than a per-crate gate. Expose it
# once instead of aliasing the same derivation onto every crate.
workspaceAuditTests = lib.optionalAttrs (rustWorkspace.units.policyChecks ? cargoAudit) {
rust-cargoAudit = rustWorkspace.units.policyChecks.cargoAudit;
};
collectRust =
group:
lib.mergeAttrsList (
map (entry: group entry.passthruTests.prefix (packageTestsFor entry)) repoEntries
++ lib.mapAttrsToList (
packageName: package: group "rust-${packageName}" (package.passthru.tests or { })
) moduleRustPackages
)
// workspaceAuditTests;
in
{
flat = collectRust flatPackageChecks;
sharded = collectRust shardedPackageChecks;
};
lintSource = fs.toSource {
inherit (paths) root;
fileset = fs.gitTracked paths.root;
};
tests = import paths.tests { inherit nixpkgs ix; };
exampleFleets = ix.exampleFleetsFor { hostSystem = system; };
# Separate aggregation with "health-check-" prepended to every node name,
# so the lifecycle scripts that force-delete VMs by name can never clobber
# an unrelated production VM that happens to share the example's node name
# (`nginx`, `factions`, ...).
healthCheckExampleFleets = ix.exampleFleetsFor {
hostSystem = system;
nodePrefix = "health-check-";
};
# Surface every example's `ix fleet <sub>` wrapper as a flake package.
# Each example contributes `packages.<system>.<example>-{up,health,...}`,
# which lets `nix run .#nginx-lifecycle-up` invoke the existing fleet
# plumbing through the wrapper's `meta.mainProgram`, and
# `nix build .#nginx-lifecycle-up` produce the wrapper script on disk.
examplePackages =
let
fleetSubs = [
"up"
"health"
"replace"
"switch"
"diff"
];
in
lib.concatMapAttrs (
name: fleet:
lib.genAttrs' fleetSubs (sub: {
name = "${name}-${sub}";
value = fleet.${sub}.overrideAttrs (old: {
meta = (old.meta or { }) // {
description = "Run `ix fleet ${sub}` against the ${name} example fleet";
};
});
})
) exampleFleets;
healthChecks =
import ./image/health-checks.nix
{
inherit lib pkgs;
inherit (ix) writeNushellApplication;
dagRunner = repoPackages.dag-runner;
}
{
exampleFleets = healthCheckExampleFleets;
exampleNames = lib.attrNames exampleFleets;
};
# Shared between `packages` and the `image-<name>` checks so blast-radius
# (which only diffs `.#ciChecks.x86_64-linux`) catches image fanouts via the
# check drvPath shift. The `base` image is omitted: its config is just
# `ix.image.name`/`tag`, and any base-profile change already fans out into
# every discovered image.
discoveredImages = ix.discoverImages {
root = paths.images;
inherit (tests) imageTests;
};
# Non-NixOS OCI example images (ubuntu, debian, ...). They live under
# `examples/_non-nix-oci` so fleet discovery skips the subtree (leading
# underscore), and are surfaced here as `non-nix-<name>` packages plus
# `image-non-nix-<name>` checks, the same validation path discovered images
# use. Each is imported with the example `{ index }` contract.
nonNixExampleImages = lib.mapAttrs' (
name: entry:
lib.nameValuePair "non-nix-${name}" (
import entry.path {
index = {
lib = ix;
};
}
)
) (ix.discoverTree { root = paths.examples + "/_non-nix-oci"; });
# The content-addressed `image.json` for each non-Nix example, surfaced as its
# own package so the small artifact is buildable directly (`nix build
# .#non-nix-ubuntu-description`) and cached independently of the materialized
# tar it regenerates. See #679.
nonNixExampleDescriptions = lib.mapAttrs' (
name: image: lib.nameValuePair "${name}-description" image.passthru.description
) nonNixExampleImages;
# Build the check catalog from a rust-package keying. `checks` (flat: one
# derivation per `checks.<system>.<name>`, required by the flake schema and
# `nix flake check`) and `ciChecks` (sharded: one `recurseForDerivations` group
# per package, what the memory-bounded CI evaluator consumes) share the same
# explicit and image checks; only the rust keying differs (ENG-2201). The
# collision guard runs per keying, so producing `ciChecks` only forces the
# cheap per-package names, never the flat per-#[test] spine.
catalogFor =
rustPackageSet:
lib.optionalAttrs (system == ix.system) (
let
rustChecks = {
cargo-unit-real-workspaces = tests.cargoUnitRealWorkspaces;
cargo-unit-prebuilt-library = tests.cargoUnitPrebuiltLibrary;
sdk-rust-prebuilt = tests.sdkRustPrebuilt;
}
// rustPackageSet;
explicitChecks = {
inherit (tests) eval;
# Instruction files are not committed; they are rendered live by the
# SessionStart hook. This gate forces the rendered always-on documents
# (which evaluates the always-on char cap assertion) and the combined
# skills directory (which evaluates the name-collision assertion and
# the no-symlink materialization check) to build.
agent-context = pkgs.runCommand "agent-context-check" { } ''
test -s ${agentContextClaudeMd}
test -s ${agentContextCodexMd}
test -d ${agentContextSkills}
mkdir -p "$out"
'';
# Pins the last-applied 3-way merge behind homeModules.mutable-json:
# first-install, preserve an app-written key, enforce a key the app
# changed, prune a key Nix stopped declaring, and keep a sibling key
# while a declared array is replaced atomically.
mutable-json-merge =
pkgs.runCommand "mutable-json-merge-check" { nativeBuildInputs = [ pkgs.jaq ]; }
''
prog=${ix.mutableJson.mergeProgram}
run() { jaq -ncS --argjson last "$1" --argjson live "$2" --argjson new "$3" -f "$prog"; }
check() {
expected=$(printf '%s' "$2" | jaq -cS .)
if [ "$expected" != "$3" ]; then
echo "FAIL $1: expected $expected got $3" >&2
exit 1
fi
echo "ok $1"
}
check first-install '{"permissions":{"defaultMode":"bypass"}}' \
"$(run '{}' '{}' '{"permissions":{"defaultMode":"bypass"}}')"
check preserve-app-key '{"permissions":{"defaultMode":"bypass"},"theme":"dark"}' \
"$(run '{"permissions":{"defaultMode":"bypass"}}' '{"permissions":{"defaultMode":"bypass"},"theme":"dark"}' '{"permissions":{"defaultMode":"bypass"}}')"
check enforce-changed '{"permissions":{"defaultMode":"bypass"},"theme":"dark"}' \
"$(run '{"permissions":{"defaultMode":"bypass"}}' '{"permissions":{"defaultMode":"off"},"theme":"dark"}' '{"permissions":{"defaultMode":"bypass"}}')"
check prune-dropped '{"a":1,"c":3}' \
"$(run '{"a":1,"b":2}' '{"a":1,"b":2,"c":3}' '{"a":1}')"
check nested-atomic-array '{"p":{"allow":["x"]},"t":1}' \
"$(run '{"p":{"allow":["x"]}}' '{"p":{"allow":["x","y"]},"t":1}' '{"p":{"allow":["x"]}}')"
# Divergent live shape at a path we stop declaring must not abort:
# the app replaced object `permissions` with a scalar, Nix dropped it.
check divergent-live-shape '{"permissions":"all"}' \
"$(run '{"permissions":{"defaultMode":"x"}}' '{"permissions":"all"}' '{}')"
mkdir -p "$out"
'';
# Offline schema gate for the loader manifests. `deepSeq` forces
# every Paper / Velocity / Fabric per-version lock through
# `readLoaderManifest` in `lib/artifacts.nix`, so malformed JSON or a
# missing key fires here before any image starts evaluating. The
# forced surface is the parsed-and-validated manifest data, not the
# wrapped `fetchurl` derivations, to keep this check pure eval.
loader-manifests =
let
forced = builtins.deepSeq ix.artifacts.minecraft.loaderManifests "ok";
in
pkgs.runCommand "loader-manifests-check" { } ''
printf '%s\n' '${forced}' > "$out"
'';
run-records-session = repoPackages.run.passthru.tests.recordsSession;
# Symphony's required quality lane (compile -Werror, mix format,
# credo, mix test) as a sandboxed derivation; see
# packages/symphony/default.nix. The advisory lane (dialyzer,
# sobelow, deps.audit) stays a local `mix quality` run.
symphony-elixir = repoPackages.symphony.passthru.tests.elixir;
# Deterministic alloc-count gate for indexbench: runs the counting-
# allocator demo bench once through `indexbench assert` and fails if its
# allocation count exceeds the declared budget. Reproducible, unlike
# timing/RSS, so it earns a flake check; the timing/RSS perf job lives
# under `apps.bench` instead.
indexbench-self-demo-alloc = indexbenchSelfDemo.check;
lint = pkgs.runCommand "ix-images-lint" { nativeBuildInputs = [ pkgs.coreutils ]; } ''
cp -R ${lintSource} source
chmod -R u+w source
cd source
${lib.getExe lint}
mkdir -p "$out"
'';
# Exercises the trusted half of the blast-radius PR comment: the
# validate/render jq embedded in its workflow, extracted from the YAML so
# the test can't drift from what the trusted comment job runs. The
# report-building logic lives in the `blast-radius` Rust crate and is
# covered by its own unit tests. See tools/blast-radius-test.sh.
blast-radius-test =
pkgs.runCommand "blast-radius-test"
{
nativeBuildInputs = [
pkgs.bash
pkgs.coreutils
pkgs.diffutils
pkgs.jq
pkgs.yq-go
];
}
''
cp -R ${lintSource} source
chmod -R u+w source
cd source
export HOME="$TMPDIR/home"
mkdir -p "$HOME"
bash tools/blast-radius-test.sh
mkdir -p "$out"
'';
# Proves the Linux→macOS cross toolchain actually emits a Darwin object,
# which a successful build alone does not assert. `file` reads the Mach-O
# header; a regression in the zig/SDK wiring fails here on x86_64-linux CI
# rather than silently shipping a wrong-arch binary.
cross-darwin-smoke = pkgs.runCommand "cross-darwin-smoke" { nativeBuildInputs = [ pkgs.file ]; } ''
bin=${crossPackages."dag-runner-aarch64-apple-darwin"}/bin/dag-runner
info=$(file -b "$bin")
echo "$info"
case "$info" in
*Mach-O*arm64*) ;;
*)
echo "expected Mach-O arm64, got: $info" >&2
exit 1
;;
esac
mkdir -p "$out"
'';
site-case-tests = pkgs.linkFarm "site-case-tests" (
lib.mapAttrsToList (name: path: { inherit name path; }) siteTests.cases
);
site-test = siteTests.all;
};
# One check per image OCI archive, built by nix-fast-build in step 1 of
# `.#check`. Without this, `.#packages` carries the image derivations
# but `.#checks` does not, so blast-radius under-reports any change
# that rebuilds every image because the drvPath shift never reaches a
# check. The `eval` aggregate at tests/default.nix:3890 only closes
# over per-image `extraScript` text, not `config.system.build.toplevel`,
# so it stays stable across semantic edits to shared image libs.
imageChecks = lib.mapAttrs' (n: v: lib.nameValuePair "image-${n}" v) (
discoveredImages // nonNixExampleImages
);
# Rust crate prefixes can be overridden in `package.nix` and image
# names are user-chosen, so a stray collision with an explicit check
# would otherwise be silently swallowed by the `//` merge. Two pairwise
# intersections cover all three pairs because every offending name is
# in at least two sets.
checkNameCollisions =
lib.intersectLists (lib.attrNames explicitChecks) (lib.attrNames rustChecks)
++ lib.intersectLists (lib.attrNames imageChecks) (lib.attrNames (explicitChecks // rustChecks));
in
assert lib.assertMsg (checkNameCollisions == [ ])
"checks: duplicate names across explicit/rust/image sets: ${lib.concatStringsSep ", " checkNameCollisions}";
explicitChecks // rustChecks // imageChecks
);
in
{
packages =
discoveredImages
// {
base =
let
package = ix.mkImage {
modules = [
{
ix.image = {
name = "ix/base";
tag = "latest";
};
}
];
};
in
package
// {
passthru = (package.passthru or { }) // {
tests = (package.passthru.tests or { }) // {
eval = tests.imageTests.base;
};
};
};
health-checks = healthChecks.dag;
health-checks-zellij = healthChecks.zellij;
inherit check lint site;
site-dev = site.passthru.devServer;
bench-filesystem = benchFilesystem;
update-mods = updateMods;
update-loaders = updateLoaders;
ix-shell-sync-ignored = ixShellSyncIgnored;
mc-source = mcSource;
update-sounds = updateSounds;
claude-md = agentContextClaudeMd;
codex-md = agentContextCodexMd;
skills = agentContextSkills;
}
// repoFlakePackages
// examplePackages
// nonNixExampleImages
// nonNixExampleDescriptions
// crossPackages
// healthChecks.lifecyclePackages;
# Flat keying: one derivation per `checks.<system>.<name>`, as the flake schema
# and `nix flake check` require. The `.#check` gate and blast-radius consume
# the sharded `ciChecks` instead, so this output is not what CI enumerates.
checks = catalogFor rustPackageTestSets.flat;
# Sharded keying for the memory-bounded CI evaluator (nix-fast-build /
# nix-eval-jobs / blast-radius): each package's per-#[test] checks sit under one
# `recurseForDerivations` group, so the evaluator lists cheap per-package names
# at the root and forces each crate's manifest IFD in its own worker job
# (ENG-2201). Not a `checks.<system>.<name>` output, because a non-derivation
# there fails the flake schema.
ciChecks = catalogFor rustPackageTestSets.sharded;
formatter = pkgs.nixfmt;
# `nix run .#bench` runs the repo's self-demo perf job (timing + RSS + custom
# metrics, gated on regressions). The flake's package-with-mainProgram
# convention already gives `nix run .#indexbench` for the bare CLI; this `apps`
# entry is the named perf-job entry point the framework documents.
apps = {
bench = {
type = "app";
program = lib.getExe indexbenchSelfDemo.app;
meta.description = "Run the indexbench self-demo perf suite";
};
};
# `nix develop .#bench` drops into a shell with the bench + profiling tools.
# tango is already a workspace dependency (built per-crate by cargo-unit); the
# shell adds the out-of-process profilers a bench author reaches for.
devShells = {
default = pkgs.mkShellNoCC {
packages = [
pkgs.ast-grep
pkgs.nixfmt
];
};
bench = pkgs.mkShellNoCC {
packages = [
indexbench
pkgs.hyperfine
pkgs.valgrind
pkgs.samply
pkgs.jemalloc
];
};
# Dev loop for packages/symphony: the Elixir/OTP pairing the runtime pins
# (1.19 on 28) plus the host tools bin/run-nix expects. codex is the plain
# nixpkgs CLI; authenticate it before `nix run .#symphony`.
symphony = pkgs.mkShellNoCC {
packages = [
(ix.languages.elixir.toolchain pkgs { version = "1.19"; })
(ix.languages.erlang.toolchain pkgs { version = "28"; })
pkgs.codex
pkgs.gh
pkgs.git
pkgs.openssh
];
};
};
}