1
0
Fork 0
mirror of https://github.com/imjasonh/terraform-playground synced 2026-07-07 23:35:16 +00:00

lock: evaluate PEP 508 markers and support --extra/--package in uv.lock closure

Co-authored-by: Jason Hall <imjasonh@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-06-11 01:30:45 +00:00
parent b484b02113
commit 7e8e2261a7
No known key found for this signature in database
8 changed files with 692 additions and 38 deletions

View file

@ -47,6 +47,8 @@ type buildFlags struct {
repo string
tags []string
platforms []string
extras []string
pkg string
pythonTag string
prefix string
workingDir string
@ -90,6 +92,8 @@ func buildCmd() *cobra.Command {
fs := cmd.Flags()
fs.StringVar(&f.base, "base", "", "base image reference (default: cgr.dev/chainguard/python:latest)")
fs.StringVar(&f.lockFile, "lock", "", "lock file (default: uv.lock in the source directory)")
fs.StringArrayVar(&f.extras, "extra", nil, "enable a project optional-dependency group from uv.lock (repeatable)")
fs.StringVar(&f.pkg, "package", "", "build a single uv workspace member (by name)")
fs.StringArrayVar(&f.findLinks, "find-links", nil, "local wheel directory (optional; wheels are downloaded from the lock when omitted)")
fs.StringVar(&f.repo, "repo", "", "destination repository, pushed by digest, e.g. gcr.io/foo/bar (default: [tool.pymage] repo)")
fs.StringArrayVarP(&f.tags, "tag", "t", nil, "tag(s) to apply at --repo; tag component only, not a full reference (repeatable; default: [tool.pymage] tags or 'latest')")
@ -141,13 +145,10 @@ func runBuild(cmd *cobra.Command, f *buildFlags) error {
return fmt.Errorf("--label: %w", err)
}
reqs, err := lock.ParseAny(f.lockFile)
lk, err := lock.Load(f.lockFile)
if err != nil {
return err
}
if err := lock.Validate(reqs, f.requireHash); err != nil {
return err
}
platforms, err := parsePlatforms(f.platforms)
if err != nil {
@ -199,7 +200,7 @@ func runBuild(cmd *cobra.Command, f *buildFlags) error {
if err != nil {
return err
}
img, deps, err := buildOne(ctx, f, reqs, pp, base, labels, layerCache, metaCache, wheelCache)
img, deps, err := buildOne(ctx, f, lk, pp, base, labels, layerCache, metaCache, wheelCache)
if err != nil {
return err
}
@ -231,7 +232,7 @@ func runBuild(cmd *cobra.Command, f *buildFlags) error {
// buildOne resolves wheels for a single platform and builds the image from the
// already-resolved base, returning the resolved wheels for SBOM aggregation.
func buildOne(ctx context.Context, f *buildFlags, reqs []lock.Requirement, platform *v1.Platform, base v1.Image, labels map[string]string, layerCache, metaCache *cache.Cache, wheelCache string) (v1.Image, []wheelhouse.ResolvedWheel, error) {
func buildOne(ctx context.Context, f *buildFlags, lk *lock.Lock, platform *v1.Platform, base v1.Image, labels map[string]string, layerCache, metaCache *cache.Cache, wheelCache string) (v1.Image, []wheelhouse.ResolvedWheel, error) {
// Determine the interpreter: honor --python (validated against the base) or
// auto-detect it from the base image.
major, minor, pyTag, err := resolveInterpreter(f, base, metaCache)
@ -242,6 +243,19 @@ func buildOne(ctx context.Context, f *buildFlags, reqs []lock.Requirement, platf
target := platformTarget(platform)
target.PyMajor, target.PyMinor = major, minor
// Resolve the lock for this target (uv.lock runtime closure honors
// --package, --extra, and the target's environment markers).
reqs, err := lk.Resolve(lock.Options{
Package: f.pkg, Extras: f.extras,
OS: target.OS, Arch: target.Arch, PyMajor: target.PyMajor, PyMinor: target.PyMinor,
})
if err != nil {
return nil, nil, err
}
if err := lock.Validate(reqs, f.requireHash); err != nil {
return nil, nil, err
}
wheels, err := wheelhouse.ResolveContext(ctx, reqs, f.findLinks, target, wheelCache)
if err != nil {
return nil, nil, err

View file

@ -69,6 +69,12 @@ func applyDefaults(cmd *cobra.Command, f *buildFlags) error {
if !changed("no-cache") && cfg.NoCache {
f.noCache = true
}
if !changed("extra") && len(cfg.Extras) > 0 {
f.extras = cfg.Extras
}
if !changed("package") && cfg.Package != "" {
f.pkg = cfg.Package
}
if !changed("python") && cfg.Python != "" {
f.pythonTag = cfg.Python
}

View file

@ -24,6 +24,14 @@ type WheelRef struct {
Filename string // wheel file name, used for tag parsing
}
// SdistRef is a pinned source distribution, used to build a wheel when no
// pre-built wheel is available.
type SdistRef struct {
URL string
SHA256 string // hex digest, no "sha256:" prefix
Filename string
}
// Requirement is a single pinned distribution.
type Requirement struct {
// Name is the project name as written (normalize with NormalizeName for
@ -35,6 +43,9 @@ type Requirement struct {
Hashes []string
// Wheels lists known wheel artifacts for this pin (populated from uv.lock).
Wheels []WheelRef
// Sdist, when set, is the source distribution to build a wheel from if no
// compatible wheel is available.
Sdist *SdistRef
}
var (
@ -81,6 +92,54 @@ func ParseAny(path string) ([]Requirement, error) {
return ParseFile(path)
}
// Lock is a parsed lock file (parsed once) that can be resolved per target.
type Lock struct {
isUV bool
uv *uvLockFile // uv.lock
reqs []Requirement // requirements format
}
// Options selects the resolution for a uv.lock: a workspace package, extras to
// enable, and the target environment for marker evaluation. They are ignored
// for requirements-format locks (which are already flat and pinned).
type Options struct {
Package string
Extras []string
OS, Arch string
PyMajor, PyMinor int
}
// Load reads and parses a lock file once. uv.lock is parsed as TOML; everything
// else is treated as a requirements file.
func Load(path string) (*Lock, error) {
if strings.EqualFold(filepath.Base(path), "uv.lock") {
lf, err := parseUVLock(path)
if err != nil {
return nil, err
}
return &Lock{isUV: true, uv: lf}, nil
}
reqs, err := ParseFile(path)
if err != nil {
return nil, err
}
return &Lock{reqs: reqs}, nil
}
// Resolve returns the pinned requirements for the given target. For uv.lock
// this is the runtime closure (extras/package/markers applied); for a
// requirements file it is the flat pinned list.
func (l *Lock) Resolve(o Options) ([]Requirement, error) {
if !l.isUV {
return append([]Requirement(nil), l.reqs...), nil
}
var env MarkerEnv
if o.PyMajor != 0 {
env = NewMarkerEnv(o.OS, o.Arch, o.PyMajor, o.PyMinor)
}
return resolveUV(l.uv, UVOptions{Package: o.Package, Extras: o.Extras, Env: env})
}
// DiscoverLock returns the path to a lock file in dir, preferring uv.lock.
func DiscoverLock(dir string) (string, error) {
for _, name := range []string{"uv.lock", "requirements.lock", "requirements.txt"} {
@ -187,7 +246,7 @@ func Validate(reqs []Requirement, requireHashes bool) error {
if r.Version == "" {
return fmt.Errorf("lock: requirement %q is not pinned with ==", r.Name)
}
if requireHashes && len(r.Hashes) == 0 {
if requireHashes && len(r.Hashes) == 0 && r.Sdist == nil {
return fmt.Errorf("lock: requirement %q has no --hash entries", r.Name)
}
}

View file

@ -0,0 +1,345 @@
package lock
import (
"fmt"
"strconv"
"strings"
)
// MarkerEnv holds the PEP 508 environment-marker variables for a build target.
type MarkerEnv map[string]string
// NewMarkerEnv derives marker variables from a target os/arch/python version.
func NewMarkerEnv(os, arch string, pyMajor, pyMinor int) MarkerEnv {
sysPlatform := map[string]string{"linux": "linux", "darwin": "darwin", "windows": "win32"}[os]
if sysPlatform == "" {
sysPlatform = os
}
platSystem := map[string]string{"linux": "Linux", "darwin": "Darwin", "windows": "Windows"}[os]
if platSystem == "" {
platSystem = os
}
osName := "posix"
if os == "windows" {
osName = "nt"
}
machine := map[string]string{
"amd64": "x86_64", "arm64": "aarch64", "386": "i686",
"arm": "armv7l", "ppc64le": "ppc64le", "s390x": "s390x",
}[arch]
if machine == "" {
machine = arch
}
return MarkerEnv{
"os_name": osName,
"sys_platform": sysPlatform,
"platform_system": platSystem,
"platform_machine": machine,
"python_version": fmt.Sprintf("%d.%d", pyMajor, pyMinor),
"python_full_version": fmt.Sprintf("%d.%d.0", pyMajor, pyMinor),
"implementation_name": "cpython",
"implementation_version": fmt.Sprintf("%d.%d.0", pyMajor, pyMinor),
"platform_python_implementation": "CPython",
}
}
// EvalMarker reports whether a PEP 508 environment marker holds for env. An
// empty marker is always true. If the marker can't be parsed it returns true,
// so an unrecognized marker never silently drops a dependency.
func EvalMarker(marker string, env MarkerEnv) bool {
marker = strings.TrimSpace(marker)
if marker == "" {
return true
}
toks, err := tokenizeMarker(marker)
if err != nil {
return true
}
p := &markerParser{toks: toks, env: env}
v, err := p.parseOr()
if err != nil || !p.atEnd() {
return true
}
return v
}
type mtokKind int
const (
mtokString mtokKind = iota
mtokIdent
mtokOp
mtokLParen
mtokRParen
)
type mtok struct {
kind mtokKind
val string
}
func tokenizeMarker(s string) ([]mtok, error) {
var toks []mtok
for i := 0; i < len(s); {
c := s[i]
switch {
case c == ' ' || c == '\t' || c == '\n':
i++
case c == '\'' || c == '"':
j := i + 1
for j < len(s) && s[j] != c {
j++
}
if j >= len(s) {
return nil, fmt.Errorf("unterminated string")
}
toks = append(toks, mtok{mtokString, s[i+1 : j]})
i = j + 1
case c == '(':
toks = append(toks, mtok{mtokLParen, "("})
i++
case c == ')':
toks = append(toks, mtok{mtokRParen, ")"})
i++
case c == '=' || c == '!' || c == '<' || c == '>' || c == '~':
j := i
for j < len(s) && strings.ContainsRune("=!<>~", rune(s[j])) {
j++
}
toks = append(toks, mtok{mtokOp, s[i:j]})
i = j
case isIdentChar(c):
j := i
for j < len(s) && isIdentChar(s[j]) {
j++
}
toks = append(toks, mtok{mtokIdent, s[i:j]})
i = j
default:
return nil, fmt.Errorf("unexpected char %q", c)
}
}
return toks, nil
}
func isIdentChar(c byte) bool {
return c == '_' || c == '.' ||
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
}
type markerParser struct {
toks []mtok
pos int
env MarkerEnv
}
func (p *markerParser) atEnd() bool { return p.pos >= len(p.toks) }
func (p *markerParser) peek() (mtok, bool) {
if p.atEnd() {
return mtok{}, false
}
return p.toks[p.pos], true
}
func (p *markerParser) parseOr() (bool, error) {
v, err := p.parseAnd()
if err != nil {
return false, err
}
for {
t, ok := p.peek()
if !ok || t.kind != mtokIdent || t.val != "or" {
return v, nil
}
p.pos++
r, err := p.parseAnd()
if err != nil {
return false, err
}
v = v || r
}
}
func (p *markerParser) parseAnd() (bool, error) {
v, err := p.parseAtom()
if err != nil {
return false, err
}
for {
t, ok := p.peek()
if !ok || t.kind != mtokIdent || t.val != "and" {
return v, nil
}
p.pos++
r, err := p.parseAtom()
if err != nil {
return false, err
}
v = v && r
}
}
func (p *markerParser) parseAtom() (bool, error) {
t, ok := p.peek()
if !ok {
return false, fmt.Errorf("unexpected end")
}
if t.kind == mtokLParen {
p.pos++
v, err := p.parseOr()
if err != nil {
return false, err
}
end, ok := p.peek()
if !ok || end.kind != mtokRParen {
return false, fmt.Errorf("missing )")
}
p.pos++
return v, nil
}
return p.parseComparison()
}
func (p *markerParser) parseComparison() (bool, error) {
l, lver, err := p.parseValue()
if err != nil {
return false, err
}
op, err := p.parseOp()
if err != nil {
return false, err
}
r, rver, err := p.parseValue()
if err != nil {
return false, err
}
return compareMarker(l, lver, op, r, rver), nil
}
func (p *markerParser) parseOp() (string, error) {
t, ok := p.peek()
if !ok {
return "", fmt.Errorf("expected operator")
}
if t.kind == mtokOp {
p.pos++
return t.val, nil
}
if t.kind == mtokIdent && t.val == "in" {
p.pos++
return "in", nil
}
if t.kind == mtokIdent && t.val == "not" {
p.pos++
n, ok := p.peek()
if !ok || n.kind != mtokIdent || n.val != "in" {
return "", fmt.Errorf("expected 'in' after 'not'")
}
p.pos++
return "not in", nil
}
return "", fmt.Errorf("expected operator, got %q", t.val)
}
// parseValue returns the value and whether it is a version-valued variable
// (python_version / python_full_version / implementation_version).
func (p *markerParser) parseValue() (string, bool, error) {
t, ok := p.peek()
if !ok {
return "", false, fmt.Errorf("expected value")
}
p.pos++
switch t.kind {
case mtokString:
return t.val, false, nil
case mtokIdent:
v, known := p.env[t.val]
if !known {
// Unknown variable: treat as empty, non-version.
return "", false, nil
}
isVer := t.val == "python_version" || t.val == "python_full_version" || t.val == "implementation_version"
return v, isVer, nil
default:
return "", false, fmt.Errorf("unexpected token %q", t.val)
}
}
func compareMarker(l string, lver bool, op string, r string, rver bool) bool {
switch op {
case "in":
return strings.Contains(r, l)
case "not in":
return !strings.Contains(r, l)
}
if lver || rver {
c := compareVersions(l, r)
switch op {
case "==", "===":
return c == 0
case "!=":
return c != 0
case "<":
return c < 0
case "<=":
return c <= 0
case ">":
return c > 0
case ">=", "~=":
return c >= 0
}
}
switch op {
case "==", "===":
return l == r
case "!=":
return l != r
case "<":
return l < r
case "<=":
return l <= r
case ">":
return l > r
case ">=":
return l >= r
}
return false
}
// compareVersions compares dotted numeric versions (e.g. "3.10" vs "3.9").
// Non-numeric components compare lexically; missing components are 0.
func compareVersions(a, b string) int {
as, bs := strings.Split(a, "."), strings.Split(b, ".")
n := len(as)
if len(bs) > n {
n = len(bs)
}
for i := 0; i < n; i++ {
var ai, bi string
if i < len(as) {
ai = as[i]
}
if i < len(bs) {
bi = bs[i]
}
an, aerr := strconv.Atoi(ai)
bn, berr := strconv.Atoi(bi)
if aerr == nil && berr == nil {
if an != bn {
if an < bn {
return -1
}
return 1
}
continue
}
if ai != bi {
if ai < bi {
return -1
}
return 1
}
}
return 0
}

View file

@ -0,0 +1,42 @@
package lock
import "testing"
func TestEvalMarker(t *testing.T) {
linux := NewMarkerEnv("linux", "amd64", 3, 12)
cases := []struct {
marker string
want bool
}{
{"", true},
{"sys_platform == 'win32'", false},
{"sys_platform == 'linux'", true},
{"sys_platform != 'win32'", true},
{"platform_system == 'Linux'", true},
{"platform_machine == 'x86_64'", true},
{"os_name == 'posix'", true},
{"python_version >= '3.11'", true},
{"python_version < '3.11'", false},
{"python_full_version >= '3.12'", true},
{"python_version >= '3.10' and python_version < '3.13'", true},
{"python_version < '3.10' or sys_platform == 'linux'", true},
{"python_version < '3.10' or sys_platform == 'win32'", false},
{"extra == 'foo'", false}, // unknown var -> empty
{"(sys_platform == 'win32') or (os_name == 'posix')", true},
{"platform_machine in 'x86_64 aarch64'", true}, // substring membership
{"'arm' not in platform_machine", true},
{"this is not valid pep508 @#$", true}, // unparseable -> include
}
for _, c := range cases {
if got := EvalMarker(c.marker, linux); got != c.want {
t.Errorf("EvalMarker(%q) = %v, want %v", c.marker, got, c.want)
}
}
// A python_version gate flips on a different target.
py310 := NewMarkerEnv("linux", "amd64", 3, 10)
if EvalMarker("python_version >= '3.11'", py310) {
t.Error("python_version >= 3.11 should be false on 3.10")
}
}

View file

@ -17,6 +17,7 @@ type uvPackage struct {
Name string `toml:"name"`
Version string `toml:"version"`
Source map[string]any `toml:"source"`
Sdist uvArtifact `toml:"sdist"`
Wheels []uvArtifact `toml:"wheels"`
Dependencies []uvDep `toml:"dependencies"`
OptionalDeps map[string][]uvDep `toml:"optional-dependencies"`
@ -24,10 +25,12 @@ type uvPackage struct {
}
// uvDep is a dependency edge in uv.lock. Extras pull a package's
// optional-dependencies; dev-dependencies groups are never followed.
// optional-dependencies; dev-dependencies groups are never followed. A marker
// gates the edge on the target environment.
type uvDep struct {
Name string `toml:"name"`
Extras []string `toml:"extra"`
Marker string `toml:"marker"`
}
type uvArtifact struct {
@ -35,16 +38,20 @@ type uvArtifact struct {
Hash string `toml:"hash"`
}
// ParseUVLockFile reads a uv.lock and returns the project's runtime dependency
// closure as pinned registry packages with wheel URLs and hashes.
//
// Only packages reachable from the local (project/workspace) packages' runtime
// dependencies — expanding requested extras via optional-dependencies — are
// included. Dev-dependency groups are excluded, matching `uv sync --no-dev`, so
// linters/test tools don't end up in the runtime image. Local packages
// themselves (editable/virtual/workspace) are skipped; application code comes
// from the source directory.
func ParseUVLockFile(path string) ([]Requirement, error) {
// UVOptions selects which part of a uv.lock to install.
type UVOptions struct {
// Package roots the closure at a single workspace member (by name). Empty
// unions the closures of all local (project/workspace) packages.
Package string
// Extras enables the root project's own optional-dependency groups.
Extras []string
// Env, when non-nil, evaluates dependency markers for the target so
// platform/python-gated deps are included only when they apply.
Env MarkerEnv
}
// parseUVLock reads and unmarshals a uv.lock file.
func parseUVLock(path string) (*uvLockFile, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
@ -53,22 +60,42 @@ func ParseUVLockFile(path string) ([]Requirement, error) {
if err := toml.Unmarshal(data, &lf); err != nil {
return nil, fmt.Errorf("uv.lock: parse %q: %w", path, err)
}
return &lf, nil
}
include := runtimeClosure(lf.Package)
// ParseUVLockFile reads a uv.lock and returns its full runtime closure with
// default options (all local roots, no extras, no marker filtering). Prefer
// Load + Resolve to pass a package/extras/target.
func ParseUVLockFile(path string) ([]Requirement, error) {
lf, err := parseUVLock(path)
if err != nil {
return nil, err
}
return resolveUV(lf, UVOptions{})
}
// resolveUV returns the project's runtime dependency closure as pinned
// packages with wheel URLs/hashes (and an sdist fallback when present).
//
// Only packages reachable from the local (project/workspace) packages' runtime
// dependencies — expanding requested extras via optional-dependencies and
// honoring markers — are included. Dev-dependency groups are excluded (like
// `uv sync --no-dev`). Local packages are skipped; app code comes from source.
func resolveUV(lf *uvLockFile, opts UVOptions) ([]Requirement, error) {
include, err := runtimeClosure(lf.Package, opts)
if err != nil {
return nil, err
}
var reqs []Requirement
for _, pkg := range lf.Package {
if isLocalSource(pkg.Source) {
continue
}
// When a closure was computed, install only its members; otherwise
// (no local/project package found) fall back to every package.
if include != nil && !include[NormalizeName(pkg.Name)] {
continue
}
if len(pkg.Wheels) == 0 {
return nil, fmt.Errorf("uv.lock: %s==%s has no wheels (sdist-only deps are not supported)", pkg.Name, pkg.Version)
}
req := Requirement{Name: pkg.Name, Version: pkg.Version}
seen := map[string]bool{}
for _, w := range pkg.Wheels {
@ -84,37 +111,60 @@ func ParseUVLockFile(path string) ([]Requirement, error) {
}
seen[sum] = true
req.Hashes = append(req.Hashes, sum)
req.Wheels = append(req.Wheels, WheelRef{
URL: w.URL,
SHA256: sum,
Filename: filepath.Base(w.URL),
})
req.Wheels = append(req.Wheels, WheelRef{URL: w.URL, SHA256: sum, Filename: filepath.Base(w.URL)})
}
// An sdist provides a wheel-build fallback when no wheel is compatible.
if pkg.Sdist.URL != "" {
sum, err := parseUVHash(pkg.Sdist.Hash)
if err != nil {
return nil, fmt.Errorf("uv.lock: %s==%s sdist: %w", pkg.Name, pkg.Version, err)
}
req.Sdist = &SdistRef{URL: pkg.Sdist.URL, SHA256: sum, Filename: filepath.Base(pkg.Sdist.URL)}
if !seen[sum] {
req.Hashes = append(req.Hashes, sum) // counts as "hashed"
}
}
if len(req.Wheels) == 0 && req.Sdist == nil {
return nil, fmt.Errorf("uv.lock: %s==%s has neither wheels nor an sdist", pkg.Name, pkg.Version)
}
reqs = append(reqs, req)
}
if len(reqs) == 0 {
return nil, fmt.Errorf("uv.lock: no installable packages in %q", path)
return nil, fmt.Errorf("uv.lock: no installable packages (closure empty)")
}
return reqs, nil
}
// runtimeClosure returns the set of (normalized) package names reachable from
// the local packages' runtime dependencies, expanding extras. It returns nil
// when the lock has no local/project package (e.g. a bare requirements lock),
// signaling "install everything" for backward compatibility.
func runtimeClosure(pkgs []uvPackage) map[string]bool {
// the selected local packages' runtime dependencies, expanding extras and
// honoring markers. It returns nil when the lock has no local/project package
// (e.g. a bare requirements lock), signaling "install everything".
func runtimeClosure(pkgs []uvPackage, opts UVOptions) (map[string]bool, error) {
byName := make(map[string]*uvPackage, len(pkgs))
var roots []*uvPackage
var localRoots []*uvPackage
for i := range pkgs {
p := &pkgs[i]
byName[NormalizeName(p.Name)] = p
if isLocalSource(p.Source) {
roots = append(roots, p)
localRoots = append(localRoots, p)
}
}
var roots []*uvPackage
if opts.Package != "" {
p := byName[NormalizeName(opts.Package)]
if p == nil {
return nil, fmt.Errorf("uv.lock: --package %q not found in lock", opts.Package)
}
roots = []*uvPackage{p}
} else {
roots = localRoots
}
if len(roots) == 0 {
return nil
return nil, nil
}
type item struct {
@ -124,11 +174,18 @@ func runtimeClosure(pkgs []uvPackage) map[string]bool {
var queue []item
enqueue := func(deps []uvDep) {
for _, d := range deps {
if opts.Env != nil && !EvalMarker(d.Marker, opts.Env) {
continue
}
queue = append(queue, item{NormalizeName(d.Name), d.Extras})
}
}
for _, r := range roots {
enqueue(r.Dependencies)
// Enable the root project's own optional-dependency groups (--extra).
for _, ex := range opts.Extras {
enqueue(r.OptionalDeps[ex])
}
}
include := map[string]bool{}
@ -154,7 +211,7 @@ func runtimeClosure(pkgs []uvPackage) map[string]bool {
enqueue(p.OptionalDeps[ex])
}
}
return include
return include, nil
}
func isLocalSource(src map[string]any) bool {

View file

@ -134,6 +134,135 @@ func TestParseAnyPrefersUVLock(t *testing.T) {
}
}
func names(reqs []Requirement) map[string]bool {
m := map[string]bool{}
for _, r := range reqs {
m[NormalizeName(r.Name)] = true
}
return m
}
// closureLock exercises extras, workspace --package selection, and markers.
const closureLock = `version = 1
[[package]]
name = "svc"
version = "0.1.0"
source = { editable = "members/svc" }
dependencies = [
{ name = "core" },
{ name = "winonly", marker = "sys_platform == 'win32'" },
]
[package.optional-dependencies]
gpu = [
{ name = "accel" },
]
[[package]]
name = "worker"
version = "0.1.0"
source = { editable = "members/worker" }
dependencies = [
{ name = "worktool" },
]
[[package]]
name = "core"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
wheels = [ { url = "https://e/core-1.0.0-py3-none-any.whl", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" } ]
[[package]]
name = "accel"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
wheels = [ { url = "https://e/accel-1.0.0-py3-none-any.whl", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" } ]
[[package]]
name = "winonly"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
wheels = [ { url = "https://e/winonly-1.0.0-py3-none-any.whl", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" } ]
[[package]]
name = "worktool"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
wheels = [ { url = "https://e/worktool-1.0.0-py3-none-any.whl", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" } ]
`
func loadClosure(t *testing.T) *Lock {
t.Helper()
dir := t.TempDir()
p := filepath.Join(dir, "uv.lock")
if err := os.WriteFile(p, []byte(closureLock), 0o644); err != nil {
t.Fatal(err)
}
lk, err := Load(p)
if err != nil {
t.Fatal(err)
}
return lk
}
func TestUVClosureMarkers(t *testing.T) {
lk := loadClosure(t)
// Linux target: winonly (sys_platform == win32) is excluded.
reqs, err := lk.Resolve(Options{OS: "linux", Arch: "amd64", PyMajor: 3, PyMinor: 12})
if err != nil {
t.Fatal(err)
}
got := names(reqs)
if !got["core"] || !got["worktool"] {
t.Fatalf("missing runtime deps: %v", got)
}
if got["winonly"] {
t.Error("winonly should be excluded on linux (marker)")
}
if got["accel"] {
t.Error("accel should be excluded without --extra gpu")
}
}
func TestUVClosureExtras(t *testing.T) {
lk := loadClosure(t)
reqs, err := lk.Resolve(Options{Extras: []string{"gpu"}, OS: "linux", Arch: "amd64", PyMajor: 3, PyMinor: 12})
if err != nil {
t.Fatal(err)
}
if !names(reqs)["accel"] {
t.Errorf("--extra gpu should include accel: %v", names(reqs))
}
}
func TestUVClosurePackage(t *testing.T) {
lk := loadClosure(t)
// --package svc: only svc's closure (core), not worker's worktool.
reqs, err := lk.Resolve(Options{Package: "svc", OS: "linux", Arch: "amd64", PyMajor: 3, PyMinor: 12})
if err != nil {
t.Fatal(err)
}
got := names(reqs)
if !got["core"] || got["worktool"] {
t.Errorf("--package svc closure = %v, want {core} (no worktool)", got)
}
// --package worker: only worktool.
reqs, err = lk.Resolve(Options{Package: "worker", OS: "linux", Arch: "amd64", PyMajor: 3, PyMinor: 12})
if err != nil {
t.Fatal(err)
}
got = names(reqs)
if !got["worktool"] || got["core"] {
t.Errorf("--package worker closure = %v, want {worktool}", got)
}
if _, err := lk.Resolve(Options{Package: "nope"}); err == nil {
t.Error("expected error for unknown --package")
}
}
func keys(m map[string]Requirement) []string {
out := make([]string, 0, len(m))
for k := range m {

View file

@ -46,6 +46,8 @@ type Config struct {
Labels map[string]string `toml:"labels"`
FindLinks []string `toml:"find-links"`
NoCache bool `toml:"no-cache"`
Extras []string `toml:"extras"`
Package string `toml:"package"`
}
const defaultBase = "cgr.dev/chainguard/python:latest"