Refactor build and topdata path resolution through project config

This commit is contained in:
2026-04-24 19:11:10 +02:00
parent d880661599
commit 831d3b47da
11 changed files with 431 additions and 64 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ type ApplyManifestResult struct {
func ApplyHAKManifest(p *project.Project, manifestPath string) (ApplyManifestResult, error) {
if manifestPath == "" {
manifestPath = filepath.Join(p.BuildDir(), "haks.json")
manifestPath = p.HAKManifestPath()
}
raw, err := os.ReadFile(manifestPath)
+17 -18
View File
@@ -133,7 +133,7 @@ func buildModule(p *project.Project, progress ProgressFunc) (BuildResult, error)
}
progressf(progress, "Writing module archive...")
outputPath := filepath.Join(p.BuildDir(), p.Config.Module.ResRef+".mod")
outputPath := p.ModuleArchivePath()
output, err := os.Create(outputPath)
if err != nil {
return BuildResult{}, fmt.Errorf("create module archive: %w", err)
@@ -197,7 +197,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo
}
var previousManifest *BuildManifest
if sourceManifest == nil || writeArchives {
previousManifest, err = loadPreviousBuildManifest(p.BuildDir())
previousManifest, err = loadPreviousBuildManifest(p.HAKManifestPath())
if err != nil {
return BuildResult{}, err
}
@@ -206,7 +206,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo
result := BuildResult{HAKAssets: len(assetResources)}
if len(assetResources) == 0 {
progressf(progress, "Cleaning previous generated HAKs...")
if err := cleanupGeneratedHAKs(p.BuildDir(), p.Config.Module.ResRef); err != nil {
if err := cleanupGeneratedHAKs(p); err != nil {
return BuildResult{}, err
}
return result, nil
@@ -254,7 +254,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo
manifest.HAKs = append(manifest.HAKs, manifestEntry)
}
manifestPath := filepath.Join(p.BuildDir(), "haks.json")
manifestPath := p.HAKManifestPath()
manifestBytes, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return BuildResult{}, fmt.Errorf("marshal hak manifest: %w", err)
@@ -263,7 +263,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo
if !writeArchives {
progressf(progress, "Cleaning previous generated HAKs...")
if err := cleanupGeneratedHAKs(p.BuildDir(), p.Config.Module.ResRef); err != nil {
if err := cleanupGeneratedHAKs(p); err != nil {
return BuildResult{}, err
}
progressf(progress, "Writing HAK manifest...")
@@ -278,7 +278,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo
result.HAKPaths = make([]string, 0, len(chunks))
for index, chunk := range chunks {
hakPath := filepath.Join(p.BuildDir(), chunk.Name+".hak")
hakPath := p.HAKArchivePath(chunk.Name)
manifestEntry := manifest.HAKs[index]
reused, err := reuseExistingHAK(previousManifest, manifestEntry, hakPath)
if err != nil {
@@ -305,7 +305,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo
}
progressf(progress, "Cleaning stale generated HAKs...")
if !preserveExistingHAKs {
if err := cleanupStaleGeneratedHAKs(p.BuildDir(), p.Config.Module.ResRef, currentNames); err != nil {
if err := cleanupStaleGeneratedHAKs(p, currentNames); err != nil {
return BuildResult{}, err
}
}
@@ -1427,8 +1427,7 @@ func chunkContentHash(chunk hakChunk) string {
return fmt.Sprintf("%x", sum.Sum(nil))
}
func loadPreviousBuildManifest(buildDir string) (*BuildManifest, error) {
manifestPath := filepath.Join(buildDir, "haks.json")
func loadPreviousBuildManifest(manifestPath string) (*BuildManifest, error) {
raw, err := os.ReadFile(manifestPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
@@ -1475,8 +1474,8 @@ func reuseExistingHAK(previousManifest *BuildManifest, current BuildManifestHAK,
return true, nil
}
func cleanupStaleGeneratedHAKs(buildDir, moduleResRef string, keepNames map[string]struct{}) error {
paths, err := manifestHAKPaths(buildDir)
func cleanupStaleGeneratedHAKs(p *project.Project, keepNames map[string]struct{}) error {
paths, err := manifestHAKPaths(p)
if err != nil {
return err
}
@@ -1490,8 +1489,8 @@ func cleanupStaleGeneratedHAKs(buildDir, moduleResRef string, keepNames map[stri
}
}
legacy := filepath.Join(buildDir, moduleResRef+".hak")
if _, keep := keepNames[moduleResRef]; !keep {
legacy := p.HAKArchivePath(p.Config.Module.ResRef)
if _, keep := keepNames[p.Config.Module.ResRef]; !keep {
if err := os.Remove(legacy); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("remove legacy generated hak %s: %w", legacy, err)
}
@@ -1514,8 +1513,8 @@ func matchPathPattern(path, pattern string) bool {
return matchSegments(pathSegs, patternSegs)
}
func cleanupGeneratedHAKs(buildDir, moduleResRef string) error {
paths, err := manifestHAKPaths(buildDir)
func cleanupGeneratedHAKs(p *project.Project) error {
paths, err := manifestHAKPaths(p)
if err != nil {
return err
}
@@ -1525,12 +1524,12 @@ func cleanupGeneratedHAKs(buildDir, moduleResRef string) error {
}
}
legacy := filepath.Join(buildDir, moduleResRef+".hak")
legacy := p.HAKArchivePath(p.Config.Module.ResRef)
if err := os.Remove(legacy); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("remove legacy generated hak %s: %w", legacy, err)
}
manifest := filepath.Join(buildDir, "haks.json")
manifest := p.HAKManifestPath()
if err := os.Remove(manifest); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("remove previous hak manifest: %w", err)
}
@@ -1538,7 +1537,7 @@ func cleanupGeneratedHAKs(buildDir, moduleResRef string) error {
}
func plannedModuleHAKOrder(p *project.Project) ([]string, error) {
manifest, err := loadPreviousBuildManifest(p.BuildDir())
manifest, err := loadPreviousBuildManifest(p.HAKManifestPath())
if err != nil {
return nil, err
}
+7 -8
View File
@@ -15,7 +15,6 @@ import (
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/gff"
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/topdata"
)
type CompareResult struct {
@@ -31,12 +30,12 @@ type resourceExpectation struct {
}
func Compare(p *project.Project) (CompareResult, error) {
modulePath := filepath.Join(p.BuildDir(), p.Config.Module.ResRef+".mod")
modulePath := p.ModuleArchivePath()
var hakPaths []string
if len(p.Inventory.AssetFiles) > 0 {
var err error
hakPaths, err = manifestHAKPaths(p.BuildDir())
hakPaths, err = manifestHAKPaths(p)
if err != nil {
return CompareResult{}, err
}
@@ -294,8 +293,8 @@ func resourceKey(name, extension string) string {
return strings.ToLower(name) + "." + strings.TrimPrefix(strings.ToLower(extension), ".")
}
func manifestHAKPaths(buildDir string) ([]string, error) {
manifestPath := filepath.Join(buildDir, "haks.json")
func manifestHAKPaths(p *project.Project) ([]string, error) {
manifestPath := p.HAKManifestPath()
raw, err := os.ReadFile(manifestPath)
if err == nil {
var manifest BuildManifest
@@ -304,7 +303,7 @@ func manifestHAKPaths(buildDir string) ([]string, error) {
}
paths := make([]string, 0, len(manifest.HAKs))
for _, hak := range manifest.HAKs {
paths = append(paths, filepath.Join(buildDir, hak.Name+".hak"))
paths = append(paths, p.HAKArchivePath(hak.Name))
}
return paths, nil
}
@@ -312,14 +311,14 @@ func manifestHAKPaths(buildDir string) ([]string, error) {
return nil, fmt.Errorf("read hak manifest: %w", err)
}
legacy := filepath.Join(buildDir, "*.hak")
legacy := filepath.Join(p.BuildDir(), "*.hak")
paths, err := filepath.Glob(legacy)
if err != nil {
return nil, fmt.Errorf("scan hak archives: %w", err)
}
filtered := make([]string, 0, len(paths))
for _, path := range paths {
if filepath.Base(path) == topdata.PackageHAKFileName {
if filepath.Base(path) == p.TopDataPackageHAKName() {
continue
}
filtered = append(filtered, path)
+14 -7
View File
@@ -16,12 +16,13 @@ import (
)
type ExtractResult struct {
ModulePath string
HAKPaths []string
Written int
Overwritten int
Removed int
Skipped int
ModulePath string
HAKPaths []string
DeletedArchivePaths []string
Written int
Overwritten int
Removed int
Skipped int
}
func Extract(p *project.Project, files ...string) (ExtractResult, error) {
@@ -36,7 +37,7 @@ func Extract(p *project.Project, files ...string) (ExtractResult, error) {
shouldExtractMod := len(allowed) == 0 || allowed[p.Config.Module.ResRef+".mod"]
if shouldExtractMod {
modulePath := filepath.Join(p.BuildDir(), p.Config.Module.ResRef+".mod")
modulePath := p.ModuleArchivePath()
input, err := os.Open(modulePath)
if err != nil {
return ExtractResult{}, fmt.Errorf("open module archive: %w", err)
@@ -94,6 +95,12 @@ func Extract(p *project.Project, files ...string) (ExtractResult, error) {
if len(failures) > 0 {
return result, errors.Join(failures...)
}
if p.Config.Extract.DeleteModuleArchiveAfterSuccess && result.ModulePath != "" {
if err := os.Remove(result.ModulePath); err != nil {
return result, fmt.Errorf("delete consumed module archive %s: %w", result.ModulePath, err)
}
result.DeletedArchivePaths = append(result.DeletedArchivePaths, result.ModulePath)
}
return result, nil
}
+135
View File
@@ -175,6 +175,141 @@ func TestExtractReadsHAKAssets(t *testing.T) {
}
}
func TestExtractDeletesConsumedModuleArchiveAfterSuccessWhenConfigured(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src", "module"))
mustMkdir(t, filepath.Join(root, "assets"))
mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{
"module": {
"name": "Test Module",
"resref": "testmod"
},
"paths": {
"source": "src",
"assets": "assets",
"build": "build"
},
"extract": {
"delete_module_archive_after_success": true
}
}
`)
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": "Original 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)
}
buildResult, err := BuildModule(p)
if err != nil {
t.Fatalf("build module: %v", err)
}
if err := os.Remove(filepath.Join(root, "src", "module", "module.ifo.json")); err != nil {
t.Fatalf("remove source file: %v", err)
}
if err := p.Scan(); err != nil {
t.Fatalf("rescan: %v", err)
}
result, err := Extract(p)
if err != nil {
t.Fatalf("extract: %v", err)
}
if len(result.DeletedArchivePaths) != 1 {
t.Fatalf("expected 1 deleted archive path, got %#v", result.DeletedArchivePaths)
}
if result.DeletedArchivePaths[0] != buildResult.ModulePath {
t.Fatalf("expected deleted archive %s, got %#v", buildResult.ModulePath, result.DeletedArchivePaths)
}
if _, err := os.Stat(buildResult.ModulePath); !os.IsNotExist(err) {
t.Fatalf("expected consumed module archive to be removed, stat err=%v", err)
}
if _, err := os.Stat(filepath.Join(root, "src", "module", "module.ifo.json")); err != nil {
t.Fatalf("expected extracted module file: %v", err)
}
}
func TestExtractKeepsConsumedModuleArchiveWhenExtractionFails(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src"))
mustMkdir(t, filepath.Join(root, "assets"))
mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{
"module": {
"name": "Test Module",
"resref": "testmod"
},
"paths": {
"source": "src",
"assets": "assets",
"build": "build"
},
"extract": {
"delete_module_archive_after_success": true
}
}
`)
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)
}
modFile, err := os.Create(p.ModuleArchivePath())
if err != nil {
t.Fatalf("create mod: %v", err)
}
if err := erf.Write(modFile, erf.New("MOD ", []erf.Resource{
{Name: "broken", Type: 0xFFFF, Data: []byte("bad")},
})); err != nil {
t.Fatalf("write mod: %v", err)
}
if err := modFile.Close(); err != nil {
t.Fatalf("close mod: %v", err)
}
result, err := Extract(p)
if err == nil {
t.Fatal("expected extract to fail")
}
if len(result.DeletedArchivePaths) != 0 {
t.Fatalf("expected no deleted archive paths, got %#v", result.DeletedArchivePaths)
}
if _, statErr := os.Stat(p.ModuleArchivePath()); statErr != nil {
t.Fatalf("expected consumed module archive to remain after failed extract, stat err=%v", statErr)
}
}
func TestBuildSplitsConfiguredHAKs(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src"))