mirror of
https://github.com/imjasonh/terraform-playground
synced 2026-07-08 07:44:57 +00:00
A no-op build's ~2-3s was almost all registry I/O: - the base index was fetched 3x (BasePlatforms + remote.Image per platform) - the base's top layer was downloaded once per platform every build to read /etc/apko.json for interpreter detection (Chainguard has no PYTHON_VERSION) Changes: - build.BaseSet resolves the base reference once and serves per-platform child images memoized (index fetched once; verified by TestBaseSet) - cache the detected interpreter version by base digest in a default per-user metadata cache (cache.GetText/PutText), so repeat builds skip the apko layer download - lazy wheel-cache dir creation; wheelhouse.Resolve defaults to the per-user cache dir Tests: TestBaseSet (index fetched once + per-platform memoized), TestCachedInterpreter (write + cache-hit short-circuit), cache text round-trip. Co-authored-by: Jason Hall <imjasonh@users.noreply.github.com>
172 lines
5.1 KiB
Go
172 lines
5.1 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/imjasonh/terraform-playground/pymage/internal/build"
|
|
"github.com/imjasonh/terraform-playground/pymage/internal/project"
|
|
)
|
|
|
|
// applyDefaults fills omitted flags from the uv project in the source directory
|
|
// (the positional arg, default "."). Precedence is: explicit CLI flag >
|
|
// [tool.pymage] in pyproject.toml >
|
|
// built-in default. A flag counts as "explicit" only when the user actually
|
|
// passed it, which we detect via cobra's Changed() so flags with non-empty
|
|
// defaults (e.g. --workdir) can still be overridden by config.
|
|
func applyDefaults(cmd *cobra.Command, f *buildFlags) error {
|
|
if f.source == "" {
|
|
f.source = "."
|
|
}
|
|
proj, discoverErr := project.Discover(f.source)
|
|
cfg := proj.Config
|
|
changed := func(name string) bool { return cmd.Flags().Changed(name) }
|
|
|
|
if f.lockFile == "" {
|
|
if proj.LockFile == "" {
|
|
if discoverErr != nil {
|
|
return discoverErr
|
|
}
|
|
return fmt.Errorf("no lock file found (expected uv.lock in the source directory, or pass --lock)")
|
|
}
|
|
f.lockFile = proj.LockFile
|
|
}
|
|
|
|
if !changed("repo") && cfg.Repo != "" {
|
|
f.repo = cfg.Repo
|
|
}
|
|
if !changed("tag") && len(cfg.Tags) > 0 {
|
|
f.tags = cfg.Tags
|
|
}
|
|
if !changed("base") && cfg.Base != "" {
|
|
f.base = cfg.Base
|
|
}
|
|
if f.base == "" {
|
|
f.base = project.DefaultBase()
|
|
}
|
|
if !changed("platform") && len(cfg.Platforms) > 0 {
|
|
f.platforms = cfg.Platforms
|
|
}
|
|
if !changed("layer-strategy") && cfg.LayerStrategy != "" {
|
|
f.strategy = cfg.LayerStrategy
|
|
}
|
|
if !changed("max-layers") && cfg.MaxLayers != 0 {
|
|
f.maxLayers = cfg.MaxLayers
|
|
}
|
|
if !changed("max-wheel-layers") && cfg.MaxWheelLayers != 0 {
|
|
f.maxWheelLayers = cfg.MaxWheelLayers
|
|
}
|
|
if !changed("push-concurrency") && cfg.PushJobs != 0 {
|
|
f.pushJobs = cfg.PushJobs
|
|
}
|
|
if !changed("python") && cfg.Python != "" {
|
|
f.pythonTag = cfg.Python
|
|
}
|
|
if !changed("prefix") && cfg.Prefix != "" {
|
|
f.prefix = cfg.Prefix
|
|
}
|
|
if !changed("workdir") && cfg.Workdir != "" {
|
|
f.workingDir = cfg.Workdir
|
|
}
|
|
if !changed("user") && cfg.User != "" {
|
|
f.user = cfg.User
|
|
}
|
|
if !changed("find-links") && len(cfg.FindLinks) > 0 {
|
|
f.findLinks = cfg.FindLinks
|
|
}
|
|
if !changed("cmd") && len(cfg.Cmd) > 0 {
|
|
f.cmd = cfg.Cmd
|
|
}
|
|
|
|
// Entrypoint: explicit flag > config > auto-detected console script.
|
|
if len(f.entrypoint) == 0 {
|
|
switch {
|
|
case len(cfg.Entrypoint) > 0:
|
|
f.entrypoint = cfg.Entrypoint
|
|
case len(proj.Entrypoint) > 0:
|
|
f.entrypoint = proj.Entrypoint
|
|
}
|
|
}
|
|
|
|
// Env is additive: auto-detected (PYTHONPATH) first, then config, then any
|
|
// --env flags last so they win on duplicate keys. The src-layout PYTHONPATH
|
|
// is derived from the resolved workdir, since the source is copied there.
|
|
var autoEnv []string
|
|
if proj.SrcLayout {
|
|
autoEnv = append(autoEnv, "PYTHONPATH="+path.Join(f.workingDir, "src"))
|
|
}
|
|
f.env = append(append(autoEnv, cfg.Env...), f.env...)
|
|
|
|
// Labels: config labels first, --label flags appended so they override.
|
|
if len(cfg.Labels) > 0 {
|
|
keys := make([]string, 0, len(cfg.Labels))
|
|
for k := range cfg.Labels {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
merged := make([]string, 0, len(cfg.Labels)+len(f.labels))
|
|
for _, k := range keys {
|
|
merged = append(merged, k+"="+cfg.Labels[k])
|
|
}
|
|
f.labels = append(merged, f.labels...)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func wheelCacheDir(f *buildFlags) (string, error) {
|
|
if f.cacheDir != "" {
|
|
return filepath.Join(f.cacheDir, "wheels"), nil
|
|
}
|
|
return project.DefaultWheelCacheDir()
|
|
}
|
|
|
|
// metaCacheDir is where small build metadata (e.g. detected base interpreter
|
|
// versions) is cached.
|
|
func metaCacheDir(f *buildFlags) (string, error) {
|
|
if f.cacheDir != "" {
|
|
return f.cacheDir, nil
|
|
}
|
|
return project.DefaultCacheDir()
|
|
}
|
|
|
|
func validateBuildFlags(f *buildFlags) error {
|
|
if f.lockFile == "" {
|
|
return fmt.Errorf("no lock file found (expected uv.lock in the source directory, or pass --lock)")
|
|
}
|
|
// "" is the zero value meaning the default (Auto); a 0 budget likewise means
|
|
// "use the default", so only reject genuinely invalid values.
|
|
switch f.strategy {
|
|
case "", string(build.Auto), string(build.PerWheel), string(build.SingleDepsLayer):
|
|
default:
|
|
return fmt.Errorf("invalid --layer-strategy %q (want auto, per-wheel, or single-deps-layer)", f.strategy)
|
|
}
|
|
if f.maxLayers < 0 {
|
|
return fmt.Errorf("--max-layers must be >= 0 (0 means use the default), got %d", f.maxLayers)
|
|
}
|
|
if f.maxWheelLayers < 0 {
|
|
return fmt.Errorf("--max-wheel-layers must be >= 0, got %d", f.maxWheelLayers)
|
|
}
|
|
if f.pushJobs < 0 {
|
|
return fmt.Errorf("--push-concurrency must be >= 0, got %d", f.pushJobs)
|
|
}
|
|
if len(f.entrypoint) == 0 {
|
|
return fmt.Errorf("no entrypoint: set [project.scripts] in pyproject.toml, add entrypoint to [tool.pymage], or pass --entrypoint")
|
|
}
|
|
if f.push {
|
|
if f.repo == "" {
|
|
return fmt.Errorf("no repo configured: set repo in [tool.pymage] (pyproject.toml) or pass --repo (or use --push=false)")
|
|
}
|
|
for _, t := range f.tags {
|
|
if strings.ContainsAny(t, "/:@") {
|
|
return fmt.Errorf("--tag/-t is a tag only, not a full reference: set the destination with --repo or [tool.pymage] repo (got %q)", t)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|