diff --git a/internal/app/app.go b/internal/app/app.go index e4a24a4..d755b97 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -508,8 +508,10 @@ func parseBuildTopDataArgs(commandName string, args []string) (topdata.BuildAndP switch arg { case "--force": opts.Force = true + case "--wiki": + opts.BuildWiki = true case "-h", "--help": - return opts, fmt.Errorf("usage: %s [--force]", commandName) + return opts, fmt.Errorf("usage: %s [--force] [--wiki]", commandName) default: return opts, fmt.Errorf("unknown %s argument %q", commandName, arg) } diff --git a/internal/topdata/native.go b/internal/topdata/native.go index adee590..2ad5d70 100644 --- a/internal/topdata/native.go +++ b/internal/topdata/native.go @@ -135,7 +135,15 @@ func Build(p *project.Project, progress func(string)) (BuildResult, error) { return BuildNative(p, progress) } +type NativeBuildOptions struct { + BuildWiki bool +} + 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 { 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()) } var prebuiltWiki *wikiResult - if newestWikiSource, _, err := newestRelevantWikiSource(p.TopDataSourceDir()); err == nil && !newestWikiSource.IsZero() { - statePath := filepath.Join(p.TopDataSourceDir(), wikiRootDirName, wikiStateFileName) - outputDir := filepath.Join(p.TopDataSourceDir(), wikiRootDirName, wikiPagesDirName) - if stateInfo, err := os.Stat(statePath); err == nil && !newestWikiSource.After(stateInfo.ModTime()) { - if outputInfo, err := os.Stat(outputDir); err == nil && outputInfo.IsDir() { - if state, err := loadWikiState(statePath); err == nil { - prebuiltWiki = &wikiResult{ - OutputDir: outputDir, - PageCount: state.Pages, - Status: wikiSkippedStatus, - Digest: state.Digest, + if opts.BuildWiki { + if newestWikiSource, _, err := newestRelevantWikiSource(p.TopDataSourceDir()); err == nil && !newestWikiSource.IsZero() { + statePath := filepath.Join(p.TopDataSourceDir(), wikiRootDirName, wikiStateFileName) + outputDir := filepath.Join(p.TopDataSourceDir(), wikiRootDirName, wikiPagesDirName) + if stateInfo, err := os.Stat(statePath); err == nil && !newestWikiSource.After(stateInfo.ModTime()) { + if outputInfo, err := os.Stat(outputDir); err == nil && outputInfo.IsDir() { + if state, err := loadWikiState(statePath); err == nil { + prebuiltWiki = &wikiResult{ + OutputDir: outputDir, + PageCount: state.Pages, + 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 { progress = func(string) {} } @@ -333,7 +343,7 @@ func buildNativeUnchecked(p *project.Project, progress func(string), prebuiltWik wikiBuild := wikiResult{Status: wikiSkippedStatus} if prebuiltWiki != nil { wikiBuild = *prebuiltWiki - } else { + } else if opts.BuildWiki { wikiBuild, err = buildWiki(p, BuildResult{ Mode: "native", Output2DADir: output2DA, diff --git a/internal/topdata/parts_manifest.go b/internal/topdata/parts_manifest.go index 37d8895..c3bac44 100644 --- a/internal/topdata/parts_manifest.go +++ b/internal/topdata/parts_manifest.go @@ -45,14 +45,6 @@ func resolveReleasedPartsManifest(p *project.Project, progress func(string)) (ma if progress == nil { 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) if strings.TrimSpace(os.Getenv("SOW_PARTS_MANIFEST_REFRESH")) == "" { manifest, fresh, err := readFreshPartsManifestCache(cachePath, partsManifestCacheMaxAge) @@ -63,6 +55,14 @@ func resolveReleasedPartsManifest(p *project.Project, progress func(string)) (ma 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)) manifest, err := fetchPartsManifest(manifestURL) if err != nil { @@ -220,6 +220,10 @@ func writePartsManifestCache(path string, manifest *partsManifest) error { payload = append(payload, '\n') current, err := os.ReadFile(path) 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 } if err != nil && !os.IsNotExist(err) { diff --git a/internal/topdata/top_package.go b/internal/topdata/top_package.go index 608736d..e6e94e3 100644 --- a/internal/topdata/top_package.go +++ b/internal/topdata/top_package.go @@ -60,7 +60,8 @@ func BuildAndPackage(p *project.Project, progress func(string)) (PackageResult, } type BuildAndPackageOptions struct { - Force bool + Force bool + BuildWiki bool } 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) { - nativeResult, err := BuildNative(p, progress) +func buildAndPackageFull(p *project.Project, opts BuildAndPackageOptions, progress func(string)) (PackageResult, error) { + nativeResult, err := BuildNativeWithOptions(p, NativeBuildOptions{BuildWiki: opts.BuildWiki}, progress) if err != nil { return PackageResult{}, err } diff --git a/internal/topdata/topdata.go b/internal/topdata/topdata.go index 98d3e37..21035c8 100644 --- a/internal/topdata/topdata.go +++ b/internal/topdata/topdata.go @@ -1554,7 +1554,7 @@ func validateNativeBuildability(p *project.Project, report *ValidationReport) { return } 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{ Severity: SeverityError, Path: p.TopDataSourceDir(), @@ -1958,7 +1958,7 @@ func compareNativeSelfCheck(p *project.Project, actual2DA, actualTLK string, pro nativeProject := *p nativeProject.Config.TopData.Build = tempBuild - nativeResult, err := buildNativeUnchecked(&nativeProject, nil, nil) + nativeResult, err := buildNativeUnchecked(&nativeProject, NativeBuildOptions{}, nil, nil) if err != nil { return 0, 0, 0, len(projectOutputs), nil } diff --git a/internal/topdata/wiki_native_test.go b/internal/topdata/wiki_native_test.go index 14a62bd..7108d1c 100644 --- a/internal/topdata/wiki_native_test.go +++ b/internal/topdata/wiki_native_test.go @@ -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) { root := testProjectRoot(t) mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))