Conversion to full YAML

This commit is contained in:
2026-05-13 19:22:07 +02:00
parent 7d286551b3
commit 03e2320788
6 changed files with 475 additions and 166 deletions
+2 -2
View File
@@ -257,7 +257,7 @@ Rationale:
- Generated and machine-readable JSON is widespread and should remain JSON: - Generated and machine-readable JSON is widespread and should remain JSON:
- build HAK manifest: `build/haks.json` - build HAK manifest: `build/haks.json`
- topdata authored/generated datasets - topdata authored/generated datasets
- topdata template conversion metadata: `topdata/templates/config.json` - topdata template conversion metadata: `topdata/templates/config.yaml`
- topdata wiki state and deploy manifests - topdata wiki state and deploy manifests
- autogen manifest caches under `.cache` - autogen manifest caches under `.cache`
- credits inventory output - credits inventory output
@@ -367,7 +367,7 @@ Keep in code if toolkit-intrinsic:
- Convert consumer root configs from `nwn-tool.json` to `nwn-tool.yaml`. - Convert consumer root configs from `nwn-tool.json` to `nwn-tool.yaml`.
- Preserve existing values exactly unless a separate behavior change is documented. - Preserve existing values exactly unless a separate behavior change is documented.
- Keep `topdata/templates/config.json` unchanged for now because it is topdata conversion metadata, not root repository configuration. - Treat `topdata/templates/config.yaml` as authored converter configuration, distinct from JSON data artifacts and root repository config.
- Keep generated datasets, manifests, caches, credits inventory, and GFF JSON unchanged. - Keep generated datasets, manifests, caches, credits inventory, and GFF JSON unchanged.
- Add before/after examples for root config migration. - Add before/after examples for root config migration.
+3 -2
View File
@@ -296,8 +296,9 @@ westgate` to limit conversion to a configured dataset.
Generated and machine-readable artifacts remain JSON, including HAK manifests, Generated and machine-readable artifacts remain JSON, including HAK manifests,
topdata datasets, caches, credits inventories, build reports, and canonical GFF topdata datasets, caches, credits inventories, build reports, and canonical GFF
documents. Nested metadata such as `topdata/templates/config.json` is not root documents. Nested metadata such as `topdata/templates/config.yaml` remains
repository configuration and is intentionally unchanged. valid when it is local workflow configuration rather than root repository
configuration.
## Wiki And Changelog Utilities ## Wiki And Changelog Utilities
+237 -76
View File
@@ -296,27 +296,18 @@ func runBuild(ctx context) error {
if err != nil { if err != nil {
return err return err
} }
if err := runBuildModulePreflight(ctx, p, nil); err != nil { preflight, err := runBuildModulePreflight(ctx, p, nil)
if err != nil {
return err return err
} }
console := newProjectConsole(ctx, p, "build")
result, err := pipeline.Build(p) result, err := pipeline.Build(p)
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) console.emitBuildResult(result, preflight)
fmt.Fprintf(ctx.stdout, "module: %s\n", result.ModulePath)
fmt.Fprintf(ctx.stdout, "resources: %d\n", result.Resources)
if result.TopPackageHAK != "" {
fmt.Fprintf(ctx.stdout, "top package hak: %s\n", result.TopPackageHAK)
fmt.Fprintf(ctx.stdout, "top package tlk: %s\n", result.TopPackageTLK)
}
if len(result.HAKPaths) > 0 {
fmt.Fprintf(ctx.stdout, "hak archives: %d\n", len(result.HAKPaths))
fmt.Fprintf(ctx.stdout, "hak assets: %d\n", result.HAKAssets)
fmt.Fprintf(ctx.stdout, "hak manifest: %s\n", result.Manifest)
}
return nil return nil
} }
@@ -325,15 +316,16 @@ func runBuildModule(ctx context) error {
if err != nil { if err != nil {
return err return err
} }
console := newProjectConsole(ctx, p, "build-module")
spin.configure(ctx.stderr, console.spinnerEnabled)
spin.start("Build Module: starting")
defer spin.stop()
progress := func(message string) { progress := func(message string) {
if ctx.logLevel == logLevelQuiet { console.progress(message)
return
} }
spin.linebreak() preflight, err := runBuildModulePreflight(ctx, p, progress)
fmt.Fprintf(ctx.stdout, "[build-module] %s\n", message) if err != nil {
}
if err := runBuildModulePreflight(ctx, p, progress); err != nil {
return err return err
} }
@@ -344,26 +336,36 @@ func runBuildModule(ctx context) error {
return err return err
} }
fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) console.emitBuildModuleResult(result, preflight)
fmt.Fprintf(ctx.stdout, "module: %s\n", result.ModulePath)
fmt.Fprintf(ctx.stdout, "resources: %d\n", result.Resources)
return nil return nil
} }
func runBuildModulePreflight(ctx context, p *project.Project, progress func(string)) error { type buildModulePreflightResult struct {
ManifestStatus string
ManifestPath string
ScriptCompilationStatus string
}
func runBuildModulePreflight(ctx context, p *project.Project, progress func(string)) (buildModulePreflightResult, error) {
result := buildModulePreflightResult{}
if progress == nil { if progress == nil {
progress = func(string) {} progress = func(string) {}
} }
if err := refreshBuildModuleManifest(ctx, p, progress); err != nil { manifestStatus, manifestPath, err := refreshBuildModuleManifest(ctx, p, progress)
return err if err != nil {
return result, err
} }
if err := prepareBuildModuleCompiler(ctx, p, progress); err != nil { result.ManifestStatus = manifestStatus
return err result.ManifestPath = manifestPath
scriptStatus, err := prepareBuildModuleCompiler(ctx, p, progress)
if err != nil {
return result, err
} }
return nil result.ScriptCompilationStatus = scriptStatus
return result, nil
} }
func refreshBuildModuleManifest(ctx context, p *project.Project, progress func(string)) error { func refreshBuildModuleManifest(ctx context, p *project.Project, progress func(string)) (string, string, error) {
manifestPath := p.HAKManifestPath() manifestPath := p.HAKManifestPath()
if override := strings.TrimSpace(os.Getenv("SOW_HAK_MANIFEST_PATH")); override != "" { if override := strings.TrimSpace(os.Getenv("SOW_HAK_MANIFEST_PATH")); override != "" {
manifestPath = override manifestPath = override
@@ -374,36 +376,36 @@ func refreshBuildModuleManifest(ctx context, p *project.Project, progress func(s
if envEnabled("SOW_MODULE_SKIP_HAK_MANIFEST_REFRESH") { if envEnabled("SOW_MODULE_SKIP_HAK_MANIFEST_REFRESH") {
if _, err := os.Stat(manifestPath); err != nil { if _, err := os.Stat(manifestPath); err != nil {
return fmt.Errorf("SOW_MODULE_SKIP_HAK_MANIFEST_REFRESH is set, but %s does not exist", manifestPath) return "", "", fmt.Errorf("SOW_MODULE_SKIP_HAK_MANIFEST_REFRESH is set, but %s does not exist", manifestPath)
} }
progress(fmt.Sprintf("Using existing hak manifest at %s; skipping published sow-assets refresh.", relPathFromRoot(p.Root, manifestPath))) progress(fmt.Sprintf("Using existing hak manifest at %s; skipping published sow-assets refresh.", relPathFromRoot(p.Root, manifestPath)))
return nil return "reused", manifestPath, nil
} }
progress("Refreshing hak list from the latest published sow-assets manifest...") progress("Refreshing hak list from the latest published sow-assets manifest...")
if err := runProjectScript(ctx, p, []string{"scripts", "fetch-hak-manifest"}, manifestPath); err != nil { if err := runProjectScript(ctx, p, []string{"scripts", "fetch-hak-manifest"}, manifestPath); err != nil {
return err return "", "", err
} }
if _, err := pipeline.ApplyHAKManifest(p, manifestPath); err != nil { if _, err := pipeline.ApplyHAKManifest(p, manifestPath); err != nil {
return err return "", "", err
} }
return nil return "refreshed", manifestPath, nil
} }
func prepareBuildModuleCompiler(ctx context, p *project.Project, progress func(string)) error { func prepareBuildModuleCompiler(ctx context, p *project.Project, progress func(string)) (string, error) {
if envEnabled("SOW_MODULE_SKIP_NSS_COMPILATION") { if envEnabled("SOW_MODULE_SKIP_NSS_COMPILATION") {
progress("SOW_MODULE_SKIP_NSS_COMPILATION is set; skipping NWScript compiler resolution and NSS compilation for this run.") progress("SOW_MODULE_SKIP_NSS_COMPILATION is set; skipping NWScript compiler resolution and NSS compilation for this run.")
return nil return "skipped", nil
} }
if !projectHasScriptSources(p) { if !projectHasScriptSources(p) {
return nil return "not-needed", nil
} }
compilerPathEnv := p.ScriptCompilerPathEnv() compilerPathEnv := p.ScriptCompilerPathEnv()
explicitCompiler := strings.TrimSpace(os.Getenv(compilerPathEnv)) explicitCompiler := strings.TrimSpace(os.Getenv(compilerPathEnv))
if explicitCompiler != "" { if explicitCompiler != "" {
if scriptCompilerRunnable(explicitCompiler) { if scriptCompilerRunnable(explicitCompiler) {
return nil return "enabled", nil
} }
if fileExists(explicitCompiler) { if fileExists(explicitCompiler) {
return disableBuildModuleCompiler(compilerPathEnv, progress, fmt.Sprintf("Configured NWScript compiler is present but cannot start: %s", explicitCompiler)) return disableBuildModuleCompiler(compilerPathEnv, progress, fmt.Sprintf("Configured NWScript compiler is present but cannot start: %s", explicitCompiler))
@@ -411,7 +413,7 @@ func prepareBuildModuleCompiler(ctx context, p *project.Project, progress func(s
} }
if _, ok := findUsableBuildModuleCompiler(p); ok { if _, ok := findUsableBuildModuleCompiler(p); ok {
return nil return "enabled", nil
} }
installScript := projectScriptPath(p.Root, "scripts", "install-script-compiler") installScript := projectScriptPath(p.Root, "scripts", "install-script-compiler")
@@ -424,21 +426,21 @@ func prepareBuildModuleCompiler(ctx context, p *project.Project, progress func(s
return disableBuildModuleCompiler(compilerPathEnv, progress, "Automatic NWScript compiler install failed.") return disableBuildModuleCompiler(compilerPathEnv, progress, "Automatic NWScript compiler install failed.")
} }
if _, ok := findUsableBuildModuleCompiler(p); ok { if _, ok := findUsableBuildModuleCompiler(p); ok {
return nil return "installed", nil
} }
return disableBuildModuleCompiler(compilerPathEnv, progress, "Installed NWScript compiler but it still cannot start.") return disableBuildModuleCompiler(compilerPathEnv, progress, "Installed NWScript compiler but it still cannot start.")
} }
func disableBuildModuleCompiler(compilerPathEnv string, progress func(string), reason string) error { func disableBuildModuleCompiler(compilerPathEnv string, progress func(string), reason string) (string, error) {
if err := os.Setenv("SOW_MODULE_SKIP_NSS_COMPILATION", "1"); err != nil { if err := os.Setenv("SOW_MODULE_SKIP_NSS_COMPILATION", "1"); err != nil {
return fmt.Errorf("set SOW_MODULE_SKIP_NSS_COMPILATION: %w", err) return "", fmt.Errorf("set SOW_MODULE_SKIP_NSS_COMPILATION: %w", err)
} }
progress("Skipping NWScript compilation for this build. " + reason) progress("Skipping NWScript compilation for this build. " + reason)
if compilerPathEnv != "" { if compilerPathEnv != "" {
_ = os.Unsetenv(compilerPathEnv) _ = os.Unsetenv(compilerPathEnv)
} }
return nil return "skipped", nil
} }
func projectHasScriptSources(p *project.Project) bool { func projectHasScriptSources(p *project.Project) bool {
@@ -585,6 +587,190 @@ const (
logLevelDebug logLevelDebug
) )
type projectConsole struct {
stdout io.Writer
projectRoot string
projectName string
commandName string
commandLabel string
level logLevel
spinnerEnabled bool
}
func newProjectConsole(ctx context, p *project.Project, commandName string) *projectConsole {
return &projectConsole{
stdout: ctx.stdout,
projectRoot: p.Root,
projectName: p.Config.Module.Name,
commandName: commandName,
commandLabel: projectCommandLabel(commandName),
level: ctx.logLevel,
spinnerEnabled: isInteractiveTTY(ctx.stderr) && strings.TrimSpace(os.Getenv("CI")) == "" && ctx.logLevel == logLevelNormal,
}
}
func projectCommandLabel(commandName string) string {
switch commandName {
case "build":
return "Build"
case "build-module":
return "Build Module"
case "extract":
return "Extract"
case "validate":
return "Validate"
case "compare":
return "Compare"
case "apply-hak-manifest":
return "Apply HAK Manifest"
default:
return commandName
}
}
func (c *projectConsole) progress(message string) {
if phase := c.phaseLabel(message); phase != "" {
spin.update(c.commandLabel + ": " + phase)
}
if c.level != logLevelDebug {
return
}
spin.linebreak()
fmt.Fprintf(c.stdout, "[debug] %s\n", message)
}
func (c *projectConsole) phaseLabel(message string) string {
switch {
case strings.HasPrefix(message, "Refreshing hak list from the latest published sow-assets manifest"):
return "refreshing HAK manifest"
case strings.HasPrefix(message, "Using existing hak manifest at "):
return "reusing HAK manifest"
case strings.HasPrefix(message, "NWScript compiler not found locally. Installing it now"):
return "installing script compiler"
case strings.HasPrefix(message, "Skipping NWScript compilation for this build."):
return "skipping script compilation"
case strings.HasPrefix(message, "Validating project"):
return "validating project"
case strings.HasPrefix(message, "Resolving module HAK order"):
return "resolving HAK order"
case strings.HasPrefix(message, "Collecting module resources"):
return "collecting module resources"
case strings.HasPrefix(message, "Writing module archive"):
return "writing module archive"
default:
return ""
}
}
func (c *projectConsole) emitBuildResult(result pipeline.BuildResult, preflight buildModulePreflightResult) {
spin.linebreak()
fmt.Fprintln(c.stdout, "Build ----------")
fmt.Fprintf(c.stdout, "project: %s\n", c.projectName)
fmt.Fprintf(c.stdout, "module: %s\n", c.relPath(result.ModulePath))
fmt.Fprintf(c.stdout, "resources: %d\n", result.Resources)
if preflight.ManifestStatus != "" {
fmt.Fprintf(c.stdout, "hak manifest: %s", preflight.ManifestStatus)
if preflight.ManifestPath != "" {
fmt.Fprintf(c.stdout, " (%s)", c.relPath(preflight.ManifestPath))
}
fmt.Fprintln(c.stdout)
}
if preflight.ScriptCompilationStatus != "" && preflight.ScriptCompilationStatus != "not-needed" {
fmt.Fprintf(c.stdout, "script compilation: %s\n", preflight.ScriptCompilationStatus)
}
if result.TopPackageHAK != "" {
fmt.Fprintf(c.stdout, "top package hak: %s\n", c.relPath(result.TopPackageHAK))
fmt.Fprintf(c.stdout, "top package tlk: %s\n", c.relPath(result.TopPackageTLK))
}
if len(result.HAKPaths) > 0 {
fmt.Fprintf(c.stdout, "hak archives: %d\n", len(result.HAKPaths))
fmt.Fprintf(c.stdout, "hak assets: %d\n", result.HAKAssets)
fmt.Fprintf(c.stdout, "hak manifest: %s\n", c.relPath(result.Manifest))
}
}
func (c *projectConsole) emitBuildModuleResult(result pipeline.BuildResult, preflight buildModulePreflightResult) {
spin.linebreak()
fmt.Fprintln(c.stdout, "Build Module ----------")
fmt.Fprintf(c.stdout, "project: %s\n", c.projectName)
fmt.Fprintf(c.stdout, "module: %s\n", c.relPath(result.ModulePath))
fmt.Fprintf(c.stdout, "resources: %d\n", result.Resources)
if preflight.ManifestStatus != "" {
fmt.Fprintf(c.stdout, "hak manifest: %s", preflight.ManifestStatus)
if preflight.ManifestPath != "" {
fmt.Fprintf(c.stdout, " (%s)", c.relPath(preflight.ManifestPath))
}
fmt.Fprintln(c.stdout)
}
if preflight.ScriptCompilationStatus != "" && preflight.ScriptCompilationStatus != "not-needed" {
fmt.Fprintf(c.stdout, "script compilation: %s\n", preflight.ScriptCompilationStatus)
}
}
func (c *projectConsole) emitExtractResult(result pipeline.ExtractResult) {
spin.linebreak()
fmt.Fprintln(c.stdout, "Extract ----------")
fmt.Fprintf(c.stdout, "project: %s\n", c.projectName)
fmt.Fprintf(c.stdout, "module: %s\n", c.relPath(result.ModulePath))
if len(result.HAKPaths) > 0 {
fmt.Fprintf(c.stdout, "hak archives: %d\n", len(result.HAKPaths))
}
if len(result.DeletedArchivePaths) > 0 {
fmt.Fprintf(c.stdout, "deleted archives: %d\n", len(result.DeletedArchivePaths))
}
fmt.Fprintf(c.stdout, "written: %d\n", result.Written)
fmt.Fprintf(c.stdout, "overwritten: %d\n", result.Overwritten)
fmt.Fprintf(c.stdout, "removed: %d\n", result.Removed)
fmt.Fprintf(c.stdout, "skipped: %d\n", result.Skipped)
}
func (c *projectConsole) emitValidationResult(report validator.Report, root string, inventoryReport project.InventoryReport) {
spin.linebreak()
fmt.Fprintln(c.stdout, "Validate ----------")
fmt.Fprintf(c.stdout, "project: %s\n", c.projectName)
fmt.Fprintf(c.stdout, "root: %s\n", c.relPath(root))
fmt.Fprintf(c.stdout, "source files: %d\n", inventoryReport.SourceFiles)
fmt.Fprintf(c.stdout, "script files: %d\n", inventoryReport.ScriptFiles)
fmt.Fprintf(c.stdout, "asset files: %d\n", inventoryReport.AssetFiles)
fmt.Fprintf(c.stdout, "module file types: %s\n", strings.Join(inventoryReport.Extensions, ", "))
if warnings := report.WarningCount(); warnings > 0 {
fmt.Fprintf(c.stdout, "warnings: %d\n", warnings)
}
fmt.Fprintln(c.stdout, "validation: ok")
}
func (c *projectConsole) emitCompareResult(result pipeline.CompareResult) {
spin.linebreak()
fmt.Fprintln(c.stdout, "Compare ----------")
fmt.Fprintf(c.stdout, "project: %s\n", c.projectName)
fmt.Fprintf(c.stdout, "module: %s\n", c.relPath(result.ModulePath))
if len(result.HAKPaths) > 0 {
fmt.Fprintf(c.stdout, "hak archives: %d\n", len(result.HAKPaths))
}
fmt.Fprintf(c.stdout, "checked resources: %d\n", result.Checked)
fmt.Fprintln(c.stdout, "compare: ok")
}
func (c *projectConsole) emitApplyManifestResult(result pipeline.ApplyManifestResult) {
spin.linebreak()
fmt.Fprintln(c.stdout, "Apply HAK Manifest ----------")
fmt.Fprintf(c.stdout, "project: %s\n", c.projectName)
fmt.Fprintf(c.stdout, "manifest: %s\n", c.relPath(result.ManifestPath))
fmt.Fprintf(c.stdout, "module source: %s\n", c.relPath(result.ModuleSource))
fmt.Fprintf(c.stdout, "hak entries: %d\n", result.HAKCount)
}
func (c *projectConsole) relPath(path string) string {
if path == "" {
return path
}
rel, err := filepath.Rel(c.projectRoot, path)
if err != nil {
return filepath.ToSlash(path)
}
return filepath.ToSlash(rel)
}
type buildHAKConsole struct { type buildHAKConsole struct {
stdout io.Writer stdout io.Writer
projectRoot string projectRoot string
@@ -1230,6 +1416,7 @@ func runExtract(ctx context) error {
if err != nil { if err != nil {
return err return err
} }
console := newProjectConsole(ctx, p, "extract")
files := ctx.args[1:] files := ctx.args[1:]
result, err := pipeline.Extract(p, files...) result, err := pipeline.Extract(p, files...)
@@ -1237,18 +1424,7 @@ func runExtract(ctx context) error {
return err return err
} }
fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) console.emitExtractResult(result)
fmt.Fprintf(ctx.stdout, "module: %s\n", result.ModulePath)
if len(result.HAKPaths) > 0 {
fmt.Fprintf(ctx.stdout, "hak archives: %d\n", len(result.HAKPaths))
}
if len(result.DeletedArchivePaths) > 0 {
fmt.Fprintf(ctx.stdout, "deleted archives: %d\n", len(result.DeletedArchivePaths))
}
fmt.Fprintf(ctx.stdout, "written: %d\n", result.Written)
fmt.Fprintf(ctx.stdout, "overwritten: %d\n", result.Overwritten)
fmt.Fprintf(ctx.stdout, "removed: %d\n", result.Removed)
fmt.Fprintf(ctx.stdout, "skipped: %d\n", result.Skipped)
return nil return nil
} }
@@ -1257,6 +1433,7 @@ func runValidate(ctx context) error {
if err != nil { if err != nil {
return err return err
} }
console := newProjectConsole(ctx, p, "validate")
validationReport := validator.ValidateProject(p) validationReport := validator.ValidateProject(p)
emitValidatorReport(ctx.stderr, validationReport) emitValidatorReport(ctx.stderr, validationReport)
@@ -1265,16 +1442,7 @@ func runValidate(ctx context) error {
} }
inventoryReport := p.Inventory.Report() inventoryReport := p.Inventory.Report()
fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) console.emitValidationResult(validationReport, p.Root, inventoryReport)
fmt.Fprintf(ctx.stdout, "root: %s\n", p.Root)
fmt.Fprintf(ctx.stdout, "source files: %d\n", inventoryReport.SourceFiles)
fmt.Fprintf(ctx.stdout, "script files: %d\n", inventoryReport.ScriptFiles)
fmt.Fprintf(ctx.stdout, "asset files: %d\n", inventoryReport.AssetFiles)
fmt.Fprintf(ctx.stdout, "module file types: %s\n", strings.Join(inventoryReport.Extensions, ", "))
if warnings := validationReport.WarningCount(); warnings > 0 {
fmt.Fprintf(ctx.stdout, "warnings: %d\n", warnings)
}
fmt.Fprintf(ctx.stdout, "validation: ok\n")
return nil return nil
} }
@@ -1480,19 +1648,14 @@ func runCompare(ctx context) error {
if err != nil { if err != nil {
return err return err
} }
console := newProjectConsole(ctx, p, "compare")
result, err := pipeline.Compare(p) result, err := pipeline.Compare(p)
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) console.emitCompareResult(result)
fmt.Fprintf(ctx.stdout, "module: %s\n", result.ModulePath)
if len(result.HAKPaths) > 0 {
fmt.Fprintf(ctx.stdout, "hak archives: %d\n", len(result.HAKPaths))
}
fmt.Fprintf(ctx.stdout, "checked resources: %d\n", result.Checked)
fmt.Fprintf(ctx.stdout, "compare: ok\n")
return nil return nil
} }
@@ -1501,6 +1664,7 @@ func runApplyHAKManifest(ctx context) error {
if err != nil { if err != nil {
return err return err
} }
console := newProjectConsole(ctx, p, "apply-hak-manifest")
manifestPath := p.HAKManifestPath() manifestPath := p.HAKManifestPath()
if len(ctx.args) > 1 { if len(ctx.args) > 1 {
@@ -1515,10 +1679,7 @@ func runApplyHAKManifest(ctx context) error {
return err return err
} }
fmt.Fprintf(ctx.stdout, "project: %s\n", p.Config.Module.Name) console.emitApplyManifestResult(result)
fmt.Fprintf(ctx.stdout, "manifest: %s\n", result.ManifestPath)
fmt.Fprintf(ctx.stdout, "module source: %s\n", result.ModuleSource)
fmt.Fprintf(ctx.stdout, "hak entries: %d\n", result.HAKCount)
return nil return nil
} }
+53 -4
View File
@@ -7,6 +7,8 @@ import (
"strings" "strings"
"testing" "testing"
"time" "time"
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/pipeline"
) )
func TestRunBuildTopPackageUsesCachedCompiledOutputs(t *testing.T) { func TestRunBuildTopPackageUsesCachedCompiledOutputs(t *testing.T) {
@@ -233,6 +235,47 @@ func TestTopdataConsoleDebugProgressAndRelativePaths(t *testing.T) {
} }
} }
func TestProjectConsoleSuppressesBuildModuleProgressInNormalMode(t *testing.T) {
var stdout bytes.Buffer
console := &projectConsole{
stdout: &stdout,
projectRoot: "/workspace/project",
projectName: "Test Module",
commandName: "build-module",
commandLabel: "Build Module",
level: logLevelNormal,
}
console.progress("Writing module archive...")
if stdout.String() != "" {
t.Fatalf("expected no normal-mode progress output, got %q", stdout.String())
}
}
func TestProjectConsoleEmitsRelativePaths(t *testing.T) {
var stdout bytes.Buffer
console := &projectConsole{
stdout: &stdout,
projectRoot: "/workspace/project",
projectName: "Test Module",
commandName: "compare",
commandLabel: "Compare",
level: logLevelNormal,
}
console.emitCompareResult(pipeline.CompareResult{
ModulePath: "/workspace/project/build/test.mod",
HAKPaths: []string{"/workspace/project/build/core.hak"},
Checked: 42,
})
output := stdout.String()
if !strings.Contains(output, "module: build/test.mod") {
t.Fatalf("expected relative module path, got %q", output)
}
}
func TestRunBuildModuleToolkitPreflightRefreshesManifestAndSkipsBrokenCompiler(t *testing.T) { func TestRunBuildModuleToolkitPreflightRefreshesManifestAndSkipsBrokenCompiler(t *testing.T) {
root := t.TempDir() root := t.TempDir()
mkdirAll(t, filepath.Join(root, "src", "module")) mkdirAll(t, filepath.Join(root, "src", "module"))
@@ -309,8 +352,11 @@ scripts:
if _, err := os.Stat(filepath.Join(root, ".cache", "ncs", "use_thing.ncs")); !os.IsNotExist(err) { if _, err := os.Stat(filepath.Join(root, ".cache", "ncs", "use_thing.ncs")); !os.IsNotExist(err) {
t.Fatalf("expected broken explicit compiler to skip compilation, got err=%v", err) t.Fatalf("expected broken explicit compiler to skip compilation, got err=%v", err)
} }
if !strings.Contains(stdout.String(), "Skipping NWScript compilation for this build.") { if !strings.Contains(stdout.String(), "script compilation: skipped") {
t.Fatalf("expected skip-compilation progress in output, got %q", stdout.String()) t.Fatalf("expected skip-compilation summary in output, got %q", stdout.String())
}
if strings.Contains(stdout.String(), "[build-module]") {
t.Fatalf("did not expect raw build-module progress in normal output, got %q", stdout.String())
} }
} }
@@ -385,8 +431,11 @@ scripts:
if _, err := os.Stat(compiledScript); err != nil { if _, err := os.Stat(compiledScript); err != nil {
t.Fatalf("expected installed compiler to compile script: %v", err) t.Fatalf("expected installed compiler to compile script: %v", err)
} }
if !strings.Contains(stdout.String(), "NWScript compiler not found locally. Installing it now...") { if !strings.Contains(stdout.String(), "script compilation: installed") {
t.Fatalf("expected install progress in output, got %q", stdout.String()) t.Fatalf("expected install summary in output, got %q", stdout.String())
}
if strings.Contains(stdout.String(), "[build-module]") {
t.Fatalf("did not expect raw build-module progress in normal output, got %q", stdout.String())
} }
} }
+20 -8
View File
@@ -13,6 +13,7 @@ import (
"strings" "strings"
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project" "gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
"gopkg.in/yaml.v3"
) )
type convertOptions struct { type convertOptions struct {
@@ -105,7 +106,7 @@ func printConvertUsage(stdout io.Writer) {
_, _ = fmt.Fprintln(stdout, " json-to-2da Convert canonical JSON rows into a 2DA file") _, _ = fmt.Fprintln(stdout, " json-to-2da Convert canonical JSON rows into a 2DA file")
_, _ = fmt.Fprintln(stdout, "") _, _ = fmt.Fprintln(stdout, "")
_, _ = fmt.Fprintln(stdout, "2da-to-module notes:") _, _ = fmt.Fprintln(stdout, "2da-to-module notes:")
_, _ = fmt.Fprintln(stdout, " - --namespace is optional when topdata/templates/config.json declares it for the input table") _, _ = fmt.Fprintln(stdout, " - --namespace is optional when topdata/templates/config.yaml declares it for the input table")
_, _ = fmt.Fprintln(stdout, " - --name controls inferred output naming: add_<namespace>_<name>.json") _, _ = fmt.Fprintln(stdout, " - --name controls inferred output naming: add_<namespace>_<name>.json")
_, _ = fmt.Fprintln(stdout, " - flags accept both --flag value and --flag=value forms") _, _ = fmt.Fprintln(stdout, " - flags accept both --flag value and --flag=value forms")
_, _ = fmt.Fprintln(stdout, " - unkeyed rows now fail conversion instead of being skipped") _, _ = fmt.Fprintln(stdout, " - unkeyed rows now fail conversion instead of being skipped")
@@ -238,7 +239,7 @@ func parseConvertArgs(args []string, allowFormat bool, defaultCollision string)
opts.KeyFields = append(opts.KeyFields, ctx.Template.KeyFields...) opts.KeyFields = append(opts.KeyFields, ctx.Template.KeyFields...)
} }
if strings.TrimSpace(opts.Namespace) == "" { if strings.TrimSpace(opts.Namespace) == "" {
return opts, "", "", errors.New("2da-to-module requires --namespace or a topdata/templates/config.json entry for the input table") return opts, "", "", errors.New("2da-to-module requires --namespace or a topdata/templates/config.yaml entry for the input table")
} }
switch opts.Type { switch opts.Type {
case "entry": case "entry":
@@ -420,7 +421,7 @@ func convert2DAToModule(data parsed2DA, outputPath string, opts convertOptions)
entries[key] = entry entries[key] = entry
} }
if len(missing) > 0 { if len(missing) > 0 {
return nil, fmt.Errorf("unable to generate keys for row ids %s; declare --key-field or configure key_fields in topdata/templates/config.json", strings.Join(missing, ", ")) return nil, fmt.Errorf("unable to generate keys for row ids %s; declare --key-field or configure key_fields in topdata/templates/config.yaml", strings.Join(missing, ", "))
} }
return map[string]any{"entries": entries}, nil return map[string]any{"entries": entries}, nil
} }
@@ -544,12 +545,12 @@ func normalizeConvertedKeyText(text string) string {
} }
type convertTemplateTable struct { type convertTemplateTable struct {
Namespace string `json:"namespace"` Namespace string `json:"namespace" yaml:"namespace"`
KeyFields []string `json:"key_fields"` KeyFields []string `json:"key_fields" yaml:"key_fields"`
} }
type convertTemplateConfig struct { type convertTemplateConfig struct {
Tables map[string]convertTemplateTable `json:"tables"` Tables map[string]convertTemplateTable `json:"tables" yaml:"tables"`
} }
type convertContext struct { type convertContext struct {
@@ -617,20 +618,31 @@ func loadTemplateTableConfig(p *project.Project, input string) (convertTemplateT
} }
func loadConvertTemplateConfig(p *project.Project) (convertTemplateConfig, error) { func loadConvertTemplateConfig(p *project.Project) (convertTemplateConfig, error) {
configPath := filepath.Join(p.TopDataSourceDir(), "templates", "config.json") templatesDir := filepath.Join(p.TopDataSourceDir(), "templates")
for _, candidate := range []string{"config.yaml", "config.yml", "config.json"} {
configPath := filepath.Join(templatesDir, candidate)
raw, err := os.ReadFile(configPath) raw, err := os.ReadFile(configPath)
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return convertTemplateConfig{}, nil continue
} }
return convertTemplateConfig{}, fmt.Errorf("read %s: %w", configPath, err) return convertTemplateConfig{}, fmt.Errorf("read %s: %w", configPath, err)
} }
var config convertTemplateConfig var config convertTemplateConfig
switch filepath.Ext(configPath) {
case ".yaml", ".yml":
if err := yaml.Unmarshal(raw, &config); err != nil {
return convertTemplateConfig{}, fmt.Errorf("parse %s: %w", configPath, err)
}
default:
if err := json.Unmarshal(raw, &config); err != nil { if err := json.Unmarshal(raw, &config); err != nil {
return convertTemplateConfig{}, fmt.Errorf("parse %s: %w", configPath, err) return convertTemplateConfig{}, fmt.Errorf("parse %s: %w", configPath, err)
} }
}
return config, nil return config, nil
}
return convertTemplateConfig{}, nil
} }
func resolveConvertOutputContext(p *project.Project, output string) (convertTemplateTable, error) { func resolveConvertOutputContext(p *project.Project, output string) (convertTemplateTable, error) {
+151 -65
View File
@@ -11,21 +11,27 @@ import (
func TestRunConvertCommandInfersTemplateNamespaceAndOutput(t *testing.T) { func TestRunConvertCommandInfersTemplateNamespaceAndOutput(t *testing.T) {
root := testProjectRoot(t) root := testProjectRoot(t)
writeFile(t, filepath.Join(root, "nwn-tool.json"), `{ writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
"module": {"name": "Test", "resref": "test"}, module:
"paths": {"source": "src", "assets": "assets", "build": "build"}, name: Test
"topdata": {"source": "topdata", "build": "build/topdata"} resref: test
}`+"\n") paths:
source: src
assets: assets
build: build
topdata:
source: topdata
build: build/topdata
`)
mkdirAll(t, filepath.Join(root, "topdata", "templates")) mkdirAll(t, filepath.Join(root, "topdata", "templates"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "appearance", "modules")) mkdirAll(t, filepath.Join(root, "topdata", "data", "appearance", "modules"))
writeFile(t, filepath.Join(root, "topdata", "templates", "config.json"), `{ writeFile(t, filepath.Join(root, "topdata", "templates", "config.yaml"), `
"tables": { tables:
"template_appearance": { template_appearance:
"namespace": "appearance", namespace: appearance
"key_fields": ["LABEL"] key_fields:
} - LABEL
} `)
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "templates", "template_appearance.2da"), "2DA V2.0\n\nLABEL STRING_REF NAME\n960 \"Zombie, Ash, Hot\" **** Zombie_Ash_Hot\n961 \"Zombie, Ash, Done\" **** Zombie_Ash_Done\n") writeFile(t, filepath.Join(root, "topdata", "templates", "template_appearance.2da"), "2DA V2.0\n\nLABEL STRING_REF NAME\n960 \"Zombie, Ash, Hot\" **** Zombie_Ash_Hot\n961 \"Zombie, Ash, Done\" **** Zombie_Ash_Done\n")
oldCwd, err := os.Getwd() oldCwd, err := os.Getwd()
@@ -64,20 +70,26 @@ func TestRunConvertCommandInfersTemplateNamespaceAndOutput(t *testing.T) {
func TestRunConvertCommand2DAToJSONInfersTemplateKeys(t *testing.T) { func TestRunConvertCommand2DAToJSONInfersTemplateKeys(t *testing.T) {
root := testProjectRoot(t) root := testProjectRoot(t)
writeFile(t, filepath.Join(root, "nwn-tool.json"), `{ writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
"module": {"name": "Test", "resref": "test"}, module:
"paths": {"source": "src", "assets": "assets", "build": "build"}, name: Test
"topdata": {"source": "topdata", "build": "build/topdata"} resref: test
}`+"\n") paths:
source: src
assets: assets
build: build
topdata:
source: topdata
build: build/topdata
`)
mkdirAll(t, filepath.Join(root, "topdata", "templates")) mkdirAll(t, filepath.Join(root, "topdata", "templates"))
writeFile(t, filepath.Join(root, "topdata", "templates", "config.json"), `{ writeFile(t, filepath.Join(root, "topdata", "templates", "config.yaml"), `
"tables": { tables:
"template_appearance": { template_appearance:
"namespace": "appearance", namespace: appearance
"key_fields": ["LABEL"] key_fields:
} - LABEL
} `)
}`+"\n")
inputPath := filepath.Join(root, "topdata", "templates", "template_appearance.2da") inputPath := filepath.Join(root, "topdata", "templates", "template_appearance.2da")
writeFile(t, inputPath, "2DA V2.0\n\nLABEL STRING_REF NAME\n960 \"Zombie, Ash, Hot\" **** Zombie_Ash_Hot\n961 \"Zombie, Ash, Done\" **** Zombie_Ash_Done\n") writeFile(t, inputPath, "2DA V2.0\n\nLABEL STRING_REF NAME\n960 \"Zombie, Ash, Hot\" **** Zombie_Ash_Hot\n961 \"Zombie, Ash, Done\" **** Zombie_Ash_Done\n")
@@ -121,21 +133,28 @@ func TestRunConvertCommand2DAToJSONInfersTemplateKeys(t *testing.T) {
func TestRunConvertCommand2DAToJSONInfersOutputDatasetKeysFromConfig(t *testing.T) { func TestRunConvertCommand2DAToJSONInfersOutputDatasetKeysFromConfig(t *testing.T) {
root := testProjectRoot(t) root := testProjectRoot(t)
writeFile(t, filepath.Join(root, "nwn-tool.json"), `{ writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
"module": {"name": "Test", "resref": "test"}, module:
"paths": {"source": "src", "assets": "assets", "build": "build"}, name: Test
"topdata": {"source": "topdata", "build": "build/topdata"} resref: test
}`+"\n") paths:
source: src
assets: assets
build: build
topdata:
source: topdata
build: build/topdata
`)
mkdirAll(t, filepath.Join(root, "topdata", "templates")) mkdirAll(t, filepath.Join(root, "topdata", "templates"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "soundset")) mkdirAll(t, filepath.Join(root, "topdata", "data", "soundset"))
writeFile(t, filepath.Join(root, "topdata", "templates", "config.json"), `{ writeFile(t, filepath.Join(root, "topdata", "templates", "config.yaml"), `
"tables": { tables:
"template_soundset": { template_soundset:
"namespace": "soundset", namespace: soundset
"key_fields": ["RESREF", "LABEL"] key_fields:
} - RESREF
} - LABEL
}`+"\n") `)
inputPath := filepath.Join(root, "scratch_soundset.2da") inputPath := filepath.Join(root, "scratch_soundset.2da")
writeFile(t, inputPath, "2DA V2.0\n\nLABEL RESREF STRREF GENDER TYPE\n0 \"Bandit Male\" nw_bandit 100 1 4\n1 \"Bandit Female\" nw_bandit 101 2 4\n") writeFile(t, inputPath, "2DA V2.0\n\nLABEL RESREF STRREF GENDER TYPE\n0 \"Bandit Male\" nw_bandit 100 1 4\n1 \"Bandit Female\" nw_bandit 101 2 4\n")
@@ -179,20 +198,26 @@ func TestRunConvertCommand2DAToJSONInfersOutputDatasetKeysFromConfig(t *testing.
func TestRunConvertCommand2DAToJSONInfersAmbientTemplateKeysFromResource(t *testing.T) { func TestRunConvertCommand2DAToJSONInfersAmbientTemplateKeysFromResource(t *testing.T) {
root := testProjectRoot(t) root := testProjectRoot(t)
writeFile(t, filepath.Join(root, "nwn-tool.json"), `{ writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
"module": {"name": "Test", "resref": "test"}, module:
"paths": {"source": "src", "assets": "assets", "build": "build"}, name: Test
"topdata": {"source": "topdata", "build": "build/topdata"} resref: test
}`+"\n") paths:
source: src
assets: assets
build: build
topdata:
source: topdata
build: build/topdata
`)
mkdirAll(t, filepath.Join(root, "topdata", "templates")) mkdirAll(t, filepath.Join(root, "topdata", "templates"))
writeFile(t, filepath.Join(root, "topdata", "templates", "config.json"), `{ writeFile(t, filepath.Join(root, "topdata", "templates", "config.yaml"), `
"tables": { tables:
"template_ambientmusic": { template_ambientmusic:
"namespace": "ambientmusic", namespace: ambientmusic
"key_fields": ["Resource"] key_fields:
} - Resource
} `)
}`+"\n")
inputPath := filepath.Join(root, "topdata", "templates", "template_ambientmusic.2da") inputPath := filepath.Join(root, "topdata", "templates", "template_ambientmusic.2da")
writeFile(t, inputPath, "2DA V2.0\n\nDescription DisplayName Resource Stinger1 Stinger2 Stinger3\n0 61901 **** **** **** **** ****\n1 61842 **** mus_ruralday1 **** **** ****\n2 61843 **** mus_ruralday2 **** **** ****\n") writeFile(t, inputPath, "2DA V2.0\n\nDescription DisplayName Resource Stinger1 Stinger2 Stinger3\n0 61901 **** **** **** **** ****\n1 61842 **** mus_ruralday1 **** **** ****\n2 61843 **** mus_ruralday2 **** **** ****\n")
@@ -239,11 +264,18 @@ func TestRunConvertCommand2DAToJSONInfersAmbientTemplateKeysFromResource(t *test
func TestRunConvertCommand2DAToJSONSuffixesDuplicateGeneratedKeys(t *testing.T) { func TestRunConvertCommand2DAToJSONSuffixesDuplicateGeneratedKeys(t *testing.T) {
root := testProjectRoot(t) root := testProjectRoot(t)
writeFile(t, filepath.Join(root, "nwn-tool.json"), `{ writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
"module": {"name": "Test", "resref": "test"}, module:
"paths": {"source": "src", "assets": "assets", "build": "build"}, name: Test
"topdata": {"source": "topdata", "build": "build/topdata"} resref: test
}`+"\n") paths:
source: src
assets: assets
build: build
topdata:
source: topdata
build: build/topdata
`)
inputPath := filepath.Join(root, "dup.2da") inputPath := filepath.Join(root, "dup.2da")
writeFile(t, inputPath, "2DA V2.0\n\nResource\n1 cmp_reserved\n2 cmp_reserved\n") writeFile(t, inputPath, "2DA V2.0\n\nResource\n1 cmp_reserved\n2 cmp_reserved\n")
@@ -353,14 +385,13 @@ func TestRunConvertCommandFailsOnUnkeyedRows(t *testing.T) {
"topdata": {"source": "topdata", "build": "build/topdata"} "topdata": {"source": "topdata", "build": "build/topdata"}
}`+"\n") }`+"\n")
mkdirAll(t, filepath.Join(root, "topdata", "templates")) mkdirAll(t, filepath.Join(root, "topdata", "templates"))
writeFile(t, filepath.Join(root, "topdata", "templates", "config.json"), `{ writeFile(t, filepath.Join(root, "topdata", "templates", "config.yaml"), `
"tables": { tables:
"template_appearance": { template_appearance:
"namespace": "appearance", namespace: appearance
"key_fields": ["STRING_REF"] key_fields:
} - STRING_REF
} `)
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "templates", "template_appearance.2da"), "2DA V2.0\n\nLABEL STRING_REF NAME\n960 \"Zombie, Ash, Hot\" **** Zombie_Ash_Hot\n") writeFile(t, filepath.Join(root, "topdata", "templates", "template_appearance.2da"), "2DA V2.0\n\nLABEL STRING_REF NAME\n960 \"Zombie, Ash, Hot\" **** Zombie_Ash_Hot\n")
oldCwd, err := os.Getwd() oldCwd, err := os.Getwd()
@@ -384,6 +415,61 @@ func TestRunConvertCommandFailsOnUnkeyedRows(t *testing.T) {
} }
} }
func TestRunConvertCommandFallsBackToLegacyTemplateJSONConfig(t *testing.T) {
root := testProjectRoot(t)
writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
name: Test
resref: test
paths:
source: src
assets: assets
build: build
topdata:
source: topdata
build: build/topdata
`)
mkdirAll(t, filepath.Join(root, "topdata", "templates"))
writeFile(t, filepath.Join(root, "topdata", "templates", "config.json"), `{
"tables": {
"template_appearance": {
"namespace": "appearance",
"key_fields": ["LABEL"]
}
}
}`+"\n")
inputPath := filepath.Join(root, "topdata", "templates", "template_appearance.2da")
writeFile(t, inputPath, "2DA V2.0\n\nLABEL\n960 Zombie_Ash_Hot\n")
oldCwd, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
t.Cleanup(func() {
_ = os.Chdir(oldCwd)
})
if err := os.Chdir(root); err != nil {
t.Fatalf("chdir: %v", err)
}
outputPath := filepath.Join(root, "out", "appearance.json")
if err := RunConvertCommand([]string{
"2da-to-json",
"topdata/templates/template_appearance.2da",
outputPath,
}, &bytes.Buffer{}); err != nil {
t.Fatalf("RunConvertCommand failed: %v", err)
}
raw, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("read output: %v", err)
}
if !strings.Contains(string(raw), `"appearance:zombie_ash_hot"`) {
t.Fatalf("expected legacy JSON template config fallback to be honored, got %s", string(raw))
}
}
func TestConvert2DAToModuleFailsOnDuplicateGeneratedKeysByDefault(t *testing.T) { func TestConvert2DAToModuleFailsOnDuplicateGeneratedKeysByDefault(t *testing.T) {
_, err := convert2DAToModule(parsed2DA{ _, err := convert2DAToModule(parsed2DA{
Columns: []string{"LABEL"}, Columns: []string{"LABEL"},