261 lines
6.4 KiB
Go
261 lines
6.4 KiB
Go
// Package build provides project type detection and cross-compilation for the Core build system.
|
|
// This file handles build cache configuration and key generation.
|
|
package build
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"dappco.re/go/core"
|
|
"dappco.re/go/core/build/internal/ax"
|
|
"dappco.re/go/core/io"
|
|
coreerr "dappco.re/go/core/log"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// CacheConfig holds build cache configuration loaded from .core/build.yaml.
|
|
//
|
|
// cfg := build.CacheConfig{
|
|
// Enabled: true,
|
|
// Directory: ".core/cache",
|
|
// Paths: []string{"~/.cache/go-build", "~/go/pkg/mod"},
|
|
// }
|
|
type CacheConfig struct {
|
|
// Enabled turns cache setup on for the build.
|
|
Enabled bool `yaml:"enabled"`
|
|
// Directory is where cache metadata is stored.
|
|
Directory string `yaml:"dir,omitempty"`
|
|
// KeyPrefix prefixes the generated cache key.
|
|
KeyPrefix string `yaml:"key_prefix,omitempty"`
|
|
// Paths are cache directories that should exist before the build starts.
|
|
Paths []string `yaml:"paths,omitempty"`
|
|
// RestoreKeys are fallback prefixes used when the exact cache key is not present.
|
|
RestoreKeys []string `yaml:"restore_keys,omitempty"`
|
|
}
|
|
|
|
// UnmarshalYAML accepts both the concise build config keys and the longer aliases.
|
|
//
|
|
// err := yaml.Unmarshal([]byte("dir: .core/cache"), &cfg)
|
|
func (c *CacheConfig) UnmarshalYAML(value *yaml.Node) error {
|
|
type rawCacheConfig struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
Directory string `yaml:"directory"`
|
|
Dir string `yaml:"dir"`
|
|
KeyPrefix string `yaml:"key_prefix"`
|
|
Key string `yaml:"key"`
|
|
Paths []string `yaml:"paths"`
|
|
RestoreKeys []string `yaml:"restore_keys"`
|
|
}
|
|
|
|
var raw rawCacheConfig
|
|
if err := value.Decode(&raw); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Enabled = raw.Enabled
|
|
c.Directory = firstNonEmpty(raw.Directory, raw.Dir)
|
|
c.KeyPrefix = firstNonEmpty(raw.KeyPrefix, raw.Key)
|
|
c.Paths = raw.Paths
|
|
c.RestoreKeys = raw.RestoreKeys
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetupCache normalises cache paths and ensures the cache directories exist.
|
|
//
|
|
// err := build.SetupCache(io.Local, ".", &build.CacheConfig{
|
|
// Enabled: true,
|
|
// Paths: []string{"~/.cache/go-build", "~/go/pkg/mod"},
|
|
// })
|
|
func SetupCache(fs io.Medium, dir string, cfg *CacheConfig) error {
|
|
if fs == nil || cfg == nil || !cfg.Enabled {
|
|
return nil
|
|
}
|
|
|
|
if cfg.Directory == "" {
|
|
cfg.Directory = ax.Join(dir, ConfigDir, "cache")
|
|
}
|
|
cfg.Directory = normaliseCachePath(dir, cfg.Directory)
|
|
|
|
if err := fs.EnsureDir(cfg.Directory); err != nil {
|
|
return coreerr.E("build.SetupCache", "failed to create cache directory", err)
|
|
}
|
|
|
|
normalisedPaths := make([]string, 0, len(cfg.Paths))
|
|
for _, path := range cfg.Paths {
|
|
path = normaliseCachePath(dir, path)
|
|
if path == "" {
|
|
continue
|
|
}
|
|
if err := fs.EnsureDir(path); err != nil {
|
|
return coreerr.E("build.SetupCache", "failed to create cache path "+path, err)
|
|
}
|
|
normalisedPaths = append(normalisedPaths, path)
|
|
}
|
|
cfg.Paths = deduplicateStrings(normalisedPaths)
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetupBuildCache prepares the cache configuration stored on a build config.
|
|
//
|
|
// err := build.SetupBuildCache(io.Local, ".", cfg)
|
|
func SetupBuildCache(fs io.Medium, dir string, cfg *BuildConfig) error {
|
|
if fs == nil || cfg == nil {
|
|
return nil
|
|
}
|
|
|
|
return SetupCache(fs, dir, &cfg.Build.Cache)
|
|
}
|
|
|
|
// CacheKey returns a deterministic cache key for the build configuration and target.
|
|
//
|
|
// key := build.CacheKey("core-build", build.Target{OS: "linux", Arch: "amd64"}, &build.CacheConfig{
|
|
// KeyPrefix: "main",
|
|
// })
|
|
func CacheKey(buildName string, target Target, cfg *CacheConfig) string {
|
|
if buildName == "" {
|
|
buildName = "build"
|
|
}
|
|
|
|
keyPrefix := buildName
|
|
if cfg != nil && cfg.KeyPrefix != "" {
|
|
keyPrefix = cfg.KeyPrefix
|
|
}
|
|
|
|
snapshot := cacheKeySnapshot(buildName, target, cfg)
|
|
sum := sha256.Sum256([]byte(snapshot))
|
|
suffix := hex.EncodeToString(sum[:])[:12]
|
|
|
|
return core.Join("-", keyPrefix, target.OS, target.Arch, suffix)
|
|
}
|
|
|
|
// CacheEnvironment returns environment variables derived from the cache config.
|
|
//
|
|
// env := build.CacheEnvironment(&build.CacheConfig{Enabled: true, Paths: []string{"/tmp/go-build"}})
|
|
func CacheEnvironment(cfg *CacheConfig) []string {
|
|
if cfg == nil || !cfg.Enabled {
|
|
return nil
|
|
}
|
|
|
|
var env []string
|
|
|
|
for _, path := range cfg.Paths {
|
|
switch cacheEnvironmentName(path) {
|
|
case "GOCACHE":
|
|
env = appendIfMissing(env, "GOCACHE="+path)
|
|
case "GOMODCACHE":
|
|
env = appendIfMissing(env, "GOMODCACHE="+path)
|
|
}
|
|
}
|
|
|
|
return deduplicateStrings(env)
|
|
}
|
|
|
|
func cacheKeySnapshot(buildName string, target Target, cfg *CacheConfig) string {
|
|
parts := []string{
|
|
"build",
|
|
buildName,
|
|
target.OS,
|
|
target.Arch,
|
|
}
|
|
|
|
if cfg == nil {
|
|
return core.Join("\n", parts...)
|
|
}
|
|
|
|
parts = append(parts,
|
|
strconv.FormatBool(cfg.Enabled),
|
|
cfg.Directory,
|
|
cfg.KeyPrefix,
|
|
)
|
|
|
|
paths := deduplicateStrings(append([]string(nil), cfg.Paths...))
|
|
sort.Strings(paths)
|
|
parts = append(parts, "paths:"+core.Join(",", paths...))
|
|
|
|
restoreKeys := deduplicateStrings(append([]string(nil), cfg.RestoreKeys...))
|
|
sort.Strings(restoreKeys)
|
|
parts = append(parts, "restore:"+core.Join(",", restoreKeys...))
|
|
|
|
return core.Join("\n", parts...)
|
|
}
|
|
|
|
func cacheEnvironmentName(path string) string {
|
|
base := strings.ToLower(ax.Base(path))
|
|
|
|
switch base {
|
|
case "go-build", "gocache":
|
|
return "GOCACHE"
|
|
case "go-mod", "gomodcache":
|
|
return "GOMODCACHE"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func appendIfMissing(values []string, value string) []string {
|
|
for _, current := range values {
|
|
if current == value {
|
|
return values
|
|
}
|
|
}
|
|
return append(values, value)
|
|
}
|
|
|
|
func normaliseCachePath(baseDir, path string) string {
|
|
path = strings.TrimSpace(path)
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
|
|
if strings.HasPrefix(path, "~") {
|
|
home := core.Env("HOME")
|
|
if home != "" {
|
|
if path == "~" {
|
|
return ax.Clean(home)
|
|
}
|
|
if strings.HasPrefix(path, "~/") {
|
|
return ax.Join(home, strings.TrimPrefix(path, "~/"))
|
|
}
|
|
}
|
|
}
|
|
|
|
if ax.IsAbs(path) {
|
|
return ax.Clean(path)
|
|
}
|
|
|
|
return ax.Join(baseDir, path)
|
|
}
|
|
|
|
func deduplicateStrings(values []string) []string {
|
|
if len(values) == 0 {
|
|
return values
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(values))
|
|
result := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
if value == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[value]; ok {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
result = append(result, value)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if strings.TrimSpace(value) != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|