Configuration Hardening 2
What changed:
- Added visible runtime override reporting with masking for sensitive values.
- Expanded effective config for build cleanup, script compiler discovery/env, extract layout/cleanup/
HAK discovery, wiki deploy defaults, autogen cache policy, and release-source env names.
- Wired configured values into build, script compiler resolution, extract, autogen cache refresh/max
age, parts manifest release source, wiki generation/deploy, and TLK output naming.
- Added config sources override output and validation-rule hints in config explain.
- Added CONFIGURATION_HARDENING_AUDIT.md.
- Updated README.md with config commands, schema defaults, and JSON artifact boundaries.
- Added regression tests for override visibility/masking, compiler/extract/wiki/autogen config,
configured HAK discovery, and config sources output.
This commit is contained in:
+25
-25
@@ -215,7 +215,7 @@ func planHAKs(p *project.Project, progress ProgressFunc) (BuildResult, error) {
|
||||
}
|
||||
|
||||
func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bool, archiveNames []string, sourceManifestPath string) (result BuildResult, err error) {
|
||||
preserveExistingHAKs := envBool("SOW_BUILD_HAKS_KEEP_EXISTING")
|
||||
preserveExistingHAKs := p.EffectiveConfig().Build.KeepExistingHAKs || envBool("SOW_BUILD_HAKS_KEEP_EXISTING")
|
||||
|
||||
progressf(progress, "Validating project...")
|
||||
if err := validateForBuild(p); err != nil {
|
||||
@@ -556,7 +556,7 @@ func compileReferencedScripts(p *project.Project) ([]erf.Resource, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compilerEnv, err := resolveScriptCompilerEnvironment()
|
||||
compilerEnv, err := resolveScriptCompilerEnvironment(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -630,40 +630,40 @@ func referencedScriptResrefs(p *project.Project) ([]string, error) {
|
||||
}
|
||||
|
||||
func resolveScriptCompiler(p *project.Project) (string, error) {
|
||||
if configured := strings.TrimSpace(os.Getenv("SOW_NWN_SCRIPT_COMPILER")); configured != "" {
|
||||
if configured := p.ScriptCompilerPath(); configured != "" {
|
||||
return configured, nil
|
||||
}
|
||||
if configured := strings.TrimSpace(os.Getenv(p.ScriptCompilerPathEnv())); configured != "" {
|
||||
return configured, nil
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
filepath.Join(p.Root, "tools", "script-compiler", "nwn_script_comp"),
|
||||
filepath.Join(p.Root, "tools", "script-compiler", "nwn_script_comp.exe"),
|
||||
filepath.Join(p.Root, "tools", "nwn_script_comp"),
|
||||
filepath.Join(p.Root, "tools", "nwn_script_comp.exe"),
|
||||
}
|
||||
candidates := p.ScriptCompilerSearchPaths()
|
||||
if runtime.GOOS == "windows" {
|
||||
candidates = []string{
|
||||
filepath.Join(p.Root, "tools", "script-compiler", "nwn_script_comp.exe"),
|
||||
filepath.Join(p.Root, "tools", "script-compiler", "nwn_script_comp"),
|
||||
filepath.Join(p.Root, "tools", "nwn_script_comp.exe"),
|
||||
filepath.Join(p.Root, "tools", "nwn_script_comp"),
|
||||
}
|
||||
slices.SortStableFunc(candidates, func(a, b string) int {
|
||||
aExe := strings.HasSuffix(strings.ToLower(a), ".exe")
|
||||
bExe := strings.HasSuffix(strings.ToLower(b), ".exe")
|
||||
if aExe == bExe {
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
if aExe {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
})
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if binary, ok := strings.CutPrefix(candidate, "PATH:"); ok {
|
||||
if compiler, err := exec.LookPath(binary); err == nil {
|
||||
return compiler, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
|
||||
if compiler, err := exec.LookPath("nwn_script_comp"); err == nil {
|
||||
return compiler, nil
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
if compiler, err := exec.LookPath("nwn_script_comp.exe"); err == nil {
|
||||
return compiler, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("script compiler not found; install nwn_script_comp into ./tools or set SOW_NWN_SCRIPT_COMPILER")
|
||||
return "", fmt.Errorf("script compiler not found; configure scripts.compiler.path or set %s", p.ScriptCompilerPathEnv())
|
||||
}
|
||||
|
||||
func validatorLoadDocument(path string) (gff.Document, string, string, error) {
|
||||
|
||||
@@ -57,7 +57,7 @@ func Extract(p *project.Project, files ...string) (ExtractResult, error) {
|
||||
failures = append(failures, errs...)
|
||||
}
|
||||
|
||||
hakPaths, err := filepath.Glob(filepath.Join(p.BuildDir(), "*.hak"))
|
||||
hakPaths, err := extractHAKPaths(p)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("scan hak archives: %w", err)
|
||||
}
|
||||
@@ -88,9 +88,11 @@ func Extract(p *project.Project, files ...string) (ExtractResult, error) {
|
||||
failures = append(failures, errs...)
|
||||
}
|
||||
|
||||
removed, errs := cleanupStaleFiles(p, desired)
|
||||
result.Removed = removed
|
||||
failures = append(failures, errs...)
|
||||
if p.EffectiveConfig().Extract.CleanupStale == nil || *p.EffectiveConfig().Extract.CleanupStale {
|
||||
removed, errs := cleanupStaleFiles(p, desired)
|
||||
result.Removed = removed
|
||||
failures = append(failures, errs...)
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
return result, errors.Join(failures...)
|
||||
@@ -157,7 +159,7 @@ func extractedFile(p *project.Project, resource erf.Resource, extension string)
|
||||
|
||||
switch extension {
|
||||
case "nss":
|
||||
return filepath.Join(p.SourceDir(), "scripts", resref+".nss"), resource.Data, nil
|
||||
return filepath.Join(p.ScriptSourceDir(), resref+".nss"), resource.Data, nil
|
||||
case "utc", "utd", "ute", "uti", "utm", "utp", "uts", "utt", "utw",
|
||||
"are", "dlg", "fac", "gic", "git", "ifo", "itp", "jrl":
|
||||
document, err := gff.Read(bytes.NewReader(resource.Data))
|
||||
@@ -175,6 +177,30 @@ func extractedFile(p *project.Project, resource erf.Resource, extension string)
|
||||
}
|
||||
}
|
||||
|
||||
func extractHAKPaths(p *project.Project) ([]string, error) {
|
||||
switch p.EffectiveConfig().Extract.HAKDiscovery {
|
||||
case "configured_haks":
|
||||
paths := make([]string, 0, len(p.Config.HAKs))
|
||||
for _, hak := range p.Config.HAKs {
|
||||
path := p.HAKArchivePath(hak.Name)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
paths = append(paths, path)
|
||||
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
slices.Sort(paths)
|
||||
return paths, nil
|
||||
default:
|
||||
hakPaths, err := filepath.Glob(filepath.Join(p.BuildDir(), "*.hak"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slices.Sort(hakPaths)
|
||||
return hakPaths, nil
|
||||
}
|
||||
}
|
||||
|
||||
type writeState int
|
||||
|
||||
const (
|
||||
|
||||
@@ -175,6 +175,77 @@ func TestExtractReadsHAKAssets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractConfiguredHAKDiscoveryIgnoresUnconfiguredArchives(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mustMkdir(t, filepath.Join(root, "src"))
|
||||
mustMkdir(t, filepath.Join(root, "assets"))
|
||||
mustMkdir(t, filepath.Join(root, "build"))
|
||||
|
||||
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
|
||||
module:
|
||||
name: Test Module
|
||||
resref: testmod
|
||||
paths:
|
||||
source: src
|
||||
assets: assets
|
||||
build: build
|
||||
extract:
|
||||
hak_discovery: configured_haks
|
||||
haks:
|
||||
- name: wanted
|
||||
priority: 1
|
||||
include:
|
||||
- wanted/**
|
||||
`)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
modFile, err := os.Create(p.ModuleArchivePath())
|
||||
if err != nil {
|
||||
t.Fatalf("create mod: %v", err)
|
||||
}
|
||||
if err := erf.Write(modFile, erf.New("MOD ", nil)); err != nil {
|
||||
t.Fatalf("write mod: %v", err)
|
||||
}
|
||||
if err := modFile.Close(); err != nil {
|
||||
t.Fatalf("close mod: %v", err)
|
||||
}
|
||||
for _, name := range []string{"wanted", "ignored"} {
|
||||
hakFile, err := os.Create(filepath.Join(root, "build", name+".hak"))
|
||||
if err != nil {
|
||||
t.Fatalf("create hak: %v", err)
|
||||
}
|
||||
if err := erf.Write(hakFile, erf.New("HAK ", []erf.Resource{
|
||||
{Name: name + "_asset", Type: 0x07D2, Data: []byte("mdl-data")},
|
||||
})); err != nil {
|
||||
t.Fatalf("write hak: %v", err)
|
||||
}
|
||||
if err := hakFile.Close(); err != nil {
|
||||
t.Fatalf("close hak: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := Extract(p)
|
||||
if err != nil {
|
||||
t.Fatalf("extract: %v", err)
|
||||
}
|
||||
if len(result.HAKPaths) != 1 || filepath.Base(result.HAKPaths[0]) != "wanted.hak" {
|
||||
t.Fatalf("expected only configured hak, got %#v", result.HAKPaths)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "assets", "mdl", "wanted_asset.mdl")); err != nil {
|
||||
t.Fatalf("expected wanted asset: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "assets", "mdl", "ignored_asset.mdl")); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected ignored asset to remain absent, err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractDeletesConsumedModuleArchiveAfterSuccessWhenConfigured(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mustMkdir(t, filepath.Join(root, "src", "module"))
|
||||
|
||||
@@ -9,19 +9,21 @@ import (
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
||||
)
|
||||
|
||||
func resolveScriptCompilerEnvironment() ([]string, error) {
|
||||
func resolveScriptCompilerEnvironment(p *project.Project) ([]string, error) {
|
||||
env := os.Environ()
|
||||
|
||||
userDir, err := resolveNWNUserDirectory()
|
||||
userDir, err := resolveNWNUserDirectory(p.ScriptCompilerNWNUserDirectoryEnvKeys())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
env = upsertEnv(env, "NWN_HOME", userDir)
|
||||
env = upsertEnv(env, "NWN_USER_DIRECTORY", userDir)
|
||||
|
||||
root, err := resolveNWNRoot()
|
||||
root, err := resolveNWNRoot(p.ScriptCompilerNWNRootEnvKeys())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -32,8 +34,8 @@ func resolveScriptCompilerEnvironment() ([]string, error) {
|
||||
return env, nil
|
||||
}
|
||||
|
||||
func resolveNWNUserDirectory() (string, error) {
|
||||
for _, key := range []string{"SOW_NWN_USER_DIRECTORY", "NWN_HOME", "NWN_USER_DIRECTORY"} {
|
||||
func resolveNWNUserDirectory(envKeys []string) (string, error) {
|
||||
for _, key := range envKeys {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
path := filepath.Clean(expandUserPath(value))
|
||||
if err := os.MkdirAll(path, 0o755); err != nil {
|
||||
@@ -71,8 +73,8 @@ func defaultNWNUserDirectory() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func resolveNWNRoot() (string, error) {
|
||||
for _, key := range []string{"SOW_NWN_ROOT", "NWN_ROOT"} {
|
||||
func resolveNWNRoot(envKeys []string) (string, error) {
|
||||
for _, key := range envKeys {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
path := filepath.Clean(expandUserPath(value))
|
||||
if !looksLikeNWNInstall(path) {
|
||||
|
||||
Reference in New Issue
Block a user