Skip to content

Commit 8b70b23

Browse files
andrewgazelkacodex
andauthored
go-unit: add locked module helper (#147)
## Summary - add `ix.goUnit.buildWorkspace` for locked Go modules - require `go.mod`, `go.sum`, and `vendorHash`, with an explicit stdlib-only escape hatch - add a locked Go fixture that exercises package-shaped build and test derivations ## Checks - `nix run .#lint` - `nix build .#checks.x86_64-linux.eval --show-trace` Co-authored-by: Codex <codex@openai.com>
1 parent 065bc35 commit 8b70b23

11 files changed

Lines changed: 353 additions & 0 deletions

File tree

lib/default.nix

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,13 @@ let
277277
nixCargoUnit = buildIxRustTool pkgs paths.packages.nixCargoUnit;
278278
};
279279
cargoUnit = cargoUnitFor pkgs;
280+
goUnitFor =
281+
pkgs:
282+
import ./go-unit.nix {
283+
inherit lib pkgs;
284+
inherit (languages) go;
285+
};
286+
goUnit = goUnitFor pkgs;
280287

281288
/**
282289
Build a repo-owned Rust package with the shared Rust policy.
@@ -771,6 +778,7 @@ let
771778
buildNpmSite
772779
buildUvApplication
773780
cargoUnit
781+
goUnit
774782
languages
775783
minecraft
776784
mkMinecraftLoader
@@ -1038,6 +1046,8 @@ let
10381046
cargoUnit
10391047
cargoUnitFor
10401048
errors
1049+
goUnit
1050+
goUnitFor
10411051
languages
10421052
minecraft
10431053
mkMinecraftLoader

lib/go-unit.nix

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
{
2+
lib,
3+
pkgs,
4+
go,
5+
}:
6+
let
7+
sanitizePackage =
8+
package:
9+
let
10+
raw =
11+
if package == "." || package == "./" then
12+
"root"
13+
else
14+
lib.replaceStrings
15+
[
16+
"./"
17+
"/"
18+
"."
19+
"*"
20+
]
21+
[
22+
""
23+
"-"
24+
"-"
25+
"all"
26+
]
27+
package;
28+
in
29+
if raw == "" then "root" else raw;
30+
31+
commonArgs =
32+
args:
33+
let
34+
src = args.src or (throw "goUnit.buildWorkspace requires src");
35+
modRoot = args.modRoot or ".";
36+
moduleRoot = if modRoot == "." then src else src + "/${modRoot}";
37+
goMod = args.goMod or (moduleRoot + "/go.mod");
38+
goSum = args.goSum or (moduleRoot + "/go.sum");
39+
in
40+
assert lib.assertMsg (builtins.pathExists goMod)
41+
"goUnit.buildWorkspace requires a checked-in go.mod at ${builtins.toString goMod}";
42+
assert lib.assertMsg (builtins.pathExists goSum)
43+
"goUnit.buildWorkspace requires a checked-in go.sum lockfile at ${builtins.toString goSum}";
44+
{
45+
pname = args.pname or "go-unit";
46+
version = args.version or "0.0.0";
47+
inherit
48+
src
49+
goMod
50+
goSum
51+
modRoot
52+
moduleRoot
53+
;
54+
vendorHash =
55+
args.vendorHash or (throw "goUnit.buildWorkspace requires vendorHash from pkgs.buildGoModule");
56+
packages =
57+
let
58+
packages = args.packages or [ "." ];
59+
in
60+
if packages == [ ] then throw "goUnit.buildWorkspace requires at least one package" else packages;
61+
goToolchain = args.goToolchain or go.toolchain pkgs { version = "latest"; };
62+
nativeBuildInputs = args.nativeBuildInputs or [ ];
63+
buildInputs = args.buildInputs or [ ];
64+
env = args.env or { };
65+
ldflags = args.ldflags or [ ];
66+
tags = args.tags or [ ];
67+
};
68+
69+
buildPackage =
70+
args: package:
71+
let
72+
buildGoModule = pkgs.buildGoModule.override { go = args.goToolchain; };
73+
in
74+
buildGoModule {
75+
pname = "${args.pname}-${sanitizePackage package}";
76+
inherit (args)
77+
version
78+
vendorHash
79+
goSum
80+
nativeBuildInputs
81+
buildInputs
82+
env
83+
ldflags
84+
tags
85+
;
86+
src = args.moduleRoot;
87+
modRoot = ".";
88+
subPackages = [ package ];
89+
doCheck = false;
90+
strictDeps = true;
91+
passthru.goUnit = {
92+
inherit (args) goSum goToolchain env;
93+
inherit package;
94+
};
95+
};
96+
97+
testPackage =
98+
args: package:
99+
let
100+
buildGoModule = pkgs.buildGoModule.override { go = args.goToolchain; };
101+
in
102+
buildGoModule {
103+
pname = "${args.pname}-${sanitizePackage package}-test";
104+
inherit (args)
105+
version
106+
vendorHash
107+
goSum
108+
nativeBuildInputs
109+
buildInputs
110+
env
111+
ldflags
112+
tags
113+
;
114+
src = args.moduleRoot;
115+
modRoot = ".";
116+
subPackages = [ package ];
117+
doCheck = true;
118+
strictDeps = true;
119+
installPhase = ''
120+
mkdir -p "$out"
121+
touch "$out/done"
122+
'';
123+
passthru.goUnit = {
124+
inherit (args) goSum goToolchain env;
125+
inherit package;
126+
};
127+
};
128+
129+
/**
130+
Build and test a locked Go module as package-shaped Nix derivations.
131+
132+
Go does not expose Cargo's rustc unit graph, so callers choose the package
133+
patterns that deserve independent cache and test boundaries. The helper
134+
requires `go.mod`, `go.sum`, and `vendorHash`.
135+
136+
Arguments:
137+
- `src`: filtered module source.
138+
- `packages`: Go package patterns to expose, default `[ "." ]`.
139+
- `vendorHash`: hash accepted by `pkgs.buildGoModule`.
140+
- `goToolchain`: optional Go package, default `ix.languages.go.toolchain pkgs { version = "latest"; }`.
141+
142+
Returns `packages`, `tests`, `default`, `checks`, and `sourceAudit`.
143+
*/
144+
buildWorkspace =
145+
rawArgs:
146+
let
147+
args = commonArgs rawArgs;
148+
packageNames = map sanitizePackage args.packages;
149+
uniquePackageNames = lib.unique packageNames;
150+
packageAttrs = lib.listToAttrs (
151+
lib.zipListsWith (
152+
name: package: lib.nameValuePair name (buildPackage args package)
153+
) packageNames args.packages
154+
);
155+
testAttrs = lib.listToAttrs (
156+
lib.zipListsWith (
157+
name: package: lib.nameValuePair name (testPackage args package)
158+
) packageNames args.packages
159+
);
160+
in
161+
assert lib.assertMsg (builtins.length uniquePackageNames == builtins.length packageNames)
162+
"goUnit.buildWorkspace package patterns must sanitize to unique names: ${lib.concatStringsSep ", " args.packages}";
163+
{
164+
packages = packageAttrs;
165+
tests = testAttrs;
166+
checks = testAttrs;
167+
default = packageAttrs.${builtins.head packageNames};
168+
sourceAudit = {
169+
module = {
170+
base = "workspace";
171+
scope = "module";
172+
relative = args.modRoot;
173+
lockFile = if builtins.pathExists args.goSum then "go.sum" else null;
174+
};
175+
};
176+
};
177+
in
178+
{
179+
inherit buildWorkspace;
180+
}

tests/default.nix

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,49 @@ let
604604
};
605605
};
606606

607+
goUnitFixture = fs.toSource {
608+
root = ./fixtures/go-unit-hello;
609+
fileset = fs.unions [
610+
./fixtures/go-unit-hello/go.mod
611+
./fixtures/go-unit-hello/go.sum
612+
./fixtures/go-unit-hello/main.go
613+
./fixtures/go-unit-hello/main_test.go
614+
];
615+
};
616+
617+
goUnitWorkspace = ix.goUnit.buildWorkspace {
618+
pname = "go-unit-hello";
619+
src = goUnitFixture;
620+
vendorHash = "sha256-36P4vOdzJotmVZon5Zud/d/jxzv4ad04aQT2G/EE3U8=";
621+
env.GOFLAGS = "-mod=readonly";
622+
packages = [ "." ];
623+
};
624+
625+
goUnitNestedFixture = fs.toSource {
626+
root = ./fixtures/go-unit-nested;
627+
fileset = ./fixtures/go-unit-nested/module;
628+
};
629+
630+
goUnitNestedWorkspace = ix.goUnit.buildWorkspace {
631+
pname = "go-unit-nested";
632+
src = goUnitNestedFixture;
633+
modRoot = "module";
634+
vendorHash = "sha256-36P4vOdzJotmVZon5Zud/d/jxzv4ad04aQT2G/EE3U8=";
635+
packages = [ "." ];
636+
};
637+
638+
goUnitPackageCollisionEval =
639+
builtins.tryEval
640+
(ix.goUnit.buildWorkspace {
641+
pname = "go-unit-collision";
642+
src = goUnitFixture;
643+
vendorHash = "sha256-36P4vOdzJotmVZon5Zud/d/jxzv4ad04aQT2G/EE3U8=";
644+
packages = [
645+
"a.b"
646+
"a/b"
647+
];
648+
}).packages;
649+
607650
cargoUnitScopePolicy = {
608651
denyUnusedCrateDependencies = false;
609652
cargoAudit.enable = false;
@@ -2538,6 +2581,40 @@ let
25382581
assertion = cargoUnitWorkspace.policyChecks ? unusedCrateDependencies;
25392582
message = "cargo-unit workspaces should expose an unused dependency policy check by default";
25402583
}
2584+
{
2585+
assertion = goUnitWorkspace.sourceAudit.module.lockFile == "go.sum";
2586+
message = "go-unit workspaces should require and report the Go module lockfile";
2587+
}
2588+
{
2589+
assertion = goUnitWorkspace.packages ? root;
2590+
message = "go-unit workspaces should expose package-shaped build derivations";
2591+
}
2592+
{
2593+
assertion = goUnitWorkspace.packages.root.goUnit.goSum == goUnitFixture + "/go.sum";
2594+
message = "go-unit package derivations should pass go.sum through to buildGoModule";
2595+
}
2596+
{
2597+
assertion =
2598+
goUnitWorkspace.packages.root.goUnit.goToolchain
2599+
== ix.languages.go.toolchain pkgs { version = "latest"; };
2600+
message = "go-unit package derivations should use the selected Go toolchain";
2601+
}
2602+
{
2603+
assertion = goUnitWorkspace.packages.root.goUnit.env.GOFLAGS == "-mod=readonly";
2604+
message = "go-unit package derivations should preserve buildGoModule env values";
2605+
}
2606+
{
2607+
assertion = goUnitWorkspace.tests ? root;
2608+
message = "go-unit workspaces should expose package-shaped test derivations";
2609+
}
2610+
{
2611+
assertion = goUnitNestedWorkspace.sourceAudit.module.relative == "module";
2612+
message = "go-unit workspaces should resolve default go.mod and go.sum below modRoot";
2613+
}
2614+
{
2615+
assertion = !goUnitPackageCollisionEval.success;
2616+
message = "go-unit workspaces should reject package patterns with colliding output names";
2617+
}
25412618
{
25422619
assertion =
25432620
let
@@ -3222,6 +3299,12 @@ let
32223299
test -e ${cargoUnitTangoComparison}/done
32233300
grep -q '^cargo-unit-hello greeting ' ${cargoUnitTangoComparison}/benchmarks.tsv
32243301
grep -q '^greeting ' ${cargoUnitTangoComparison}/logs/cargo-unit-hello-greeting.log
3302+
${goUnitWorkspace.default}/bin/go-unit-hello > go-unit-hello.out
3303+
grep -q 'hello from go-unit: Hello, world.' go-unit-hello.out
3304+
test -e ${goUnitWorkspace.tests.root}/done
3305+
${goUnitNestedWorkspace.default}/bin/go-unit-nested > go-unit-nested.out
3306+
grep -q 'hello from nested go-unit: Hello, world.' go-unit-nested.out
3307+
test -e ${goUnitNestedWorkspace.tests.root}/done
32253308
32263309
grep -q 'class="ix bun"' ${bunSite}/share/bun-site-fixture/index.html
32273310
test -d ${bunSite.bunNodeModules}/node_modules/clsx
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module ix.test/go-unit-hello
2+
3+
go 1.25.0
4+
5+
require rsc.io/quote/v4 v4.0.1
6+
7+
require (
8+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
9+
rsc.io/sampler v1.3.0 // indirect
10+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
2+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
3+
rsc.io/quote/v4 v4.0.1 h1:i/LHLEinr65wwTCqlP4OnMoMWeCgnFIZFvifdXNK+5M=
4+
rsc.io/quote/v4 v4.0.1/go.mod h1:w/DafQky66grMesu3uPhdDMS3knhBippwwemZtMOyCI=
5+
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
6+
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"rsc.io/quote/v4"
7+
)
8+
9+
func Message() string {
10+
return "hello from go-unit: " + quote.Hello()
11+
}
12+
13+
func main() {
14+
fmt.Println(Message())
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package main
2+
3+
import "testing"
4+
5+
func TestMessage(t *testing.T) {
6+
if got := Message(); got != "hello from go-unit: Hello, world." {
7+
t.Fatalf("Message() = %q", got)
8+
}
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module ix.test/go-unit-nested
2+
3+
go 1.25.0
4+
5+
require rsc.io/quote/v4 v4.0.1
6+
7+
require (
8+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
9+
rsc.io/sampler v1.3.0 // indirect
10+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
2+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
3+
rsc.io/quote/v4 v4.0.1 h1:i/LHLEinr65wwTCqlP4OnMoMWeCgnFIZFvifdXNK+5M=
4+
rsc.io/quote/v4 v4.0.1/go.mod h1:w/DafQky66grMesu3uPhdDMS3knhBippwwemZtMOyCI=
5+
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
6+
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"rsc.io/quote/v4"
7+
)
8+
9+
func Message() string {
10+
return "hello from nested go-unit: " + quote.Hello()
11+
}
12+
13+
func main() {
14+
fmt.Println(Message())
15+
}

0 commit comments

Comments
 (0)