Go
mkSecureArtifact detects a Go project by the presence of a go.mod file at
the root of src. It then builds the module with buildGoModule and hands the
result to mkHardenedImage.
The marker
Detection is file-presence based: a go.mod at the top of src means “go”. If
a .goreleaser.yml / .goreleaser.yaml is also present, detect records it as
the buildConfigFile, but it does not act on it automatically — you opt into
the goreleaser path explicitly (below).
Required and optional ecosystemArgs
vendorHash(required) — thebuildGoModulevendor hash. There is no way around this; a missingvendorHashthrows at eval time.subPackages(optional) — which packages to build (default[ "." ]).goreleaser(optional) — a parsed goreleaser config; when present, the build args (subPackages, ldflags,CGO_ENABLED) come from it instead ofsubPackages.goVersion,buildId(optional) — only meaningful on the goreleaser path.
Plain buildGoModule (subPackages)
hb.mkSecureArtifact {
name = "my-tool";
version = "1.2.0";
src = ./.;
ecosystemArgs = {
vendorHash = "sha256-…";
subPackages = [ "cmd/my-tool" ];
};
image.cmd = [ "/bin/my-tool" ];
}
Anything under image.* is forwarded to mkHardenedImage — that is how you set
cmd, entrypoint, env, and so on.
The goreleaser path
Many gitlab-org/* Go tools drive their release builds with goreleaser. Parse
the config and pass it through so the resulting binary carries goreleaser’s
ldflags (version stamping, CGO_ENABLED, the right main package):
let
gorel = hardened-builds.lib.${system}.parseGoreleaser ./.goreleaser.yml; # see note
in
hb.mkSecureArtifact {
name = "glab";
version = "1.40.0";
src = ./.;
rev = self.rev or "0000000000000000000000000000000000000000";
ecosystemArgs = {
vendorHash = "sha256-…";
goreleaser = gorel;
goVersion = "1.22.3";
};
}
The emitter resolves goreleaser’s {{.Version}}, {{.ShortCommit}}, and
{{.Env.GO_VERSION}} template placeholders by plain string substitution against
version, rev, and goVersion. rev must be the real full commit SHA —
{{.ShortCommit}} is the first 7 characters of it — even if you fetch the
source by tag elsewhere. Any other unresolved {{…}} placeholder throws at eval
time rather than baking a literal template string into your binary.
Note: the goreleaser parser lives in
lib/ecosystem/goreleaser.nix. Wire it into your consumer flake the same way you wire the rest of the API; the exact attribute name is in the API reference.
Derivation-first alternative
If the source-first dispatch doesn’t fit — a monorepo with several binaries, an
unusual build, or you already have a package — build the buildGoModule
derivation yourself and pass it straight to mkHardenedImage:
let
myTool = pkgs.buildGoModule {
pname = "my-tool";
version = "1.2.0";
src = ./.;
vendorHash = "sha256-…";
subPackages = [ "cmd/my-tool" ];
};
in
hb.mkHardenedImage {
name = "my-tool";
version = "1.2.0";
package = myTool;
cmd = [ "/bin/my-tool" ];
}
You get the same image and SBOM either way; mkSecureArtifact is just the
convenience wrapper around detect-then-build-then-image.
SBOM
The Go module set and the Go stdlib are folded into the CycloneDX SBOM
automatically (the SBOM builder reads them off the finished buildGoModule
derivation’s goModules). No extra configuration needed. See SBOMs.
FIPS
Go’s FIPS story is BoringCrypto (ADR-0026). Pass fips = true only if you
built the package under the FIPS package set — the flag applies FIPS labels but
does not convert a non-FIPS build. See FIPS.