cli/internal/bnp/pack.go
Alex Dunmow 2f065e3ed4 feat: ninja CLI in its own repo (block/cli, WO-WZ-023)
The ninja developer CLI moves out of block/core into its own module
git.dev.alexdunmow.com/block/cli. Pins block/core@v0.18.2 for abi/v1, the
plugin.mod parser, and DESCRIBE guest builds; vendors its own orchestrator
registry client (internal/api, generated from a vendored plugin_registry.proto)
since it cannot import block/core/internal. Produces byte-identical v1
artifacts (verified: art-deco codeless SHA256-identical to the pre-move build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 22:39:32 +08:00

139 lines
3.6 KiB
Go

package bnp
import (
"archive/tar"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/klauspost/compress/zstd"
)
// packEntry is one file destined for the artifact: its path inside the .bnp
// (always forward-slash relative) and either literal Data or a Source file to
// stream from.
type packEntry struct {
ArtifactPath string
Data []byte
Source string
}
// packArtifact writes entries as a zstd-compressed tar to outPath. Entries are
// sorted by artifact path for a deterministic archive. Directory entries are
// synthesized as needed so the reader's dirExists checks fire for
// migrations/, schemas/, assets/, web/.
func packArtifact(outPath string, entries []packEntry) (int64, error) {
sort.Slice(entries, func(i, j int) bool { return entries[i].ArtifactPath < entries[j].ArtifactPath })
out, err := os.Create(outPath)
if err != nil {
return 0, fmt.Errorf("create %s: %w", outPath, err)
}
defer func() { _ = out.Close() }()
enc, err := zstd.NewWriter(out, zstd.WithEncoderLevel(zstd.SpeedDefault))
if err != nil {
return 0, err
}
tw := tar.NewWriter(enc)
seenDir := map[string]bool{}
writeDir := func(dir string) error {
if dir == "" || dir == "." || seenDir[dir] {
return nil
}
seenDir[dir] = true
return tw.WriteHeader(&tar.Header{
Name: dir + "/",
Typeflag: tar.TypeDir,
Mode: 0o755,
})
}
var total int64
for _, e := range entries {
name := filepath.ToSlash(e.ArtifactPath)
// Ensure parent directories exist as explicit entries.
for _, d := range parentDirs(name) {
if err := writeDir(d); err != nil {
return 0, fmt.Errorf("write dir header %q: %w", d, err)
}
}
var data []byte
if e.Source != "" {
data, err = os.ReadFile(e.Source)
if err != nil {
return 0, fmt.Errorf("read %s: %w", e.Source, err)
}
} else {
data = e.Data
}
if err := tw.WriteHeader(&tar.Header{
Name: name,
Typeflag: tar.TypeReg,
Mode: 0o644,
Size: int64(len(data)),
}); err != nil {
return 0, fmt.Errorf("write header %q: %w", name, err)
}
if _, err := tw.Write(data); err != nil {
return 0, fmt.Errorf("write %q: %w", name, err)
}
total += int64(len(data))
}
if err := tw.Close(); err != nil {
return 0, fmt.Errorf("close tar: %w", err)
}
if err := enc.Close(); err != nil {
return 0, fmt.Errorf("close zstd: %w", err)
}
return total, nil
}
// parentDirs returns the ancestor directories of a forward-slash path, from
// shallowest to deepest ("a/b/c.txt" → ["a", "a/b"]).
func parentDirs(name string) []string {
var dirs []string
parts := strings.Split(name, "/")
for i := 1; i < len(parts); i++ {
dirs = append(dirs, strings.Join(parts[:i], "/"))
}
return dirs
}
// collectDir returns pack entries for every regular file under root, mapped
// under artifactPrefix. Symlinks and irregular files are skipped. Returns
// (entries, hadFiles).
func collectDir(root, artifactPrefix string) ([]packEntry, bool, error) {
info, err := os.Stat(root)
if err != nil || !info.IsDir() {
return nil, false, nil //nolint:nilerr // absent optional dir is not an error
}
var entries []packEntry
walkErr := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || !d.Type().IsRegular() {
return nil
}
rel, err := filepath.Rel(root, path)
if err != nil {
return err
}
entries = append(entries, packEntry{
ArtifactPath: artifactPrefix + "/" + filepath.ToSlash(rel),
Source: path,
})
return nil
})
if walkErr != nil {
return nil, false, fmt.Errorf("walk %s: %w", root, walkErr)
}
return entries, len(entries) > 0, nil
}