Compare commits

..
2 Commits
Author SHA1 Message Date
archvillainette eb5d15061c fix(pipeline): make the full module build work for module-only projects (#17)
test / test (push) Successful in 1m23s
build-binaries / build-binaries (push) Successful in 2m9s
build-image / publish (push) Successful in 52s
`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>

Reviewed-on: #17
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
2026-06-24 15:32:01 +00:00
archvillainette 5c46824ebd fix depot cdn url (#16)
build-binaries / build-binaries (push) Successful in 2m10s
test / test (push) Successful in 1m23s
build-image / publish (push) Successful in 55s
Reviewed-on: #16
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
2026-06-22 12:34:12 +00:00
4 changed files with 100 additions and 30 deletions
+7 -1
View File
@@ -661,11 +661,17 @@ func collectAssetResources(p *project.Project, requireContent bool, allowed map[
if err != nil { if err != nil {
return nil, err return nil, err
} }
assetsRelPath, err := filepath.Rel(p.Root, p.AssetsDir()) // A module-only project (consumes prebuilt HAKs, has no paths.assets) has no
// asset files to collect — AssetFiles is empty and AssetsDir() is "". Skip the
// relative-path resolve, which would otherwise fail with `Rel: can't make ""`.
assetsRelPath := ""
if p.AssetsDir() != "" {
assetsRelPath, err = filepath.Rel(p.Root, p.AssetsDir())
if err != nil { if err != nil {
return nil, fmt.Errorf("resolve assets dir relative path: %w", err) return nil, fmt.Errorf("resolve assets dir relative path: %w", err)
} }
assetsRelPath = filepath.ToSlash(assetsRelPath) assetsRelPath = filepath.ToSlash(assetsRelPath)
}
for _, rel := range p.Inventory.AssetFiles { for _, rel := range p.Inventory.AssetFiles {
if musicAssets != nil { if musicAssets != nil {
+6 -1
View File
@@ -435,7 +435,11 @@ func writeCreditsArtifacts(p *project.Project, generated []musicCreditGroup) (cr
} }
} }
err := filepath.WalkDir(p.AssetsDir(), func(path string, d os.DirEntry, walkErr error) error { // Authored credits.md files live under the assets tree. A project with no
// configured assets dir (e.g. a module that consumes prebuilt HAKs) has none
// to collect — walking "" would fail with `lstat : no such file or directory`.
if assetsDir := p.AssetsDir(); assetsDir != "" {
err := filepath.WalkDir(assetsDir, func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil { if walkErr != nil {
return walkErr return walkErr
} }
@@ -463,6 +467,7 @@ func writeCreditsArtifacts(p *project.Project, generated []musicCreditGroup) (cr
if err != nil { if err != nil {
return creditsArtifactWriteResult{}, err return creditsArtifactWriteResult{}, err
} }
}
sort.Slice(sources, func(i, j int) bool { return sources[i].Path < sources[j].Path }) sort.Slice(sources, func(i, j int) bool { return sources[i].Path < sources[j].Path })
payload, err := json.MarshalIndent(music.CreditsInventory{Sources: sources}, "", " ") payload, err := json.MarshalIndent(music.CreditsInventory{Sources: sources}, "", " ")
+59
View File
@@ -4084,3 +4084,62 @@ haks:
t.Fatalf("plan-only should not stage music, stat err=%v", err) 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)
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ import (
const autogenManifestCacheMaxAge = time.Hour const autogenManifestCacheMaxAge = time.Hour
const defaultBunnyCDNBase = "https://depot.westgate.pw" const defaultBunnyCDNBase = "https://cdn-a7f3k9.westgate.pw"
// errAutogenManifestUnavailable marks a released autogen manifest that could not // errAutogenManifestUnavailable marks a released autogen manifest that could not
// be located or fetched (network failure, missing release/asset, empty payload, // be located or fetched (network failure, missing release/asset, empty payload,