1package compose23import (4 "encoding/json"5 "fmt"6 "log/slog"7 "os"8 "path/filepath"9 "sort"1011 "github.com/FelineStateMachine/atlas/format/vnext"12 "github.com/FelineStateMachine/atlas/internal/generate/tiles"13 "github.com/FelineStateMachine/atlas/internal/logging"14)1516func revisionOf(options Options) int {17 if options.Revision != 0 {18 return options.Revision19 }20 return PolicyRevision21}2223// write builds exactly one native semantic bundle. The producer never creates24// the retired document-shaped archive, even as an intermediate artifact.25func write(options Options, worlds []composedWorld, icons map[string][]byte, log *slog.Logger) (Result, error) {26 semantic, rasterBlobs, tileCount, capturedAt, err := nativeVolume(options, worlds, icons)27 if err != nil {28 return Result{}, err29 }30 packed, err := vnext.Compile(semantic)31 if err != nil {32 return Result{}, fmt.Errorf("compile native volume: %w", err)33 }34 packed.Release.CreatedAt = capturedAt35 packed.Release.Revision = revisionOf(options)36 packed.Blobs = append(packed.Blobs, rasterBlobs...)3738 stageDir := options.BundleDir39 if stageDir == "" {40 stageDir = os.TempDir()41 } else if err := os.MkdirAll(stageDir, 0o755); err != nil {42 return Result{}, err43 }44 file, err := os.CreateTemp(stageDir, semantic.ID+".atlas.tmp-*")45 if err != nil {46 return Result{}, err47 }48 stage := file.Name()49 defer os.Remove(stage)50 if err := vnext.Write(file, packed); err != nil {51 file.Close()52 return Result{}, err53 }54 if err := file.Sync(); err != nil {55 file.Close()56 return Result{}, err57 }58 if err := file.Close(); err != nil {59 return Result{}, err60 }61 opened, err := vnext.OpenFile(stage, vnext.StandardSchema())62 if err != nil {63 return Result{}, fmt.Errorf("reopen native volume: %w", err)64 }65 if err := opened.Validate(); err != nil {66 opened.Close()67 return Result{}, fmt.Errorf("validate native volume: %w", err)68 }69 descriptor := opened.Descriptor()70 if err := opened.Close(); err != nil {71 return Result{}, err72 }7374 result := Result{75 Volume: semantic.ID, Stamp: descriptor.Stamp,76 File: vnext.VersionedFileName(semantic.ID, descriptorRelease(descriptor)),77 Worlds: len(worlds), Tiles: tileCount, Icons: len(icons),78 }79 if options.BundleDir == "" {80 log.Info("native volume composed", logging.Stamp(vnext.ShortStamp(result.Stamp)),81 "worlds", result.Worlds, "tiles", result.Tiles, "icons", result.Icons)82 return result, nil83 }8485 target := filepath.Join(options.BundleDir, result.File)86 result.Path = target87 if _, err := os.Stat(target); err == nil {88 result.Present = true89 log.Info("native build already installed", logging.Stamp(vnext.ShortStamp(result.Stamp)), logging.Path(result.File))90 return result, nil91 }92 if err := os.Chmod(stage, 0o644); err != nil {93 return Result{}, err94 }95 if err := os.Rename(stage, target); err != nil {96 return Result{}, err97 }98 log.Info("native bundle installed", logging.Stamp(vnext.ShortStamp(result.Stamp)),99 logging.Path(result.File), "worlds", result.Worlds, "tiles", result.Tiles, "icons", result.Icons)100 return result, nil101}102103func descriptorRelease(descriptor vnext.Descriptor) vnext.Release {104 return vnext.Release{105 Title: descriptor.Title, CreatedAt: descriptor.CreatedAt, Revision: descriptor.Revision,106 Stamp: descriptor.Stamp, Worlds: descriptor.Worlds,107 }108}109110// WriteRegistryIndex writes a derived convenience listing. It has no authority:111// deleting it is harmless because native release metadata and the directory112// fold remain the registry.113func WriteRegistryIndex(dir string) error {114 descriptors, _, err := vnext.Scan(dir, vnext.StandardSchema())115 if err != nil {116 return err117 }118 sort.Slice(descriptors, func(i, j int) bool {119 if descriptors[i].Slug != descriptors[j].Slug {120 return descriptors[i].Slug < descriptors[j].Slug121 }122 return vnext.Newer(descriptors[i], descriptors[j])123 })124 type build struct {125 File string `json:"file"`126 Slug string `json:"slug"`127 Title string `json:"title"`128 Stamp string `json:"stamp"`129 CreatedAt string `json:"createdAt"`130 Revision int `json:"revision,omitempty"`131 Size int64 `json:"size"`132 Worlds int `json:"worlds"`133 }134 listed := make([]build, 0, len(descriptors))135 for _, descriptor := range descriptors {136 listed = append(listed, build{137 File: filepath.Base(descriptor.Locator), Slug: descriptor.Slug, Title: descriptor.Title,138 Stamp: descriptor.Stamp, CreatedAt: descriptor.CreatedAt, Revision: descriptor.Revision,139 Size: descriptor.Size, Worlds: descriptor.Worlds,140 })141 }142 data, err := json.Marshal(struct {143 Builds []build `json:"builds"`144 }{Builds: listed})145 if err != nil {146 return err147 }148 data = append(data, '\n')149 stage, err := os.CreateTemp(dir, ".index-*")150 if err != nil {151 return err152 }153 name := stage.Name()154 defer os.Remove(name)155 if _, err := stage.Write(data); err != nil {156 stage.Close()157 return err158 }159 if err := stage.Close(); err != nil {160 return err161 }162 return os.Rename(name, filepath.Join(dir, "index.json"))163}164165func rasterBlobList(set *tiles.Set, pyramids map[string]tiles.Pyramid) ([]vnext.Blob, int, error) {166 locals := make([]string, 0, len(pyramids))167 for local := range pyramids {168 locals = append(locals, local)169 }170 sort.Strings(locals)171 var blobs []vnext.Blob172 for _, local := range locals {173 list, err := set.Tiles(pyramids[local])174 if err != nil {175 return nil, 0, err176 }177 for _, tile := range list {178 blobs = append(blobs, vnext.Blob{Name: "tiles/" + local + "/" + tile.Name, Path: tile.Path})179 }180 }181 return blobs, len(blobs), nil182}