// Package assets is the crucible `assets` builder: NWN:EE model/texture tools // that operate in place on a target directory. Mirrors internal/depot's // Run(args, stdout, stderr, getenv) shape and bypasses the legacy internal/app // surface entirely. package assets import ( "os" "os/exec" "path/filepath" "sort" "strings" ) // runner is the single external-command indirection so tests can observe and // stub every engine / ImageMagick / upscaler invocation. dir is the working // directory ("" = inherit); env replaces the child environment when non-nil. var runner = func(dir string, env []string, name string, args ...string) ([]byte, error) { cmd := exec.Command(name, args...) cmd.Dir = dir if env != nil { cmd.Env = env } return cmd.CombinedOutput() } // walk collects files under each root whose lower-cased extension is in exts. A // root that is itself a matching file is included. recursive controls descent // into subdirectories. Results are sorted and deduplicated. func walk(roots []string, exts map[string]bool, recursive bool) ([]string, error) { seen := map[string]bool{} var out []string add := func(p string) { if exts[strings.ToLower(filepath.Ext(p))] && !seen[p] { seen[p] = true out = append(out, p) } } for _, root := range roots { fi, err := os.Stat(root) if err != nil { return nil, err } if !fi.IsDir() { add(root) continue } err = filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { if !recursive && p != root { return filepath.SkipDir } return nil } add(p) return nil }) if err != nil { return nil, err } } sort.Strings(out) return out, nil } // look returns the first bare name found on PATH, or the first candidate that // is an existing path. Returns "" if none resolve. func look(candidates ...string) string { for _, c := range candidates { if strings.ContainsRune(c, filepath.Separator) { if fi, err := os.Stat(c); err == nil && !fi.IsDir() { return c } continue } if p, err := exec.LookPath(c); err == nil { return p } } return "" }