Phase 5: scaffold Crucible (sow-tools) — dispatcher + builder shims, container, PR-first CI, fail-closed (D11, D19).
This commit is contained in:
@@ -1,713 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
|
||||
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
||||
)
|
||||
|
||||
const (
|
||||
PackageHAKResRef = "sow_top"
|
||||
PackageHAKFileName = PackageHAKResRef + ".hak"
|
||||
PackageTLKFileName = defaultTLKName
|
||||
)
|
||||
|
||||
func compiled2DAOutputDir(p *project.Project) string {
|
||||
return p.TopDataCompiled2DADir()
|
||||
}
|
||||
|
||||
func topDataBuildUsesRepoCache(p *project.Project) bool {
|
||||
return p.TopDataUsesRepoCache()
|
||||
}
|
||||
|
||||
func compiledTLKOutputPath(p *project.Project) string {
|
||||
return p.TopDataCompiledTLKPath()
|
||||
}
|
||||
|
||||
func compiledTLKOutputDir(p *project.Project) string {
|
||||
return p.TopDataCompiledTLKDir()
|
||||
}
|
||||
|
||||
func wikiOutputRootDir(p *project.Project) string {
|
||||
return p.TopDataWikiRootDir()
|
||||
}
|
||||
|
||||
func wikiOutputPagesDir(p *project.Project) string {
|
||||
return p.TopDataWikiPagesDir()
|
||||
}
|
||||
|
||||
func wikiOutputStatePath(p *project.Project) string {
|
||||
return p.TopDataWikiStatePath()
|
||||
}
|
||||
|
||||
func legacyCompiled2DAOutputDir(p *project.Project) string {
|
||||
return filepath.Join(p.TopDataSourceDir(), "assets", "2da")
|
||||
}
|
||||
|
||||
func legacyWikiOutputRootDir(p *project.Project) string {
|
||||
return filepath.Join(p.TopDataSourceDir(), legacyWikiRootDirName)
|
||||
}
|
||||
|
||||
func BuildPackage(p *project.Project, progress func(string)) (PackageResult, error) {
|
||||
if progress == nil {
|
||||
progress = func(string) {}
|
||||
}
|
||||
nativeResult, err := packagedBuildResult(p)
|
||||
if err != nil {
|
||||
return PackageResult{}, err
|
||||
}
|
||||
return packageBuiltTopData(p, nativeResult, progress)
|
||||
}
|
||||
|
||||
func BuildAndPackage(p *project.Project, progress func(string)) (PackageResult, error) {
|
||||
return BuildAndPackageWithOptions(p, BuildAndPackageOptions{}, progress)
|
||||
}
|
||||
|
||||
type BuildAndPackageOptions struct {
|
||||
Force bool
|
||||
BuildWiki bool
|
||||
}
|
||||
|
||||
func BuildAndPackageWithOptions(p *project.Project, opts BuildAndPackageOptions, progress func(string)) (PackageResult, error) {
|
||||
if progress == nil {
|
||||
progress = func(string) {}
|
||||
}
|
||||
if !opts.Force {
|
||||
incrementalResult, ok, err := currentTopPackageResult(p)
|
||||
if err != nil {
|
||||
return PackageResult{}, err
|
||||
}
|
||||
if ok {
|
||||
progress("Topdata outputs are current; skipping native build and top package refresh.")
|
||||
return incrementalResult, nil
|
||||
}
|
||||
|
||||
nativeResult, ok, err := currentCompiledBuildResult(p)
|
||||
if err != nil {
|
||||
return PackageResult{}, err
|
||||
}
|
||||
if ok {
|
||||
progress(fmt.Sprintf("Compiled topdata outputs are current; refreshing %s and %s only.", p.TopDataPackageHAKName(), p.TopDataPackageTLKName()))
|
||||
return packageBuiltTopData(p, nativeResult, progress)
|
||||
}
|
||||
}
|
||||
|
||||
return buildAndPackageFull(p, opts, progress)
|
||||
}
|
||||
|
||||
func buildAndPackageFull(p *project.Project, opts BuildAndPackageOptions, progress func(string)) (PackageResult, error) {
|
||||
nativeResult, err := BuildNativeWithOptions(p, NativeBuildOptions{BuildWiki: opts.BuildWiki, Force: opts.Force}, progress)
|
||||
if err != nil {
|
||||
return PackageResult{}, err
|
||||
}
|
||||
return packageBuiltTopData(p, nativeResult, progress)
|
||||
}
|
||||
|
||||
func BuildPackageFromBuild(p *project.Project, nativeResult BuildResult, progress func(string)) (PackageResult, error) {
|
||||
if progress == nil {
|
||||
progress = func(string) {}
|
||||
}
|
||||
return packageBuiltTopData(p, nativeResult, progress)
|
||||
}
|
||||
|
||||
func packageBuiltTopData(p *project.Project, nativeResult BuildResult, progress func(string)) (PackageResult, error) {
|
||||
if progress == nil {
|
||||
progress = func(string) {}
|
||||
}
|
||||
outputHAK := p.TopDataPackageHAKPath()
|
||||
outputTLK := p.TopDataPackageTLKPath()
|
||||
|
||||
progress(fmt.Sprintf("Packaging compiled topdata resources into %s...", filepath.Base(outputHAK)))
|
||||
resources, assetFiles, err := collectTopPackageResources(p, nativeResult.Output2DADir)
|
||||
if err != nil {
|
||||
return PackageResult{}, err
|
||||
}
|
||||
if err := writeHAK(outputHAK, resources); err != nil {
|
||||
return PackageResult{}, err
|
||||
}
|
||||
|
||||
progress(fmt.Sprintf("Preparing compiled TLK release output %s...", filepath.Base(outputTLK)))
|
||||
sourceTLK := compiledTLKOutputPath(p)
|
||||
if nativeResult.OutputTLKDir != "" {
|
||||
sourceTLK, err = resolveCompiledTLKPath(nativeResult.OutputTLKDir)
|
||||
if err != nil {
|
||||
return PackageResult{}, err
|
||||
}
|
||||
}
|
||||
if err := copyFile(outputTLK, sourceTLK); err != nil {
|
||||
return PackageResult{}, err
|
||||
}
|
||||
|
||||
return PackageResult{
|
||||
Mode: nativeResult.Mode,
|
||||
Output2DADir: nativeResult.Output2DADir,
|
||||
OutputTLKDir: nativeResult.OutputTLKDir,
|
||||
OutputHAKPath: outputHAK,
|
||||
OutputTLKPath: outputTLK,
|
||||
Files2DA: nativeResult.Files2DA,
|
||||
FilesTLK: nativeResult.FilesTLK,
|
||||
HAKResources: len(resources),
|
||||
AssetFiles: assetFiles,
|
||||
WikiOutputDir: nativeResult.WikiOutputDir,
|
||||
WikiPages: nativeResult.WikiPages,
|
||||
WikiStatus: nativeResult.WikiStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func collectTopPackageResources(p *project.Project, compiled2DADir string) ([]erf.Resource, int, error) {
|
||||
resourceByKey := map[string]erf.Resource{}
|
||||
resourceSourceByKey := map[string]string{}
|
||||
|
||||
entries, err := os.ReadDir(compiled2DADir)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("read compiled 2da output dir: %w", err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".2da") {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(compiled2DADir, entry.Name())
|
||||
resource, err := topPackageResourceFromPath(path)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
key := topPackageResourceKey(resource)
|
||||
resourceByKey[key] = resource
|
||||
resourceSourceByKey[key] = path
|
||||
}
|
||||
|
||||
assetFiles := 0
|
||||
assetsDir := filepath.Join(p.TopDataSourceDir(), "assets")
|
||||
skipDirs := map[string]struct{}{
|
||||
filepath.Clean(compiled2DADir): {},
|
||||
filepath.Clean(legacyCompiled2DAOutputDir(p)): {},
|
||||
}
|
||||
info, err := os.Stat(assetsDir)
|
||||
if err == nil && info.IsDir() {
|
||||
err = filepath.WalkDir(assetsDir, func(path string, d os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if d.IsDir() {
|
||||
if _, ok := skipDirs[filepath.Clean(path)]; ok {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(filepath.Base(path), ".") {
|
||||
return nil
|
||||
}
|
||||
resource, err := topPackageResourceFromPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := topPackageResourceKey(resource)
|
||||
if existing, ok := resourceSourceByKey[key]; ok {
|
||||
return fmt.Errorf("topdata asset %s collides with %s for top package resource %s", path, existing, key)
|
||||
}
|
||||
resourceByKey[key] = resource
|
||||
resourceSourceByKey[key] = path
|
||||
assetFiles++
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
} else if err != nil && !os.IsNotExist(err) {
|
||||
return nil, 0, fmt.Errorf("stat topdata assets dir: %w", err)
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(resourceByKey))
|
||||
for key := range resourceByKey {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
|
||||
resources := make([]erf.Resource, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
resources = append(resources, resourceByKey[key])
|
||||
}
|
||||
return resources, assetFiles, nil
|
||||
}
|
||||
|
||||
func shouldSkipTopDataSourceDir(path string, skipDirs map[string]struct{}) bool {
|
||||
_, ok := skipDirs[filepath.Clean(path)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func topDataSourceSkipDirs(p *project.Project) map[string]struct{} {
|
||||
return map[string]struct{}{
|
||||
filepath.Clean(filepath.Join(p.TopDataSourceDir(), "templates")): {},
|
||||
filepath.Clean(legacyWikiOutputRootDir(p)): {},
|
||||
filepath.Clean(legacyCompiled2DAOutputDir(p)): {},
|
||||
filepath.Clean(wikiOutputRootDir(p)): {},
|
||||
}
|
||||
}
|
||||
|
||||
func newestTopDataSource(p *project.Project) (time.Time, string, error) {
|
||||
newest := time.Time{}
|
||||
newestPath := ""
|
||||
skipDirs := topDataSourceSkipDirs(p)
|
||||
sourceDir := p.TopDataSourceDir()
|
||||
err := filepath.WalkDir(sourceDir, func(path string, d fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if d.IsDir() && shouldSkipTopDataSourceDir(path, skipDirs) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if path == sourceDir {
|
||||
return nil
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if newest.IsZero() || info.ModTime().After(newest) {
|
||||
newest = info.ModTime()
|
||||
newestPath = path
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return time.Time{}, "", fmt.Errorf("scan topdata sources: %w", err)
|
||||
}
|
||||
return newest, newestPath, nil
|
||||
}
|
||||
|
||||
func newestTopDataInput(p *project.Project, now time.Time) (time.Time, string, error) {
|
||||
newest, newestPath, err := newestTopDataSource(p)
|
||||
if err != nil {
|
||||
return time.Time{}, "", err
|
||||
}
|
||||
autogenTime, autogenPath, err := newestAutogenInput(p, now)
|
||||
if err != nil {
|
||||
return time.Time{}, "", err
|
||||
}
|
||||
if autogenTime.After(newest) {
|
||||
return autogenTime, autogenPath, nil
|
||||
}
|
||||
return newest, newestPath, nil
|
||||
}
|
||||
|
||||
func newestAutogenInput(p *project.Project, now time.Time) (time.Time, string, error) {
|
||||
newest := time.Time{}
|
||||
newestPath := ""
|
||||
for _, consumer := range p.Config.Autogen.Consumers {
|
||||
overrideRoot, err := resolveAutogenLocalOverrideRoot(p, consumer)
|
||||
if err != nil {
|
||||
return time.Time{}, "", err
|
||||
}
|
||||
if overrideRoot != "" {
|
||||
candidateRoot := filepath.Join(overrideRoot, consumer.Root)
|
||||
candidateTime, candidatePath, err := newestMatchingAutogenOverrideInput(candidateRoot, consumer.Include)
|
||||
if err != nil {
|
||||
return time.Time{}, "", err
|
||||
}
|
||||
if candidateTime.After(newest) {
|
||||
newest = candidateTime
|
||||
newestPath = candidatePath
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
cachePath := p.AutogenCachePath(consumer.Manifest.CacheName)
|
||||
candidateTime, candidatePath, err := newestReleasedAutogenManifestInput(p, consumer, now, cachePath)
|
||||
if err != nil {
|
||||
return time.Time{}, "", err
|
||||
}
|
||||
if candidateTime.After(newest) {
|
||||
newest = candidateTime
|
||||
newestPath = candidatePath
|
||||
}
|
||||
}
|
||||
return newest, newestPath, nil
|
||||
}
|
||||
|
||||
func newestMatchingAutogenOverrideInput(scanRoot string, include []string) (time.Time, string, error) {
|
||||
info, err := os.Stat(scanRoot)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return time.Time{}, "", nil
|
||||
}
|
||||
return time.Time{}, "", fmt.Errorf("stat autogen override root %s: %w", scanRoot, err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return time.Time{}, "", fmt.Errorf("autogen override root %s is not a directory", scanRoot)
|
||||
}
|
||||
|
||||
newest := time.Time{}
|
||||
newestPath := ""
|
||||
err = filepath.WalkDir(scanRoot, func(path string, d fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
rel, err := filepath.Rel(scanRoot, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !matchesAutogenInclude(filepath.ToSlash(rel), include) {
|
||||
return nil
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.ModTime().After(newest) {
|
||||
newest = info.ModTime()
|
||||
newestPath = path
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return time.Time{}, "", fmt.Errorf("scan autogen override %s: %w", scanRoot, err)
|
||||
}
|
||||
return newest, newestPath, nil
|
||||
}
|
||||
|
||||
func topPackageResourceFromPath(path string) (erf.Resource, error) {
|
||||
extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(path)), ".")
|
||||
resourceType, ok := erf.HAKResourceTypeForExtension(extension)
|
||||
if !ok {
|
||||
return erf.Resource{}, fmt.Errorf("unsupported top package HAK resource extension %q", filepath.Ext(path))
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return erf.Resource{}, fmt.Errorf("stat %s: %w", path, err)
|
||||
}
|
||||
return erf.Resource{
|
||||
Name: strings.ToLower(strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))),
|
||||
Type: resourceType,
|
||||
SourcePath: path,
|
||||
Size: info.Size(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func topPackageResourceKey(resource erf.Resource) string {
|
||||
extension, ok := erf.ExtensionForResourceType(resource.Type)
|
||||
if !ok {
|
||||
return strings.ToLower(resource.Name)
|
||||
}
|
||||
return strings.ToLower(resource.Name) + "." + strings.TrimPrefix(strings.ToLower(extension), ".")
|
||||
}
|
||||
|
||||
func writeHAK(path string, resources []erf.Resource) error {
|
||||
output, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create top package hak: %w", err)
|
||||
}
|
||||
defer output.Close()
|
||||
if err := erf.Write(output, erf.New("HAK ", resources)); err != nil {
|
||||
return fmt.Errorf("write top package hak: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveCompiledTLKPath(outputDir string) (string, error) {
|
||||
entries, err := os.ReadDir(outputDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read compiled tlk output dir: %w", err)
|
||||
}
|
||||
var tlkFiles []string
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".tlk") {
|
||||
continue
|
||||
}
|
||||
tlkFiles = append(tlkFiles, filepath.Join(outputDir, entry.Name()))
|
||||
}
|
||||
if len(tlkFiles) != 1 {
|
||||
return "", fmt.Errorf("expected exactly 1 compiled tlk output, found %d", len(tlkFiles))
|
||||
}
|
||||
return tlkFiles[0], nil
|
||||
}
|
||||
|
||||
func copyFile(dst, src string) error {
|
||||
if samePath(dst, src) {
|
||||
return nil
|
||||
}
|
||||
input, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open %s: %w", src, err)
|
||||
}
|
||||
defer input.Close()
|
||||
|
||||
output, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create %s: %w", dst, err)
|
||||
}
|
||||
defer output.Close()
|
||||
|
||||
if _, err := io.Copy(output, input); err != nil {
|
||||
return fmt.Errorf("copy %s to %s: %w", src, dst, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func samePath(left, right string) bool {
|
||||
leftAbs, leftErr := filepath.Abs(left)
|
||||
rightAbs, rightErr := filepath.Abs(right)
|
||||
if leftErr != nil || rightErr != nil {
|
||||
return left == right
|
||||
}
|
||||
return leftAbs == rightAbs
|
||||
}
|
||||
|
||||
func packagedBuildResult(p *project.Project) (BuildResult, error) {
|
||||
if !p.HasTopData() {
|
||||
return BuildResult{}, errors.New("topdata is not configured for this project")
|
||||
}
|
||||
|
||||
output2DA := compiled2DAOutputDir(p)
|
||||
outputTLK := compiledTLKOutputDir(p)
|
||||
if _, err := os.Stat(output2DA); err != nil {
|
||||
return BuildResult{}, fmt.Errorf("topdata build output missing: run build-topdata first")
|
||||
}
|
||||
if _, err := os.Stat(compiledTLKOutputPath(p)); err != nil {
|
||||
return BuildResult{}, fmt.Errorf("topdata tlk output missing: run build-topdata first")
|
||||
}
|
||||
|
||||
files2DA, oldest2DA, err := countCompiledFiles(output2DA, ".2da")
|
||||
if err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
filesTLK := 1
|
||||
tlkInfo, err := os.Stat(compiledTLKOutputPath(p))
|
||||
if err != nil {
|
||||
return BuildResult{}, fmt.Errorf("stat compiled tlk output %s: %w", compiledTLKOutputPath(p), err)
|
||||
}
|
||||
oldestTLK := tlkInfo.ModTime()
|
||||
if files2DA == 0 {
|
||||
return BuildResult{}, fmt.Errorf("topdata build output missing: run build-topdata first")
|
||||
}
|
||||
if err := validateCompiled2DAOutputCatalog(p, output2DA); err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
|
||||
newestSource, newestPath, err := newestTopDataInput(p, time.Now())
|
||||
if err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
oldestBuild := oldest2DA
|
||||
if oldestBuild.IsZero() || (!oldestTLK.IsZero() && oldestTLK.Before(oldestBuild)) {
|
||||
oldestBuild = oldestTLK
|
||||
}
|
||||
if !newestSource.IsZero() && !oldestBuild.IsZero() && newestSource.After(oldestBuild) {
|
||||
return BuildResult{}, fmt.Errorf("topdata build output is older than source file %s; run build-topdata first", newestPath)
|
||||
}
|
||||
|
||||
return BuildResult{
|
||||
Mode: "native",
|
||||
Output2DADir: output2DA,
|
||||
OutputTLKDir: outputTLK,
|
||||
Files2DA: files2DA,
|
||||
FilesTLK: filesTLK,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateCompiled2DAOutputCatalog(p *project.Project, output2DA string) error {
|
||||
expected, err := expectedCompiled2DAOutputs(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actual, err := compiled2DAOutputNames(output2DA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
missing := make([]string, 0)
|
||||
for outputName := range expected {
|
||||
if _, ok := actual[outputName]; !ok {
|
||||
missing = append(missing, outputName)
|
||||
}
|
||||
}
|
||||
stale := make([]string, 0)
|
||||
for outputName := range actual {
|
||||
if _, ok := expected[outputName]; !ok {
|
||||
stale = append(stale, outputName)
|
||||
}
|
||||
}
|
||||
slices.Sort(missing)
|
||||
slices.Sort(stale)
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("topdata build output is missing compiled 2da %s; run build-topdata first", strings.Join(missing, ", "))
|
||||
}
|
||||
if len(stale) > 0 {
|
||||
return fmt.Errorf("topdata build output contains stale compiled 2da %s; run build-topdata first", strings.Join(stale, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func expectedCompiled2DAOutputs(p *project.Project) (map[string]struct{}, error) {
|
||||
dataDir := filepath.Join(p.TopDataSourceDir(), "data")
|
||||
datasets, err := discoverNativeDatasets(dataDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
registryDatasets, err := collectGeneratedRegistryDatasets(dataDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outputs := make(map[string]struct{}, len(datasets)+len(registryDatasets))
|
||||
for _, dataset := range datasets {
|
||||
outputs[dataset.OutputName] = struct{}{}
|
||||
}
|
||||
for _, dataset := range registryDatasets {
|
||||
outputs[dataset.Dataset.OutputName] = struct{}{}
|
||||
}
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
func compiled2DAOutputNames(output2DA string) (map[string]struct{}, error) {
|
||||
entries, err := os.ReadDir(output2DA)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read compiled 2da output dir %s: %w", output2DA, err)
|
||||
}
|
||||
outputs := make(map[string]struct{})
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".2da") {
|
||||
continue
|
||||
}
|
||||
outputs[entry.Name()] = struct{}{}
|
||||
}
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
func currentCompiledBuildResult(p *project.Project) (BuildResult, bool, error) {
|
||||
nativeResult, err := packagedBuildResult(p)
|
||||
if err == nil {
|
||||
return nativeResult, true, nil
|
||||
}
|
||||
if strings.Contains(err.Error(), "run build-topdata first") {
|
||||
return BuildResult{}, false, nil
|
||||
}
|
||||
return BuildResult{}, false, err
|
||||
}
|
||||
|
||||
func currentTopPackageResult(p *project.Project) (PackageResult, bool, error) {
|
||||
nativeResult, ok, err := currentCompiledBuildResult(p)
|
||||
if err != nil || !ok {
|
||||
return PackageResult{}, ok, err
|
||||
}
|
||||
|
||||
outputHAK := p.TopDataPackageHAKPath()
|
||||
outputTLK := p.TopDataPackageTLKPath()
|
||||
hakInfo, err := os.Stat(outputHAK)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return PackageResult{}, false, nil
|
||||
}
|
||||
return PackageResult{}, false, fmt.Errorf("stat top package hak %s: %w", outputHAK, err)
|
||||
}
|
||||
tlkInfo, err := os.Stat(outputTLK)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return PackageResult{}, false, nil
|
||||
}
|
||||
return PackageResult{}, false, fmt.Errorf("stat top package tlk %s: %w", outputTLK, err)
|
||||
}
|
||||
|
||||
newestSource, _, err := newestTopDataInput(p, time.Now())
|
||||
if err != nil {
|
||||
return PackageResult{}, false, err
|
||||
}
|
||||
newestCompiled, err := newestFileModTime(nativeResult.Output2DADir, ".2da")
|
||||
if err != nil {
|
||||
return PackageResult{}, false, err
|
||||
}
|
||||
compiledTLKInfo, err := os.Stat(compiledTLKOutputPath(p))
|
||||
if err != nil {
|
||||
return PackageResult{}, false, fmt.Errorf("stat compiled tlk output %s: %w", compiledTLKOutputPath(p), err)
|
||||
}
|
||||
if compiledTLKInfo.ModTime().After(newestCompiled) {
|
||||
newestCompiled = compiledTLKInfo.ModTime()
|
||||
}
|
||||
newestInput := newestSource
|
||||
if newestCompiled.After(newestInput) {
|
||||
newestInput = newestCompiled
|
||||
}
|
||||
|
||||
oldestPackage := hakInfo.ModTime()
|
||||
if tlkInfo.ModTime().Before(oldestPackage) {
|
||||
oldestPackage = tlkInfo.ModTime()
|
||||
}
|
||||
if !newestInput.IsZero() && newestInput.After(oldestPackage) {
|
||||
return PackageResult{}, false, nil
|
||||
}
|
||||
|
||||
resources, assetFiles, err := collectTopPackageResources(p, nativeResult.Output2DADir)
|
||||
if err != nil {
|
||||
return PackageResult{}, false, err
|
||||
}
|
||||
return PackageResult{
|
||||
Mode: "incremental-noop",
|
||||
Output2DADir: nativeResult.Output2DADir,
|
||||
OutputTLKDir: nativeResult.OutputTLKDir,
|
||||
OutputHAKPath: outputHAK,
|
||||
OutputTLKPath: outputTLK,
|
||||
Files2DA: nativeResult.Files2DA,
|
||||
FilesTLK: nativeResult.FilesTLK,
|
||||
HAKResources: len(resources),
|
||||
AssetFiles: assetFiles,
|
||||
WikiOutputDir: nativeResult.WikiOutputDir,
|
||||
WikiPages: nativeResult.WikiPages,
|
||||
WikiStatus: nativeResult.WikiStatus,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func newestFileModTime(dir, extension string) (time.Time, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("read output dir %s: %w", dir, err)
|
||||
}
|
||||
newest := time.Time{}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), extension) {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("stat output %s: %w", filepath.Join(dir, entry.Name()), err)
|
||||
}
|
||||
if newest.IsZero() || info.ModTime().After(newest) {
|
||||
newest = info.ModTime()
|
||||
}
|
||||
}
|
||||
return newest, nil
|
||||
}
|
||||
|
||||
func countCompiledFiles(dir, extension string) (int, time.Time, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return 0, time.Time{}, fmt.Errorf("read compiled output dir %s: %w", dir, err)
|
||||
}
|
||||
count := 0
|
||||
oldest := time.Time{}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), extension) {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return 0, time.Time{}, fmt.Errorf("stat compiled output %s: %w", filepath.Join(dir, entry.Name()), err)
|
||||
}
|
||||
count++
|
||||
if oldest.IsZero() || info.ModTime().Before(oldest) {
|
||||
oldest = info.ModTime()
|
||||
}
|
||||
}
|
||||
return count, oldest, nil
|
||||
}
|
||||
Reference in New Issue
Block a user