335 lines
9.3 KiB
Go
335 lines
9.3 KiB
Go
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 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) {
|
|
if progress == nil {
|
|
progress = func(string) {}
|
|
}
|
|
nativeResult, err := BuildNative(p, 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) {}
|
|
}
|
|
buildDir := p.BuildDir()
|
|
outputHAK := filepath.Join(buildDir, PackageHAKFileName)
|
|
outputTLK := filepath.Join(buildDir, PackageTLKFileName)
|
|
|
|
progress("Packaging compiled topdata resources into sow_top.hak...")
|
|
resources, assetFiles, err := collectTopPackageResources(p, nativeResult.Output2DADir)
|
|
if err != nil {
|
|
return PackageResult{}, err
|
|
}
|
|
if err := writeHAK(outputHAK, resources); err != nil {
|
|
return PackageResult{}, err
|
|
}
|
|
|
|
progress("Preparing compiled TLK release output...")
|
|
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,
|
|
}, nil
|
|
}
|
|
|
|
func collectTopPackageResources(p *project.Project, compiled2DADir string) ([]erf.Resource, int, error) {
|
|
resourceByKey := map[string]erf.Resource{}
|
|
|
|
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
|
|
}
|
|
resourceByKey[topPackageResourceKey(resource)] = resource
|
|
}
|
|
|
|
assetFiles := 0
|
|
assetsDir := filepath.Join(p.TopDataSourceDir(), "assets")
|
|
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() {
|
|
return nil
|
|
}
|
|
if strings.HasPrefix(filepath.Base(path), ".") {
|
|
return nil
|
|
}
|
|
resource, err := topPackageResourceFromPath(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
key := topPackageResourceKey(resource)
|
|
if _, ok := resourceByKey[key]; ok {
|
|
return fmt.Errorf("topdata asset %s collides with generated top package resource %s", path, key)
|
|
}
|
|
resourceByKey[key] = resource
|
|
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 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 {
|
|
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 packagedBuildResult(p *project.Project) (BuildResult, error) {
|
|
if !p.HasTopData() {
|
|
return BuildResult{}, errors.New("topdata is not configured for this project")
|
|
}
|
|
|
|
buildDir := p.TopDataBuildDir()
|
|
output2DA := filepath.Join(buildDir, "2da")
|
|
outputTLK := filepath.Join(buildDir, "tlk")
|
|
if _, err := os.Stat(output2DA); err != nil {
|
|
return BuildResult{}, fmt.Errorf("topdata build output missing: run build-topdata first")
|
|
}
|
|
if _, err := os.Stat(outputTLK); 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, oldestTLK, err := countCompiledFiles(outputTLK, ".tlk")
|
|
if err != nil {
|
|
return BuildResult{}, err
|
|
}
|
|
if files2DA == 0 {
|
|
return BuildResult{}, fmt.Errorf("topdata build output missing: run build-topdata first")
|
|
}
|
|
if filesTLK != 1 {
|
|
return BuildResult{}, fmt.Errorf("expected exactly 1 compiled tlk output, found %d", filesTLK)
|
|
}
|
|
|
|
newestSource, newestPath, err := newestTopDataSource(p.TopDataSourceDir())
|
|
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 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
|
|
}
|
|
|
|
func newestTopDataSource(sourceDir string) (time.Time, string, error) {
|
|
newest := time.Time{}
|
|
newestPath := ""
|
|
templatesDir := filepath.Join(sourceDir, "templates")
|
|
err := filepath.WalkDir(sourceDir, func(path string, d fs.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if d.IsDir() && path == templatesDir {
|
|
return filepath.SkipDir
|
|
}
|
|
if d.IsDir() {
|
|
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
|
|
}
|