feat(nwsync): emit blobs and per-artifact NSYM, assemble merged manifests
Adds `crucible nwsync emit` and `crucible nwsync assemble`, the split replacement for upstream nwn_nwsync_write, which cannot be used because it wants every hak, the TLK and the module present in one run on one disk. emit explodes one artifact — a .hak/.erf or a loose file such as the TLK — into NWSync blobs (NWCompressedBuffer framing, magic NSYC, zstd) plus a NSYM v3 manifest describing only that artifact, with the same .json sidecar upstream writes. Blob names are the sha1 of the uncompressed bytes, restypes nss/ndb/gic are always skipped, an unresolvable restype is a hard error, a resource over 15 MB fails closed, and no latest or .origin file is ever written. assemble merges the per-artifact manifests into one, reading no bulk data. The merge rule is resref shadowing, not concatenation: a resref present in more than one artifact resolves to the earliest artifact in --order, the way the game resolves it. It refuses to merge across mismatched emitter versions, and takes --group-id from the caller (1 current, 2 testing; 0 is absent). Refs #53.
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// Package nwsync publishes NWSync repository data: blobs and a per-artifact
|
||||
// NSYM manifest at each artifact's birth (emit), and one merged manifest at
|
||||
// module release (assemble).
|
||||
//
|
||||
// The split exists because upstream nwn_nwsync_write wants every hak, the TLK
|
||||
// and the module present in one run on one disk, which our build hosts cannot
|
||||
// hold. Upstream stays the conformance oracle: manifests compare byte for
|
||||
// byte, blobs compare after decompression.
|
||||
package nwsync
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
exitOK = 0
|
||||
exitUsage = 64
|
||||
exitInternal = 70
|
||||
)
|
||||
|
||||
// Run executes an nwsync subcommand. args[0] is the subcommand (emit|assemble);
|
||||
// returns the process exit code.
|
||||
func Run(args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 {
|
||||
printRunUsage(stderr)
|
||||
return exitUsage
|
||||
}
|
||||
switch args[0] {
|
||||
case "emit":
|
||||
return runEmit(args[1:], stdout, stderr)
|
||||
case "assemble":
|
||||
return runAssemble(args[1:], stdout, stderr)
|
||||
case "-h", "--help", "help":
|
||||
printRunUsage(stdout)
|
||||
return exitOK
|
||||
default:
|
||||
fmt.Fprintf(stderr, "nwsync: unknown subcommand %q\n\n", args[0])
|
||||
printRunUsage(stderr)
|
||||
return exitUsage
|
||||
}
|
||||
}
|
||||
|
||||
func printRunUsage(w io.Writer) {
|
||||
fmt.Fprint(w, `usage:
|
||||
nwsync emit <artifact> --out DIR
|
||||
nwsync assemble --order NAMES --entries DIR --out DIR [--group-id N]
|
||||
|
||||
emit explodes one .hak/.erf or one loose file (the TLK) into NWSync blobs plus
|
||||
a NSYM manifest covering only that artifact. assemble merges those per-artifact
|
||||
manifests into one, reading no bulk data.
|
||||
`)
|
||||
}
|
||||
|
||||
func runEmit(args []string, stdout, stderr io.Writer) int {
|
||||
fs := flag.NewFlagSet("emit", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
out := fs.String("out", "", "output directory (blobs plus the per-artifact manifest)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return exitUsage
|
||||
}
|
||||
if fs.NArg() != 1 {
|
||||
fmt.Fprintf(stderr, "nwsync emit: exactly one artifact is required\n")
|
||||
return exitUsage
|
||||
}
|
||||
if *out == "" {
|
||||
fmt.Fprintf(stderr, "nwsync emit: --out is required\n")
|
||||
return exitUsage
|
||||
}
|
||||
|
||||
result, err := Emit(fs.Arg(0), *out)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "nwsync emit: %v\n", err)
|
||||
return exitInternal
|
||||
}
|
||||
fmt.Fprintf(stdout, "emitted %s: %d resources, %d new blobs, manifest %s\n",
|
||||
result.Name, result.Entries, result.BlobsWritten, result.ManifestPath)
|
||||
return exitOK
|
||||
}
|
||||
|
||||
func runAssemble(args []string, stdout, stderr io.Writer) int {
|
||||
fs := flag.NewFlagSet("assemble", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
order := fs.String("order", "", "comma-separated artifact names, highest priority first")
|
||||
entries := fs.String("entries", "", "directory holding the per-artifact .nsym files")
|
||||
out := fs.String("out", "", "repository root; the manifest lands in <out>/manifests")
|
||||
groupID := fs.Int("group-id", 0, "NWSync group id (1 = current, 2 = testing; 0 omits it)")
|
||||
moduleName := fs.String("module-name", "", "module name recorded in the sidecar")
|
||||
description := fs.String("description", "", "description recorded in the sidecar")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return exitUsage
|
||||
}
|
||||
switch {
|
||||
case *order == "":
|
||||
fmt.Fprintf(stderr, "nwsync assemble: --order is required\n")
|
||||
return exitUsage
|
||||
case *entries == "":
|
||||
fmt.Fprintf(stderr, "nwsync assemble: --entries is required\n")
|
||||
return exitUsage
|
||||
case *out == "":
|
||||
fmt.Fprintf(stderr, "nwsync assemble: --out is required\n")
|
||||
return exitUsage
|
||||
}
|
||||
|
||||
names := make([]string, 0, 8)
|
||||
for _, name := range strings.Split(*order, ",") {
|
||||
if name = strings.TrimSpace(name); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := Assemble(AssembleOptions{
|
||||
Order: names,
|
||||
EntriesDir: *entries,
|
||||
OutDir: *out,
|
||||
GroupID: *groupID,
|
||||
ModuleName: *moduleName,
|
||||
Description: *description,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "nwsync assemble: %v\n", err)
|
||||
return exitInternal
|
||||
}
|
||||
fmt.Fprintf(stdout, "assembled %s: %d resources from %d artifacts\n",
|
||||
result.SHA1, result.Entries, len(names))
|
||||
return exitOK
|
||||
}
|
||||
Reference in New Issue
Block a user