Files
aego/tools/gendocs/tryassets/prepare.go
sanspie 5eb1832907
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 15m28s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 7m47s
Complete Stage 12 for v0.12.0
2026-11-05 11:46:53 +03:00

198 lines
5.7 KiB
Go

// Package tryassets prepares documentation resources at build time. Browser
// sessions decode AETB and use the ordinary loader; they never import sources.
package tryassets
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"fmt"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"aego/asset"
"aego/asset/importers/atlas"
matimp "aego/asset/importers/material"
shaderimp "aego/asset/importers/shader"
teximp "aego/asset/importers/texture"
"aego/asset/pipeline"
"aego/core/ids"
"aego/core/log"
"aego/project"
"aego/render/shader"
"aego/render/shaderx"
)
// Prepare imports a bounded source project copied to an isolated temporary
// directory. Existing .meta IDs are mandatory and the source tree is read-only.
func Prepare(source string) ([]byte, error) {
root, err := filepath.Abs(source)
if err != nil {
return nil, err
}
tmp, err := os.MkdirTemp("", "aego-try-assets-")
if err != nil {
return nil, err
}
defer os.RemoveAll(tmp)
hash := sha256.New()
fmt.Fprintf(hash, "AETB=%d;pipeline=%d;texture=%d;atlas=%d;material=%d;shader=%d;include=%d;generator=%d;abi=%d\n", asset.TryArtifactsVersion, pipeline.PipelineVersion, teximp.Version, atlas.GroupVersion, matimp.Version, shaderimp.Version, shaderimp.IncludeVersion, shaderx.GeneratorVersion, shader.ABI)
count, total := 0, 0
err = filepath.WalkDir(root, func(name string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
rel, err := filepath.Rel(root, name)
if err != nil {
return err
}
if entry.Type()&fs.ModeSymlink != 0 {
return fmt.Errorf("try source cannot contain symlinks: %s", rel)
}
if entry.IsDir() {
if rel != "." && strings.HasPrefix(entry.Name(), ".") {
return filepath.SkipDir
}
return os.MkdirAll(filepath.Join(tmp, rel), 0o755)
}
info, err := entry.Info()
if err != nil {
return err
}
count++
if !info.Mode().IsRegular() || count > 256 || info.Size() > asset.MaxTryArtifactBytes {
return fmt.Errorf("try source file budget exceeded: %s", rel)
}
data, err := os.ReadFile(name)
if err != nil {
return err
}
total += len(data)
if total > asset.MaxTryArtifactsBytes {
return fmt.Errorf("try source byte budget exceeded")
}
path := filepath.ToSlash(rel)
var lengths [16]byte
binary.LittleEndian.PutUint64(lengths[:8], uint64(len(path)))
binary.LittleEndian.PutUint64(lengths[8:], uint64(len(data)))
hash.Write(lengths[:])
hash.Write([]byte(path))
hash.Write(data)
return os.WriteFile(filepath.Join(tmp, rel), data, 0o644)
})
if err != nil {
return nil, err
}
p, err := project.Load(tmp)
if err != nil {
return nil, err
}
imps := pipeline.NewImporterRegistry()
for _, imp := range []pipeline.Importer{atlas.Wrap(teximp.New()), matimp.New(), shaderimp.New(), shaderimp.NewInclude()} {
if err := imps.Register(imp); err != nil {
return nil, err
}
}
pl, err := pipeline.Open(p, imps, log.Nop())
if err != nil {
return nil, err
}
defer pl.Close()
pl.SetMetaMode(pipeline.MetaReadOnly)
if _, _, err := pl.Scan(); err != nil {
return nil, err
}
if issues, _ := pl.MetaIssues(); len(issues) > 0 {
return nil, fmt.Errorf("try source metadata must be complete before preparation: %s", issues[0].Message())
}
batch, err := pl.ImportAll()
if err != nil {
return nil, err
}
if len(batch.Failed) > 0 {
var messages []string
for id, err := range batch.Failed {
messages = append(messages, fmt.Sprintf("%s: %v", id, err))
}
slices.Sort(messages)
return nil, fmt.Errorf("try source import failed: %s", strings.Join(messages, "; "))
}
bundle := asset.TryArtifacts{}
var buildErr error
builtins := map[ids.AssetID]bool{}
pl.Index().Each(func(entry *asset.Entry) {
if buildErr != nil || entry.Kind == asset.ArtShaderInclude {
return
}
if entry.Missing || entry.Error != "" {
buildErr = fmt.Errorf("try asset %s is unavailable: %s", entry.ID, entry.Error)
return
}
out := asset.TryAsset{ID: entry.ID, Kind: entry.Kind, Name: entry.Name}
for _, dep := range entry.Deps {
if e, ok := pl.Index().Lookup(dep); ok && e.Kind == asset.ArtShaderInclude {
continue
}
out.Deps = append(out.Deps, dep)
if asset.IsBuiltin(dep) {
builtins[dep] = true
}
}
for _, artifact := range entry.Artifacts {
data, err := pl.Artifacts().ReadFile(artifact.Path)
if err != nil {
buildErr = err
return
}
out.Artifacts = append(out.Artifacts, asset.TryArtifact{Kind: artifact.Kind, Name: artifact.Name, Data: data})
}
bundle.Assets = append(bundle.Assets, out)
})
if buildErr != nil {
return nil, buildErr
}
var builtinIDs []ids.AssetID
for id := range builtins {
builtinIDs = append(builtinIDs, id)
}
slices.SortFunc(builtinIDs, func(a, b ids.AssetID) int { return strings.Compare(a.String(), b.String()) })
for _, id := range builtinIDs {
hash.Write([]byte(id.String()))
if data, ok := asset.BuiltinShader(id); ok {
hash.Write(data)
}
if data, ok := asset.BuiltinMaterial(id); ok {
hash.Write(data)
}
}
copy(bundle.SourceHash[:], hash.Sum(nil))
return asset.EncodeTryArtifacts(&bundle)
}
// Generate writes or verifies X.aetry next to X.assets. A fresh import in check
// mode detects source/meta, importer, ABI and embedded shader changes.
func Generate(source string, check bool) (string, error) {
if !strings.HasSuffix(source, ".assets") {
return "", fmt.Errorf("try source project must end in .assets")
}
data, err := Prepare(source)
if err != nil {
return "", err
}
target := strings.TrimSuffix(source, ".assets") + ".aetry"
if check {
existing, err := os.ReadFile(target)
if err != nil {
return "", err
}
if !bytes.Equal(existing, data) {
return "", fmt.Errorf("%s is stale; run gendocs -prepare-try %s", target, source)
}
return target, nil
}
return target, os.WriteFile(target, data, 0o644)
}