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
+107 -3
View File
@@ -63,10 +63,13 @@ type TopDataConfig struct {
Build string `json:"build"`
ReferenceBuilder string `json:"reference_builder"`
Assets string `json:"assets"`
PackageHAK string `json:"package_hak"`
PackageTLK string `json:"package_tlk"`
}
type ExtractConfig struct {
IgnoreExtensions []string `json:"ignore_extensions"`
IgnoreExtensions []string `json:"ignore_extensions"`
DeleteModuleArchiveAfterSuccess bool `json:"delete_module_archive_after_success,omitempty"`
}
type Inventory struct {
@@ -127,6 +130,17 @@ func (p *Project) ValidateLayout() error {
if len(p.Config.Module.ResRef) > 16 {
failures = append(failures, fmt.Errorf("module.resref %q exceeds 16 characters", p.Config.Module.ResRef))
}
if p.HasTopData() {
if err := validateOutputFileName("topdata.package_hak", p.TopDataPackageHAKName(), ".hak"); err != nil {
failures = append(failures, err)
}
if err := validateOutputFileName("topdata.package_tlk", p.TopDataPackageTLKName(), ".tlk"); err != nil {
failures = append(failures, err)
}
if err := validateOutputFileName("topdata.compiled_tlk", filepath.Base(p.TopDataCompiledTLKPath()), ".tlk"); err != nil {
failures = append(failures, err)
}
}
for index, hak := range p.Config.HAKs {
if strings.TrimSpace(hak.Name) == "" {
@@ -244,6 +258,18 @@ func (p *Project) BuildDir() string {
return filepath.Join(p.Root, p.Config.Paths.Build)
}
func (p *Project) ModuleArchivePath() string {
return filepath.Join(p.BuildDir(), p.Config.Module.ResRef+".mod")
}
func (p *Project) HAKManifestPath() string {
return filepath.Join(p.BuildDir(), "haks.json")
}
func (p *Project) HAKArchivePath(name string) string {
return filepath.Join(p.BuildDir(), strings.TrimSpace(name)+".hak")
}
func (p *Project) CloneWithHAKNames(names []string) (*Project, error) {
if len(names) == 0 {
return p, nil
@@ -341,12 +367,70 @@ func (p *Project) TopDataAssetsDir() string {
return absPath
}
func (p *Project) TopDataUsesRepoCache() bool {
return sameCleanPath(p.TopDataBuildDir(), filepath.Join(p.Root, ".cache"))
}
func (p *Project) TopDataCompiled2DADir() string {
return filepath.Join(p.TopDataBuildDir(), "2da")
}
func (p *Project) TopDataCompiledTLKPath() string {
if p.TopDataUsesRepoCache() {
return filepath.Join(p.BuildDir(), "sow_tlk.tlk")
}
return filepath.Join(p.TopDataBuildDir(), "sow_tlk.tlk")
}
func (p *Project) TopDataCompiledTLKDir() string {
return filepath.Dir(p.TopDataCompiledTLKPath())
}
func (p *Project) TopDataWikiRootDir() string {
if p.TopDataUsesRepoCache() {
return filepath.Join(p.Root, ".cache", "wiki")
}
return filepath.Join(p.TopDataBuildDir(), "wiki")
}
func (p *Project) TopDataWikiPagesDir() string {
return filepath.Join(p.TopDataWikiRootDir(), "pages")
}
func (p *Project) TopDataWikiStatePath() string {
return filepath.Join(p.TopDataWikiRootDir(), "state.json")
}
func (p *Project) TopDataPackageHAKName() string {
name := strings.TrimSpace(p.Config.TopData.PackageHAK)
if name == "" {
name = "sow_top.hak"
}
return name
}
func (p *Project) TopDataPackageTLKName() string {
name := strings.TrimSpace(p.Config.TopData.PackageTLK)
if name == "" {
name = "sow_tlk.tlk"
}
return name
}
func (p *Project) TopDataPackageHAKPath() string {
return filepath.Join(p.BuildDir(), p.TopDataPackageHAKName())
}
func (p *Project) TopDataPackageTLKPath() string {
return filepath.Join(p.BuildDir(), p.TopDataPackageTLKName())
}
func (i Inventory) Report() InventoryReport {
return InventoryReport{
SourceFiles: len(i.SourceFiles),
ScriptFiles: len(i.ScriptFiles),
AssetFiles: len(i.AssetFiles),
Extensions: i.Extensions,
Extensions: i.Extensions,
}
}
@@ -356,7 +440,9 @@ func defaultConfig() Config {
Build: "build",
},
TopData: TopDataConfig{
Build: ".cache",
Build: ".cache",
PackageHAK: "sow_top.hak",
PackageTLK: "sow_tlk.tlk",
},
}
}
@@ -406,3 +492,21 @@ func scanDir(root string, include func(path string) bool) ([]string, []string, e
slices.Sort(exts)
return files, exts, nil
}
func validateOutputFileName(field, name, expectedExt string) error {
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return fmt.Errorf("%s is required", field)
}
if filepath.Base(trimmed) != trimmed {
return fmt.Errorf("%s must be a file name, not a path: %q", field, name)
}
if !strings.EqualFold(filepath.Ext(trimmed), expectedExt) {
return fmt.Errorf("%s must use %s extension: %q", field, expectedExt, name)
}
return nil
}
func sameCleanPath(a, b string) bool {
return filepath.Clean(a) == filepath.Clean(b)
}
+92
View File
@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"slices"
"strings"
"testing"
)
@@ -83,6 +84,67 @@ func TestTopDataAssetsDirReturnsEmptyForNoConfig(t *testing.T) {
}
}
func TestModuleArchivePathUsesConfiguredBuildDirAndResRef(t *testing.T) {
root := t.TempDir()
mkdirAll(t, filepath.Join(root, "output"))
proj := &Project{
Root: root,
Config: Config{
Module: ModuleConfig{Name: "Test", ResRef: "testmod"},
Paths: PathConfig{Build: "output"},
},
}
got := proj.ModuleArchivePath()
want := filepath.Join(root, "output", "testmod.mod")
if got != want {
t.Fatalf("expected %s, got %s", want, got)
}
}
func TestHAKPathsUseConfiguredBuildDir(t *testing.T) {
root := t.TempDir()
mkdirAll(t, filepath.Join(root, "output"))
proj := &Project{
Root: root,
Config: Config{
Paths: PathConfig{Build: "output"},
},
}
if got, want := proj.HAKManifestPath(), filepath.Join(root, "output", "haks.json"); got != want {
t.Fatalf("expected manifest path %s, got %s", want, got)
}
if got, want := proj.HAKArchivePath("core_01"), filepath.Join(root, "output", "core_01.hak"); got != want {
t.Fatalf("expected hak path %s, got %s", want, got)
}
}
func TestTopDataPackagePathsUseConfiguredNamesAndBuildDir(t *testing.T) {
root := t.TempDir()
mkdirAll(t, filepath.Join(root, "output"))
proj := &Project{
Root: root,
Config: Config{
Paths: PathConfig{Build: "output"},
TopData: TopDataConfig{
PackageHAK: "custom_top.hak",
PackageTLK: "custom_dialog.tlk",
},
},
}
if got, want := proj.TopDataPackageHAKPath(), filepath.Join(root, "output", "custom_top.hak"); got != want {
t.Fatalf("expected top hak path %s, got %s", want, got)
}
if got, want := proj.TopDataPackageTLKPath(), filepath.Join(root, "output", "custom_dialog.tlk"); got != want {
t.Fatalf("expected top tlk path %s, got %s", want, got)
}
}
func TestValidateLayoutAllowsMissingAssetsDir(t *testing.T) {
root := t.TempDir()
mkdirAll(t, filepath.Join(root, "src"))
@@ -104,6 +166,36 @@ func TestValidateLayoutAllowsMissingAssetsDir(t *testing.T) {
}
}
func TestValidateLayoutRejectsInvalidTopDataPackageOutputNames(t *testing.T) {
root := t.TempDir()
mkdirAll(t, filepath.Join(root, "src"))
mkdirAll(t, filepath.Join(root, "build"))
proj := &Project{
Root: root,
Config: Config{
Module: ModuleConfig{Name: "Test", ResRef: "test"},
Paths: PathConfig{Source: "src", Build: "build"},
TopData: TopDataConfig{
Source: "topdata",
PackageHAK: "nested/custom_top.hak",
PackageTLK: "custom_dialog.txt",
},
},
}
err := proj.ValidateLayout()
if err == nil {
t.Fatal("expected topdata output validation error")
}
if !strings.Contains(err.Error(), "topdata.package_hak") {
t.Fatalf("expected package_hak validation error, got %v", err)
}
if !strings.Contains(err.Error(), "topdata.package_tlk") {
t.Fatalf("expected package_tlk validation error, got %v", err)
}
}
func TestScanAllowsMissingAssetsDir(t *testing.T) {
root := t.TempDir()
mkdirAll(t, filepath.Join(root, "src"))