fix(pipeline): make the full module build work for module-only projects
build-binaries / build-binaries (pull_request) Successful in 2m13s
test-image / build-image (pull_request) Successful in 46s
test / test (pull_request) Successful in 1m23s

`crucible module build` runs the full pipeline (module + HAK producer +
topdata). A module-only project consumes prebuilt HAKs and has no
`paths.assets`, so the HAK stage crashed on the unset assets dir: the
music-credits walk hit `filepath.WalkDir("")` (`lstat : no such file or
directory`) and asset collection hit `filepath.Rel(root, "")`.

Guard both on an unset `AssetsDir()`: an absent assets tree simply means
no authored credits and no asset files to collect, so the HAK stage
no-ops cleanly while topdata generated-2DA assets still build. Honors
spec Decision 3 (canonical `crucible module build`, no `build-module`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 17:21:59 +02:00
co-authored by Claude Opus 4.8
parent 5c46824ebd
commit e8e8ba421a
3 changed files with 99 additions and 29 deletions
+59
View File
@@ -4084,3 +4084,62 @@ haks:
t.Fatalf("plan-only should not stage music, stat err=%v", err)
}
}
// A module-only project (consumes prebuilt HAKs, no paths.assets) must build via
// the full `crucible module build` (pipeline.Build) without an assets tree. The
// HAK + music stages used to crash on the unset assets dir
// (`lstat : no such file or directory` / `Rel: can't make "" relative`).
func TestBuildModuleOnlyProjectWithoutAssetsDir(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src", "module"))
mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
name: Test Module
resref: testmod
paths:
source: src
build: build
`)
mustWriteFile(t, filepath.Join(root, "src", "module", "module.ifo.json"), `{
"file_type": "IFO ",
"file_version": "V3.2",
"root": {
"struct_type": 0,
"fields": [
{
"label": "Mod_Name",
"type": "CExoString",
"value": "Test Module"
}
]
}
}
`)
p, err := project.Load(root)
if err != nil {
t.Fatalf("load project: %v", err)
}
if err := p.ValidateLayout(); err != nil {
t.Fatalf("validate layout: %v", err)
}
if err := p.Scan(); err != nil {
t.Fatalf("scan: %v", err)
}
if p.AssetsDir() != "" {
t.Fatalf("test precondition: expected unset assets dir, got %q", p.AssetsDir())
}
result, err := Build(p)
if err != nil {
t.Fatalf("full build of module-only project: %v", err)
}
if _, err := os.Stat(result.ModulePath); err != nil {
t.Fatalf("expected built .mod at %s: %v", result.ModulePath, err)
}
if result.HAKAssets != 0 {
t.Fatalf("module-only project has no assets, expected 0 HAK assets, got %d", result.HAKAssets)
}
}