Update BuildAndPackage

This commit is contained in:
2026-04-09 17:23:00 +02:00
parent 4f8bfc133a
commit 68aea4ba00
5 changed files with 250 additions and 6 deletions
+121
View File
@@ -1,12 +1,15 @@
package topdata
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"time"
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
@@ -19,6 +22,17 @@ const (
)
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 {
progress = func(string) {}
}
@@ -29,6 +43,13 @@ func BuildPackage(p *project.Project, progress func(string)) (PackageResult, err
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) {
if progress == nil {
progress = func(string) {}
@@ -207,3 +228,103 @@ func copyFile(dst, src string) error {
}
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
}
+50 -3
View File
@@ -6,6 +6,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
"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)
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
mkdirAll(t, filepath.Join(root, "topdata", "assets", "gui"))
@@ -8077,9 +8078,9 @@ func TestBuildPackageIncludesCompiled2DAAndTopAssets(t *testing.T) {
proj := testProject(root)
proj.Config.TopData.ReferenceBuilder = ""
result, err := BuildPackage(proj, nil)
result, err := BuildAndPackage(proj, nil)
if err != nil {
t.Fatalf("BuildPackage failed: %v", err)
t.Fatalf("BuildAndPackage failed: %v", err)
}
if result.OutputHAKPath != filepath.Join(root, "build", PackageHAKFileName) {
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) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))