Opt-In Wiki Generation

This commit is contained in:
2026-04-18 09:11:03 +02:00
parent 338b4abf3b
commit 6daf5bf7a7
6 changed files with 96 additions and 29 deletions
+3 -1
View File
@@ -508,8 +508,10 @@ func parseBuildTopDataArgs(commandName string, args []string) (topdata.BuildAndP
switch arg { switch arg {
case "--force": case "--force":
opts.Force = true opts.Force = true
case "--wiki":
opts.BuildWiki = true
case "-h", "--help": case "-h", "--help":
return opts, fmt.Errorf("usage: %s [--force]", commandName) return opts, fmt.Errorf("usage: %s [--force] [--wiki]", commandName)
default: default:
return opts, fmt.Errorf("unknown %s argument %q", commandName, arg) return opts, fmt.Errorf("unknown %s argument %q", commandName, arg)
} }
+24 -14
View File
@@ -135,7 +135,15 @@ func Build(p *project.Project, progress func(string)) (BuildResult, error) {
return BuildNative(p, progress) return BuildNative(p, progress)
} }
type NativeBuildOptions struct {
BuildWiki bool
}
func BuildNative(p *project.Project, progress func(string)) (BuildResult, error) { func BuildNative(p *project.Project, progress func(string)) (BuildResult, error) {
return BuildNativeWithOptions(p, NativeBuildOptions{BuildWiki: true}, progress)
}
func BuildNativeWithOptions(p *project.Project, opts NativeBuildOptions, progress func(string)) (BuildResult, error) {
if progress == nil { if progress == nil {
progress = func(string) {} progress = func(string) {}
} }
@@ -152,26 +160,28 @@ func BuildNative(p *project.Project, progress func(string)) (BuildResult, error)
return BuildResult{}, fmt.Errorf("topdata validation failed with %d error(s)", report.ErrorCount()) return BuildResult{}, fmt.Errorf("topdata validation failed with %d error(s)", report.ErrorCount())
} }
var prebuiltWiki *wikiResult var prebuiltWiki *wikiResult
if newestWikiSource, _, err := newestRelevantWikiSource(p.TopDataSourceDir()); err == nil && !newestWikiSource.IsZero() { if opts.BuildWiki {
statePath := filepath.Join(p.TopDataSourceDir(), wikiRootDirName, wikiStateFileName) if newestWikiSource, _, err := newestRelevantWikiSource(p.TopDataSourceDir()); err == nil && !newestWikiSource.IsZero() {
outputDir := filepath.Join(p.TopDataSourceDir(), wikiRootDirName, wikiPagesDirName) statePath := filepath.Join(p.TopDataSourceDir(), wikiRootDirName, wikiStateFileName)
if stateInfo, err := os.Stat(statePath); err == nil && !newestWikiSource.After(stateInfo.ModTime()) { outputDir := filepath.Join(p.TopDataSourceDir(), wikiRootDirName, wikiPagesDirName)
if outputInfo, err := os.Stat(outputDir); err == nil && outputInfo.IsDir() { if stateInfo, err := os.Stat(statePath); err == nil && !newestWikiSource.After(stateInfo.ModTime()) {
if state, err := loadWikiState(statePath); err == nil { if outputInfo, err := os.Stat(outputDir); err == nil && outputInfo.IsDir() {
prebuiltWiki = &wikiResult{ if state, err := loadWikiState(statePath); err == nil {
OutputDir: outputDir, prebuiltWiki = &wikiResult{
PageCount: state.Pages, OutputDir: outputDir,
Status: wikiSkippedStatus, PageCount: state.Pages,
Digest: state.Digest, Status: wikiSkippedStatus,
Digest: state.Digest,
}
} }
} }
} }
} }
} }
return buildNativeUnchecked(p, progress, prebuiltWiki) return buildNativeUnchecked(p, opts, progress, prebuiltWiki)
} }
func buildNativeUnchecked(p *project.Project, progress func(string), prebuiltWiki *wikiResult) (BuildResult, error) { func buildNativeUnchecked(p *project.Project, opts NativeBuildOptions, progress func(string), prebuiltWiki *wikiResult) (BuildResult, error) {
if progress == nil { if progress == nil {
progress = func(string) {} progress = func(string) {}
} }
@@ -333,7 +343,7 @@ func buildNativeUnchecked(p *project.Project, progress func(string), prebuiltWik
wikiBuild := wikiResult{Status: wikiSkippedStatus} wikiBuild := wikiResult{Status: wikiSkippedStatus}
if prebuiltWiki != nil { if prebuiltWiki != nil {
wikiBuild = *prebuiltWiki wikiBuild = *prebuiltWiki
} else { } else if opts.BuildWiki {
wikiBuild, err = buildWiki(p, BuildResult{ wikiBuild, err = buildWiki(p, BuildResult{
Mode: "native", Mode: "native",
Output2DADir: output2DA, Output2DADir: output2DA,
+12 -8
View File
@@ -45,14 +45,6 @@ func resolveReleasedPartsManifest(p *project.Project, progress func(string)) (ma
if progress == nil { if progress == nil {
progress = func(string) {} progress = func(string) {}
} }
spec, err := deriveSowAssetsRepoSpec(p.Root)
if err != nil {
return nil, err
}
manifestURL, err := resolvePartsManifestAssetURL(spec)
if err != nil {
return nil, err
}
cachePath := filepath.Join(p.Root, ".cache", "topdata", partsManifestCacheFileName) cachePath := filepath.Join(p.Root, ".cache", "topdata", partsManifestCacheFileName)
if strings.TrimSpace(os.Getenv("SOW_PARTS_MANIFEST_REFRESH")) == "" { if strings.TrimSpace(os.Getenv("SOW_PARTS_MANIFEST_REFRESH")) == "" {
manifest, fresh, err := readFreshPartsManifestCache(cachePath, partsManifestCacheMaxAge) manifest, fresh, err := readFreshPartsManifestCache(cachePath, partsManifestCacheMaxAge)
@@ -63,6 +55,14 @@ func resolveReleasedPartsManifest(p *project.Project, progress func(string)) (ma
return inventoryFromPartsManifest(manifest), nil return inventoryFromPartsManifest(manifest), nil
} }
} }
spec, err := deriveSowAssetsRepoSpec(p.Root)
if err != nil {
return nil, err
}
manifestURL, err := resolvePartsManifestAssetURL(spec)
if err != nil {
return nil, err
}
progress(fmt.Sprintf("Fetching released parts manifest from %s...", manifestURL)) progress(fmt.Sprintf("Fetching released parts manifest from %s...", manifestURL))
manifest, err := fetchPartsManifest(manifestURL) manifest, err := fetchPartsManifest(manifestURL)
if err != nil { if err != nil {
@@ -220,6 +220,10 @@ func writePartsManifestCache(path string, manifest *partsManifest) error {
payload = append(payload, '\n') payload = append(payload, '\n')
current, err := os.ReadFile(path) current, err := os.ReadFile(path)
if err == nil && bytes.Equal(current, payload) { if err == nil && bytes.Equal(current, payload) {
now := time.Now()
if err := os.Chtimes(path, now, now); err != nil {
return fmt.Errorf("refresh parts manifest cache timestamp: %w", err)
}
return nil return nil
} }
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
+5 -4
View File
@@ -60,7 +60,8 @@ func BuildAndPackage(p *project.Project, progress func(string)) (PackageResult,
} }
type BuildAndPackageOptions struct { type BuildAndPackageOptions struct {
Force bool Force bool
BuildWiki bool
} }
func BuildAndPackageWithOptions(p *project.Project, opts BuildAndPackageOptions, progress func(string)) (PackageResult, error) { func BuildAndPackageWithOptions(p *project.Project, opts BuildAndPackageOptions, progress func(string)) (PackageResult, error) {
@@ -87,11 +88,11 @@ func BuildAndPackageWithOptions(p *project.Project, opts BuildAndPackageOptions,
} }
} }
return buildAndPackageFull(p, progress) return buildAndPackageFull(p, opts, progress)
} }
func buildAndPackageFull(p *project.Project, progress func(string)) (PackageResult, error) { func buildAndPackageFull(p *project.Project, opts BuildAndPackageOptions, progress func(string)) (PackageResult, error) {
nativeResult, err := BuildNative(p, progress) nativeResult, err := BuildNativeWithOptions(p, NativeBuildOptions{BuildWiki: opts.BuildWiki}, progress)
if err != nil { if err != nil {
return PackageResult{}, err return PackageResult{}, err
} }
+2 -2
View File
@@ -1554,7 +1554,7 @@ func validateNativeBuildability(p *project.Project, report *ValidationReport) {
return return
} }
defer restoreLockfiles(lockSnapshot) defer restoreLockfiles(lockSnapshot)
if _, err := buildNativeUnchecked(&clone, nil, nil); err != nil { if _, err := buildNativeUnchecked(&clone, NativeBuildOptions{}, nil, nil); err != nil {
report.Diagnostics = append(report.Diagnostics, Diagnostic{ report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError, Severity: SeverityError,
Path: p.TopDataSourceDir(), Path: p.TopDataSourceDir(),
@@ -1958,7 +1958,7 @@ func compareNativeSelfCheck(p *project.Project, actual2DA, actualTLK string, pro
nativeProject := *p nativeProject := *p
nativeProject.Config.TopData.Build = tempBuild nativeProject.Config.TopData.Build = tempBuild
nativeResult, err := buildNativeUnchecked(&nativeProject, nil, nil) nativeResult, err := buildNativeUnchecked(&nativeProject, NativeBuildOptions{}, nil, nil)
if err != nil { if err != nil {
return 0, 0, 0, len(projectOutputs), nil return 0, 0, 0, len(projectOutputs), nil
} }
+50
View File
@@ -86,6 +86,56 @@ func TestBuildNativeGeneratesAndSkipsWikiPages(t *testing.T) {
} }
} }
func TestBuildAndPackageBuildsWikiOnlyWhenRequested(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{
"output": "skills.2da",
"columns": ["Label", "Name", "Description", "HideFromLevelUp", "Untrained", "KeyAbility", "ArmorCheckPenalty", "Constant"],
"rows": [
{
"id": 0,
"key": "skills:athletics",
"Label": "Athletics",
"Name": {"tlk": {"key": "skills:athletics.name", "text": "Athletics"}},
"Description": {"tlk": {"key": "skills:athletics.description", "text": "Skill text."}},
"HideFromLevelUp": "0",
"Untrained": "1",
"KeyAbility": "STR",
"ArmorCheckPenalty": "1",
"Constant": "SKILL_ATHLETICS"
}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{"skills:athletics":0}`+"\n")
proj := testProject(root)
proj.Config.TopData.ReferenceBuilder = ""
result, err := BuildAndPackageWithOptions(proj, BuildAndPackageOptions{}, nil)
if err != nil {
t.Fatalf("BuildAndPackageWithOptions failed: %v", err)
}
if result.WikiOutputDir != "" || result.WikiPages != 0 {
t.Fatalf("expected wiki generation to be skipped by default, got dir=%q pages=%d", result.WikiOutputDir, result.WikiPages)
}
if _, err := os.Stat(filepath.Join(root, "topdata", ".wiki", "state.json")); !os.IsNotExist(err) {
t.Fatalf("expected no wiki state without explicit wiki build, got %v", err)
}
result, err = BuildAndPackageWithOptions(proj, BuildAndPackageOptions{Force: true, BuildWiki: true}, nil)
if err != nil {
t.Fatalf("BuildAndPackageWithOptions with wiki failed: %v", err)
}
if result.WikiPages != 1 {
t.Fatalf("expected one generated wiki page when requested, got %d", result.WikiPages)
}
if _, err := os.Stat(filepath.Join(root, "topdata", ".wiki", "state.json")); err != nil {
t.Fatalf("expected wiki state after explicit wiki build: %v", err)
}
}
func TestBuildNativeRegeneratesWikiWhenTLKTextChanges(t *testing.T) { func TestBuildNativeRegeneratesWikiWhenTLKTextChanges(t *testing.T) {
root := testProjectRoot(t) root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills")) mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))