Update BuildAndPackage
This commit is contained in:
+1
-1
@@ -397,7 +397,7 @@ func runBuildTopPackage(ctx context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := topdata.BuildPackage(p, func(message string) {
|
result, err := topdata.BuildAndPackage(p, func(message string) {
|
||||||
fmt.Fprintf(ctx.stdout, "[build-top-package] %s\n", message)
|
fmt.Fprintf(ctx.stdout, "[build-top-package] %s\n", message)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -94,8 +94,16 @@ func buildModule(p *project.Project, progress ProgressFunc) (BuildResult, error)
|
|||||||
var topPackage topdata.PackageResult
|
var topPackage topdata.PackageResult
|
||||||
var err error
|
var err error
|
||||||
if p.HasTopData() {
|
if p.HasTopData() {
|
||||||
progressf(progress, "Building native top package...")
|
progressf(progress, "Building native topdata...")
|
||||||
topPackage, err = topdata.BuildPackage(p, func(message string) {
|
nativeTopData, buildErr := topdata.BuildNative(p, func(message string) {
|
||||||
|
progressf(progress, "Top package: "+message)
|
||||||
|
})
|
||||||
|
if buildErr != nil {
|
||||||
|
return BuildResult{}, buildErr
|
||||||
|
}
|
||||||
|
|
||||||
|
progressf(progress, "Packaging native top package...")
|
||||||
|
topPackage, err = topdata.BuildPackageFromBuild(p, nativeTopData, func(message string) {
|
||||||
progressf(progress, "Top package: "+message)
|
progressf(progress, "Top package: "+message)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -546,6 +546,74 @@ func TestBuildModuleBuildsTopPackageAndInjectsHAKList(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildModuleCompilesTopDataOnlyOnce(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"))
|
||||||
|
mustMkdir(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
||||||
|
|
||||||
|
mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{
|
||||||
|
"module": {
|
||||||
|
"name": "Test Module",
|
||||||
|
"resref": "testmod",
|
||||||
|
"hak_order": ["sow_top"]
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"source": "src",
|
||||||
|
"assets": "assets",
|
||||||
|
"build": "build"
|
||||||
|
},
|
||||||
|
"topdata": {
|
||||||
|
"source": "topdata",
|
||||||
|
"build": "build/topdata"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
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"},
|
||||||
|
{"label": "Mod_CustomTlk", "type": "CExoString", "value": "sow_tlk"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
mustWriteFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||||
|
mustWriteFile(t, filepath.Join(root, "topdata", "data", "repadjust", "base.json"), `{
|
||||||
|
"output": "repadjust.2da",
|
||||||
|
"columns": ["Label"],
|
||||||
|
"rows": [{"id": 0, "Label": "TEST_LABEL"}]
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress []string
|
||||||
|
if _, err := BuildModuleWithProgress(p, func(message string) {
|
||||||
|
progress = append(progress, message)
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("build module: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
joined := strings.Join(progress, "\n")
|
||||||
|
if got := strings.Count(joined, "Building native topdata datasets..."); got != 1 {
|
||||||
|
t.Fatalf("expected exactly one native topdata compile pass, got %d\nprogress:\n%s", got, joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCompareFailsWhenTopPackageIsStale(t *testing.T) {
|
func TestCompareFailsWhenTopPackageIsStale(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
mustMkdir(t, filepath.Join(root, "src", "module"))
|
mustMkdir(t, filepath.Join(root, "src", "module"))
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package topdata
|
package topdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
|
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
|
||||||
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
||||||
@@ -19,6 +22,17 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func BuildPackage(p *project.Project, progress func(string)) (PackageResult, error) {
|
func BuildPackage(p *project.Project, progress func(string)) (PackageResult, error) {
|
||||||
|
if progress == nil {
|
||||||
|
progress = func(string) {}
|
||||||
|
}
|
||||||
|
nativeResult, err := packagedBuildResult(p)
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, err
|
||||||
|
}
|
||||||
|
return packageBuiltTopData(p, nativeResult, progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildAndPackage(p *project.Project, progress func(string)) (PackageResult, error) {
|
||||||
if progress == nil {
|
if progress == nil {
|
||||||
progress = func(string) {}
|
progress = func(string) {}
|
||||||
}
|
}
|
||||||
@@ -29,6 +43,13 @@ func BuildPackage(p *project.Project, progress func(string)) (PackageResult, err
|
|||||||
return packageBuiltTopData(p, nativeResult, progress)
|
return packageBuiltTopData(p, nativeResult, progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BuildPackageFromBuild(p *project.Project, nativeResult BuildResult, progress func(string)) (PackageResult, error) {
|
||||||
|
if progress == nil {
|
||||||
|
progress = func(string) {}
|
||||||
|
}
|
||||||
|
return packageBuiltTopData(p, nativeResult, progress)
|
||||||
|
}
|
||||||
|
|
||||||
func packageBuiltTopData(p *project.Project, nativeResult BuildResult, progress func(string)) (PackageResult, error) {
|
func packageBuiltTopData(p *project.Project, nativeResult BuildResult, progress func(string)) (PackageResult, error) {
|
||||||
if progress == nil {
|
if progress == nil {
|
||||||
progress = func(string) {}
|
progress = func(string) {}
|
||||||
@@ -207,3 +228,103 @@ func copyFile(dst, src string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func packagedBuildResult(p *project.Project) (BuildResult, error) {
|
||||||
|
if !p.HasTopData() {
|
||||||
|
return BuildResult{}, errors.New("topdata is not configured for this project")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDir := p.TopDataBuildDir()
|
||||||
|
output2DA := filepath.Join(buildDir, "2da")
|
||||||
|
outputTLK := filepath.Join(buildDir, "tlk")
|
||||||
|
if _, err := os.Stat(output2DA); err != nil {
|
||||||
|
return BuildResult{}, fmt.Errorf("topdata build output missing: run build-topdata first")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(outputTLK); err != nil {
|
||||||
|
return BuildResult{}, fmt.Errorf("topdata tlk output missing: run build-topdata first")
|
||||||
|
}
|
||||||
|
|
||||||
|
files2DA, oldest2DA, err := countCompiledFiles(output2DA, ".2da")
|
||||||
|
if err != nil {
|
||||||
|
return BuildResult{}, err
|
||||||
|
}
|
||||||
|
filesTLK, oldestTLK, err := countCompiledFiles(outputTLK, ".tlk")
|
||||||
|
if err != nil {
|
||||||
|
return BuildResult{}, err
|
||||||
|
}
|
||||||
|
if files2DA == 0 {
|
||||||
|
return BuildResult{}, fmt.Errorf("topdata build output missing: run build-topdata first")
|
||||||
|
}
|
||||||
|
if filesTLK != 1 {
|
||||||
|
return BuildResult{}, fmt.Errorf("expected exactly 1 compiled tlk output, found %d", filesTLK)
|
||||||
|
}
|
||||||
|
|
||||||
|
newestSource, newestPath, err := newestTopDataSource(p.TopDataSourceDir())
|
||||||
|
if err != nil {
|
||||||
|
return BuildResult{}, err
|
||||||
|
}
|
||||||
|
oldestBuild := oldest2DA
|
||||||
|
if oldestBuild.IsZero() || (!oldestTLK.IsZero() && oldestTLK.Before(oldestBuild)) {
|
||||||
|
oldestBuild = oldestTLK
|
||||||
|
}
|
||||||
|
if !newestSource.IsZero() && !oldestBuild.IsZero() && newestSource.After(oldestBuild) {
|
||||||
|
return BuildResult{}, fmt.Errorf("topdata build output is older than source file %s; run build-topdata first", newestPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return BuildResult{
|
||||||
|
Mode: "native",
|
||||||
|
Output2DADir: output2DA,
|
||||||
|
OutputTLKDir: outputTLK,
|
||||||
|
Files2DA: files2DA,
|
||||||
|
FilesTLK: filesTLK,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func countCompiledFiles(dir, extension string) (int, time.Time, error) {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return 0, time.Time{}, fmt.Errorf("read compiled output dir %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
oldest := time.Time{}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), extension) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return 0, time.Time{}, fmt.Errorf("stat compiled output %s: %w", filepath.Join(dir, entry.Name()), err)
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
if oldest.IsZero() || info.ModTime().Before(oldest) {
|
||||||
|
oldest = info.ModTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count, oldest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newestTopDataSource(sourceDir string) (time.Time, string, error) {
|
||||||
|
newest := time.Time{}
|
||||||
|
newestPath := ""
|
||||||
|
err := filepath.WalkDir(sourceDir, func(path string, d fs.DirEntry, walkErr error) error {
|
||||||
|
if walkErr != nil {
|
||||||
|
return walkErr
|
||||||
|
}
|
||||||
|
if d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
info, err := d.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if newest.IsZero() || info.ModTime().After(newest) {
|
||||||
|
newest = info.ModTime()
|
||||||
|
newestPath = path
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, "", fmt.Errorf("scan topdata sources: %w", err)
|
||||||
|
}
|
||||||
|
return newest, newestPath, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
|
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
|
||||||
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
||||||
@@ -8062,7 +8063,7 @@ func TestBuildNativeWithSiblingAssetsRepo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildPackageIncludesCompiled2DAAndTopAssets(t *testing.T) {
|
func TestBuildAndPackageIncludesCompiled2DAAndTopAssets(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "assets", "gui"))
|
mkdirAll(t, filepath.Join(root, "topdata", "assets", "gui"))
|
||||||
@@ -8077,9 +8078,9 @@ func TestBuildPackageIncludesCompiled2DAAndTopAssets(t *testing.T) {
|
|||||||
proj := testProject(root)
|
proj := testProject(root)
|
||||||
proj.Config.TopData.ReferenceBuilder = ""
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
|
||||||
result, err := BuildPackage(proj, nil)
|
result, err := BuildAndPackage(proj, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("BuildPackage failed: %v", err)
|
t.Fatalf("BuildAndPackage failed: %v", err)
|
||||||
}
|
}
|
||||||
if result.OutputHAKPath != filepath.Join(root, "build", PackageHAKFileName) {
|
if result.OutputHAKPath != filepath.Join(root, "build", PackageHAKFileName) {
|
||||||
t.Fatalf("unexpected hak path: %s", result.OutputHAKPath)
|
t.Fatalf("unexpected hak path: %s", result.OutputHAKPath)
|
||||||
@@ -8120,6 +8121,52 @@ func TestBuildPackageIncludesCompiled2DAAndTopAssets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildPackageRequiresExistingCompiledOutput(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "repadjust", "base.json"), `{
|
||||||
|
"output": "repadjust.2da",
|
||||||
|
"columns": ["Label"],
|
||||||
|
"rows": [{"id": 0, "Label": "TEST_LABEL"}]
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
proj := testProject(root)
|
||||||
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
|
||||||
|
if _, err := BuildPackage(proj, nil); err == nil || !strings.Contains(err.Error(), "run build-topdata first") {
|
||||||
|
t.Fatalf("expected missing compiled output error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildPackageFailsWhenCompiledOutputIsStale(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||||
|
basePath := filepath.Join(root, "topdata", "data", "repadjust", "base.json")
|
||||||
|
writeFile(t, basePath, `{
|
||||||
|
"output": "repadjust.2da",
|
||||||
|
"columns": ["Label"],
|
||||||
|
"rows": [{"id": 0, "Label": "TEST_LABEL"}]
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
proj := testProject(root)
|
||||||
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
|
||||||
|
if _, err := BuildNative(proj, nil); err != nil {
|
||||||
|
t.Fatalf("BuildNative failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newTime := time.Now().Add(2 * time.Second)
|
||||||
|
if err := os.Chtimes(basePath, newTime, newTime); err != nil {
|
||||||
|
t.Fatalf("touch topdata source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := BuildPackage(proj, nil); err == nil || !strings.Contains(err.Error(), "older than source file") {
|
||||||
|
t.Fatalf("expected stale compiled output error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateProjectErrorsOnTopPackageAssetCollision(t *testing.T) {
|
func TestValidateProjectErrorsOnTopPackageAssetCollision(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
||||||
|
|||||||
Reference in New Issue
Block a user