Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6afac1a4d9 | ||
|
|
79595e55be | ||
|
|
eefe00ebc5 | ||
|
|
eb5d15061c | ||
|
|
5c46824ebd | ||
|
|
2ea7959693 |
@@ -18,3 +18,4 @@ result-*
|
|||||||
*.swp
|
*.swp
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.direnv/
|
||||||
|
|||||||
@@ -1329,6 +1329,9 @@ git commit -m "test(topdata): parity guard — cdn_channel yields accessory rows
|
|||||||
|
|
||||||
- [ ] **Step 1:** Merge Phase 1; tag/release Crucible per the repo's existing release workflow. Note the resulting version tag for Phase 3.
|
- [ ] **Step 1:** Merge Phase 1; tag/release Crucible per the repo's existing release workflow. Note the resulting version tag for Phase 3.
|
||||||
|
|
||||||
|
> ✅ Release tag: `v0.3.0`
|
||||||
|
> <https://git.westgate.pw/ShadowsOverWestgate/sow-tools/releases/tag/v0.3.0>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 3 — Toolchain convergence (flakes)
|
## Phase 3 — Toolchain convergence (flakes)
|
||||||
@@ -1392,6 +1395,8 @@ Edit `sow-topdata/flake.nix`. Add the input and thread it through:
|
|||||||
|
|
||||||
> Note: `forAllSystems` now passes both `system` and `pkgs`. If the repo's existing helper signature differs, match the convention used by `sow-assets-manifest/flake.nix` (which already pins crucible per-system) rather than introducing a new pattern.
|
> Note: `forAllSystems` now passes both `system` and `pkgs`. If the repo's existing helper signature differs, match the convention used by `sow-assets-manifest/flake.nix` (which already pins crucible per-system) rather than introducing a new pattern.
|
||||||
|
|
||||||
|
> Operator concern: The `scripts/` still exist? We should be removing wrappers for tasks Crucible controls.
|
||||||
|
|
||||||
- [ ] **Step 2: Lock the input**
|
- [ ] **Step 2: Lock the input**
|
||||||
|
|
||||||
Run: `cd sow-topdata && nix flake update sow-tools` (or `nix flake lock --update-input sow-tools`)
|
Run: `cd sow-topdata && nix flake update sow-tools` (or `nix flake lock --update-input sow-tools`)
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
gotools
|
gotools
|
||||||
ffmpeg
|
ffmpeg
|
||||||
git
|
git
|
||||||
|
tea
|
||||||
gnumake
|
gnumake
|
||||||
bashInteractive
|
bashInteractive
|
||||||
shellcheck
|
shellcheck
|
||||||
|
|||||||
@@ -661,11 +661,17 @@ func collectAssetResources(p *project.Project, requireContent bool, allowed map[
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
assetsRelPath, err := filepath.Rel(p.Root, p.AssetsDir())
|
// A module-only project (consumes prebuilt HAKs, has no paths.assets) has no
|
||||||
|
// asset files to collect — AssetFiles is empty and AssetsDir() is "". Skip the
|
||||||
|
// relative-path resolve, which would otherwise fail with `Rel: can't make ""`.
|
||||||
|
assetsRelPath := ""
|
||||||
|
if p.AssetsDir() != "" {
|
||||||
|
assetsRelPath, err = filepath.Rel(p.Root, p.AssetsDir())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("resolve assets dir relative path: %w", err)
|
return nil, fmt.Errorf("resolve assets dir relative path: %w", err)
|
||||||
}
|
}
|
||||||
assetsRelPath = filepath.ToSlash(assetsRelPath)
|
assetsRelPath = filepath.ToSlash(assetsRelPath)
|
||||||
|
}
|
||||||
|
|
||||||
for _, rel := range p.Inventory.AssetFiles {
|
for _, rel := range p.Inventory.AssetFiles {
|
||||||
if musicAssets != nil {
|
if musicAssets != nil {
|
||||||
|
|||||||
@@ -435,7 +435,11 @@ func writeCreditsArtifacts(p *project.Project, generated []musicCreditGroup) (cr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := filepath.WalkDir(p.AssetsDir(), func(path string, d os.DirEntry, walkErr error) error {
|
// Authored credits.md files live under the assets tree. A project with no
|
||||||
|
// configured assets dir (e.g. a module that consumes prebuilt HAKs) has none
|
||||||
|
// to collect — walking "" would fail with `lstat : no such file or directory`.
|
||||||
|
if assetsDir := p.AssetsDir(); assetsDir != "" {
|
||||||
|
err := filepath.WalkDir(assetsDir, func(path string, d os.DirEntry, walkErr error) error {
|
||||||
if walkErr != nil {
|
if walkErr != nil {
|
||||||
return walkErr
|
return walkErr
|
||||||
}
|
}
|
||||||
@@ -463,6 +467,7 @@ func writeCreditsArtifacts(p *project.Project, generated []musicCreditGroup) (cr
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return creditsArtifactWriteResult{}, err
|
return creditsArtifactWriteResult{}, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sort.Slice(sources, func(i, j int) bool { return sources[i].Path < sources[j].Path })
|
sort.Slice(sources, func(i, j int) bool { return sources[i].Path < sources[j].Path })
|
||||||
payload, err := json.MarshalIndent(music.CreditsInventory{Sources: sources}, "", " ")
|
payload, err := json.MarshalIndent(music.CreditsInventory{Sources: sources}, "", " ")
|
||||||
|
|||||||
@@ -4084,3 +4084,62 @@ haks:
|
|||||||
t.Fatalf("plan-only should not stage music, stat err=%v", err)
|
t.Fatalf("plan-only should not stage music, stat err=%v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A module-only project (consumes prebuilt HAKs, no paths.assets) must build via
|
||||||
|
// the full `crucible module build` (pipeline.Build) without an assets tree. The
|
||||||
|
// HAK + music stages used to crash on the unset assets dir
|
||||||
|
// (`lstat : no such file or directory` / `Rel: can't make "" relative`).
|
||||||
|
func TestBuildModuleOnlyProjectWithoutAssetsDir(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
mustMkdir(t, filepath.Join(root, "src", "module"))
|
||||||
|
mustMkdir(t, filepath.Join(root, "build"))
|
||||||
|
|
||||||
|
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
|
||||||
|
module:
|
||||||
|
name: Test Module
|
||||||
|
resref: testmod
|
||||||
|
paths:
|
||||||
|
source: src
|
||||||
|
build: build
|
||||||
|
`)
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if p.AssetsDir() != "" {
|
||||||
|
t.Fatalf("test precondition: expected unset assets dir, got %q", p.AssetsDir())
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := Build(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("full build of module-only project: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(result.ModulePath); err != nil {
|
||||||
|
t.Fatalf("expected built .mod at %s: %v", result.ModulePath, err)
|
||||||
|
}
|
||||||
|
if result.HAKAssets != 0 {
|
||||||
|
t.Fatalf("module-only project has no assets, expected 0 HAK assets, got %d", result.HAKAssets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -788,6 +788,13 @@ func (p *Project) ValidateLayout() error {
|
|||||||
}
|
}
|
||||||
info, err := os.Stat(path)
|
info, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// An output dir that does not exist yet is fine: the builder creates
|
||||||
|
// it (MkdirAll) before writing. A bare clone must validate/build from
|
||||||
|
// a clean tree with no pre-step (R2/parity) — only a path that exists
|
||||||
|
// but is NOT a directory is a real error.
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
failures = append(failures, fmt.Errorf("%s: %w", label, err))
|
failures = append(failures, fmt.Errorf("%s: %w", label, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1113,6 +1113,52 @@ func TestValidateLayoutAllowsMissingAssetsDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// paths.build is an OUTPUT dir the builder creates (MkdirAll) before writing, so
|
||||||
|
// a bare clone with no build dir yet must still validate/build with no pre-step
|
||||||
|
// (R2/parity). Only a build path that exists but is not a directory is an error.
|
||||||
|
func TestValidateLayoutAllowsMissingBuildDir(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
mkdirAll(t, filepath.Join(root, "src"))
|
||||||
|
// deliberately do NOT create build/
|
||||||
|
|
||||||
|
proj := &Project{
|
||||||
|
Root: root,
|
||||||
|
Config: Config{
|
||||||
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
||||||
|
Paths: PathConfig{Source: "src", Build: "build"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := proj.ValidateLayout(); err != nil {
|
||||||
|
t.Fatalf("ValidateLayout returned error for missing build dir: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(root, "build")); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("expected missing build dir to remain absent, got err=%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A build path that exists but is a regular file is still a hard error.
|
||||||
|
func TestValidateLayoutRejectsBuildDirThatIsAFile(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
mkdirAll(t, filepath.Join(root, "src"))
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "build"), []byte("i am a file, not a dir"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proj := &Project{
|
||||||
|
Root: root,
|
||||||
|
Config: Config{
|
||||||
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
||||||
|
Paths: PathConfig{Source: "src", Build: "build"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := proj.ValidateLayout()
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "paths.build must be a directory") {
|
||||||
|
t.Fatalf("expected paths.build-must-be-a-directory error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateLayoutRejectsInvalidTopDataPackageOutputNames(t *testing.T) {
|
func TestValidateLayoutRejectsInvalidTopDataPackageOutputNames(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
mkdirAll(t, filepath.Join(root, "src"))
|
mkdirAll(t, filepath.Join(root, "src"))
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
|
|
||||||
const autogenManifestCacheMaxAge = time.Hour
|
const autogenManifestCacheMaxAge = time.Hour
|
||||||
|
|
||||||
const defaultBunnyCDNBase = "https://depot.westgate.pw"
|
const defaultBunnyCDNBase = "https://cdn-a7f3k9.westgate.pw"
|
||||||
|
|
||||||
// errAutogenManifestUnavailable marks a released autogen manifest that could not
|
// errAutogenManifestUnavailable marks a released autogen manifest that could not
|
||||||
// be located or fetched (network failure, missing release/asset, empty payload,
|
// be located or fetched (network failure, missing release/asset, empty payload,
|
||||||
|
|||||||
@@ -3,3 +3,4 @@
|
|||||||
# wrappers/ changes. Add a repo here AND grant the sync bot write access to it.
|
# wrappers/ changes. Add a repo here AND grant the sync bot write access to it.
|
||||||
ShadowsOverWestgate/sow-module
|
ShadowsOverWestgate/sow-module
|
||||||
ShadowsOverWestgate/sow-topdata
|
ShadowsOverWestgate/sow-topdata
|
||||||
|
ShadowsOverWestgate/sow-assets-manifest
|
||||||
|
|||||||
Reference in New Issue
Block a user