Builds sow-tools#53. Format spec followed is the resolution comment of sow-platform#94, checked line by line against niv/neverwinter.nim at HEAD (`nwsync.nim`, `compressedbuf.nim`, `nwsync/private/libupdate.nim`). ## What lands `crucible nwsync emit <artifact> --out DIR` — explodes one `.hak`/`.erf`, or one loose file such as the TLK, into NWSync blobs plus a NSYM v3 manifest covering only that artifact, with the same `.json` sidecar upstream writes. Blob path `data/sha1/<h0h1>/<h2h3>/<sha1>`, body in NWCompressedBuffer framing (magic `NSYC`, version 3, algorithm 2, uncompressed size, zstd header version 1, dictionary 0, raw zstd frame). The sha1 that names a blob is over the uncompressed bytes. `crucible nwsync assemble --order NAMES --entries DIR --out DIR [--group-id N]` — merges the per-artifact manifests into one, reading no bulk data at all. Merge rule is resref shadowing, not concatenation: a resref in more than one artifact resolves to the earliest artifact in `--order`, which is how the game resolves it. `--group-id` stays caller-supplied (1 current, 2 testing; 0 is absent, matching upstream omitting a zero integer meta field). Rules taken from upstream and not re-invented: `nss`/`ndb`/`gic` always skipped; an unresolvable restype is a hard error, not a skip; a resource over 15 MB fails closed; no `latest` file and no `.origin` file, ever. A `.mod` is refused outright — a persistent world publishes no module contents, so the module contributes no bytes. ## Two deliberate departures - **Emitter version is its own field, not the build revision.** `emitter_version` is a constant bumped only when emitted bytes change. Keying the refuse-to-merge check on `created_with` would invalidate every published index on every unrelated crucible commit and force a re-emit of the whole 15 GB corpus — the opposite of "nothing downstream ever needs the hak again". - **`SOURCE_DATE_EPOCH` pins the sidecar timestamp.** The manifest itself was already deterministic; the sidecar's `created` was not, against the determinism rule in `docs/consumer-contract.md`. ## Not in this PR, and why - **Direct upload.** Only the local `--out` sink exists, which is the conformance path. The upload sink and the mid-hak-failure question are sow-tools#60, and the consumer wiring is #65. - **The conformance run against upstream.** sow-tools#59 owns getting `nwn_nwsync_write` running and capturing reference output. The format here was read from upstream source rather than from its output, so the byte-for-byte manifest comparison and the after-decompression blob comparison still have to happen — that is what #59 is for. The tests in this PR check the layout against the spec, so a shared misreading would pass them. - **The `artifacts/haks/sha256/<a>/<b>/<sha256>.nsym` location.** emit writes `<out>/<name>.nsym`; where a publisher puts it is the publisher's business (#65). - **The acceptance gate** — a real client syncing from an assembled manifest — is unchanged and still open. ## Checks `make check` green (vet, unit tests, shellcheck, yamllint, workflow contract), `make smoke` green with the new builder, `nix build .#crucible` produces `crucible-nwsync`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #71 Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
91 lines
3.0 KiB
Nix
91 lines
3.0 KiB
Nix
{
|
|
description = "sow-tools / Crucible — NWN build/conversion/sync toolchain (self-contained Go devshell)";
|
|
|
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
|
|
outputs = { self, nixpkgs }:
|
|
let
|
|
systems = [ "x86_64-linux" "aarch64-linux" ];
|
|
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
|
|
in
|
|
{
|
|
# Self-contained (D8): a host with ONLY nix can enter this shell and run
|
|
# `make check`.
|
|
# Daemonless image build (D8): `nix build .#image` produces an OCI tarball
|
|
# with no docker/podman daemon. CI loads/pushes it with skopeo. This is the
|
|
# Nix-native replacement for `docker build` on the host-mode runner, which
|
|
# has no container runtime.
|
|
packages = forAllSystems (pkgs:
|
|
let
|
|
version = self.shortRev or self.dirtyShortRev or "unknown";
|
|
crucible = pkgs.buildGoModule {
|
|
pname = "crucible";
|
|
inherit version;
|
|
src = ./.;
|
|
vendorHash = "sha256-0I8j7On9YGD2GK9xbj/KkgBrlkMJ6Y6XQv+KCLTgBBU=";
|
|
subPackages = [
|
|
"cmd/crucible"
|
|
"cmd/crucible-depot"
|
|
"cmd/crucible-hak"
|
|
"cmd/crucible-module"
|
|
"cmd/crucible-nwsync"
|
|
"cmd/crucible-topdata"
|
|
"cmd/crucible-wiki"
|
|
];
|
|
env.CGO_ENABLED = 0;
|
|
ldflags = [
|
|
"-s"
|
|
"-w"
|
|
"-X git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/buildinfo.Version=${version}"
|
|
];
|
|
# Unit tests run in the `test` workflow / `make check`, not here.
|
|
doCheck = false;
|
|
};
|
|
in
|
|
{
|
|
inherit crucible;
|
|
default = crucible;
|
|
|
|
image = pkgs.dockerTools.buildLayeredImage {
|
|
name = "registry.westgate.pw/deployment/crucible";
|
|
tag = version;
|
|
contents = [ crucible pkgs.cacert pkgs.fakeNss ];
|
|
config = {
|
|
Entrypoint = [ "/bin/crucible" ];
|
|
Cmd = [ "help" ];
|
|
User = "65532:65532";
|
|
Env = [
|
|
"PATH=/bin"
|
|
"SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"
|
|
];
|
|
};
|
|
};
|
|
});
|
|
|
|
devShells = forAllSystems (pkgs: {
|
|
default = pkgs.mkShell {
|
|
name = "sow-tools";
|
|
packages = with pkgs; [
|
|
go
|
|
gopls
|
|
gotools
|
|
git
|
|
tea
|
|
gnumake
|
|
bashInteractive
|
|
shellcheck
|
|
yamllint
|
|
jq
|
|
];
|
|
shellHook = ''
|
|
export GOCACHE="$PWD/.cache/go-build"
|
|
echo "sow-tools / Crucible devshell (self-contained). $(go version 2>/dev/null)"
|
|
echo " make check go vet + go test + shellcheck + yamllint"
|
|
echo " make build build every cmd/* into ./bin"
|
|
echo " make smoke build + run the binary smoke test"
|
|
'';
|
|
};
|
|
});
|
|
};
|
|
}
|