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

uv.lock: install only the runtime dependency closure (exclude dev groups)

Previously pymage installed every package in uv.lock, including dev-dependency
groups and the whole resolution universe. On real projects this bloated images
badly: ruff alone was 50% of deps in astral-sh/uv-docker-example, and dev/CI
tools (ruff, mypy, ty, zizmor, ...) were 63% of deps in
fastapi/full-stack-fastapi-template.

ParseUVLockFile now computes the runtime closure from the local/project
packages' dependencies, transitively following dependencies and expanding
requested extras via optional-dependencies, never following dev-dependencies
(matching 'uv sync --no-dev'). Locks with no project package still install
everything. sdist-only errors now only trigger for packages actually in the
closure. Cuts deps 50-63% on the studied projects.

Co-authored-by: Jason Hall <imjasonh@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-06-11 01:05:26 +00:00
parent b721e31fd6
commit 89e64cb3d2
No known key found for this signature in database
2 changed files with 165 additions and 17 deletions

View file

@ -14,10 +14,20 @@ type uvLockFile struct {
}
type uvPackage struct {
Name string `toml:"name"`
Version string `toml:"version"`
Source map[string]any `toml:"source"`
Wheels []uvArtifact `toml:"wheels"`
Name string `toml:"name"`
Version string `toml:"version"`
Source map[string]any `toml:"source"`
Wheels []uvArtifact `toml:"wheels"`
Dependencies []uvDep `toml:"dependencies"`
OptionalDeps map[string][]uvDep `toml:"optional-dependencies"`
DevDeps map[string][]uvDep `toml:"dev-dependencies"`
}
// uvDep is a dependency edge in uv.lock. Extras pull a package's
// optional-dependencies; dev-dependencies groups are never followed.
type uvDep struct {
Name string `toml:"name"`
Extras []string `toml:"extra"`
}
type uvArtifact struct {
@ -25,9 +35,15 @@ type uvArtifact struct {
Hash string `toml:"hash"`
}
// ParseUVLockFile reads a uv.lock and returns pinned registry packages with
// wheel URLs and hashes. Local workspace packages (editable, virtual, etc.)
// are skipped — application code comes from the source directory instead.
// 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) {
data, err := os.ReadFile(path)
if err != nil {
@ -38,11 +54,18 @@ func ParseUVLockFile(path string) ([]Requirement, error) {
return nil, fmt.Errorf("uv.lock: parse %q: %w", path, err)
}
include := runtimeClosure(lf.Package)
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)
}
@ -76,6 +99,64 @@ func ParseUVLockFile(path string) ([]Requirement, error) {
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 {
byName := make(map[string]*uvPackage, len(pkgs))
var roots []*uvPackage
for i := range pkgs {
p := &pkgs[i]
byName[NormalizeName(p.Name)] = p
if isLocalSource(p.Source) {
roots = append(roots, p)
}
}
if len(roots) == 0 {
return nil
}
type item struct {
name string
extras []string
}
var queue []item
enqueue := func(deps []uvDep) {
for _, d := range deps {
queue = append(queue, item{NormalizeName(d.Name), d.Extras})
}
}
for _, r := range roots {
enqueue(r.Dependencies)
}
include := map[string]bool{}
visited := map[string]bool{}
for len(queue) > 0 {
it := queue[0]
queue = queue[1:]
key := it.name + "|" + strings.Join(it.extras, ",")
if visited[key] {
continue
}
visited[key] = true
p := byName[it.name]
if p == nil {
continue
}
if !isLocalSource(p.Source) {
include[it.name] = true
}
enqueue(p.Dependencies)
for _, ex := range it.extras {
enqueue(p.OptionalDeps[ex])
}
}
return include
}
func isLocalSource(src map[string]any) bool {
if src == nil {
return false

View file

@ -6,6 +6,10 @@ import (
"testing"
)
// uvLockFixture exercises: a virtual + an editable local package (both
// skipped), the runtime closure of the editable project (alpha, its transitive
// beta, and extradep pulled by the requested alpha[x] extra), a multi-wheel
// package (native), and a dev-only package (linter) that must be excluded.
const uvLockFixture = `version = 1
requires-python = ">=3.14"
@ -18,15 +22,48 @@ source = { virtual = "." }
name = "app"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "alpha", extra = ["x"] },
{ name = "native" },
]
[package.dev-dependencies]
dev = [
{ name = "linter" },
]
[[package]]
name = "alpha"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beta" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/a/alpha-1.0.0-py3-none-any.whl", hash = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" },
]
[package.optional-dependencies]
x = [
{ name = "extradep" },
]
[[package]]
name = "beta"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://example.com/beta-1.0.0-py3-none-any.whl", hash = "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" },
]
[[package]]
name = "extradep"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://example.com/extradep-1.0.0-py3-none-any.whl", hash = "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" },
]
[[package]]
name = "native"
version = "2.0.0"
@ -35,6 +72,14 @@ wheels = [
{ url = "https://example.com/native-2.0.0-py3-none-any.whl", hash = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" },
{ url = "https://example.com/native-2.0.0-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" },
]
[[package]]
name = "linter"
version = "9.9.9"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://example.com/linter-9.9.9-py3-none-any.whl", hash = "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" },
]
`
func TestParseUVLockFile(t *testing.T) {
@ -47,17 +92,31 @@ func TestParseUVLockFile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(reqs) != 2 {
t.Fatalf("got %d reqs, want 2 (virtual skipped): %+v", len(reqs), reqs)
got := map[string]Requirement{}
for _, r := range reqs {
got[r.Name] = r
}
if reqs[0].Name != "alpha" || len(reqs[0].Wheels) != 1 {
t.Fatalf("alpha = %+v", reqs[0])
// Runtime closure: alpha, its transitive beta, extradep (alpha[x]), native.
for _, want := range []string{"alpha", "beta", "extradep", "native"} {
if _, ok := got[want]; !ok {
t.Errorf("missing runtime dep %q (got %v)", want, keys(got))
}
}
if reqs[0].Wheels[0].Filename != "alpha-1.0.0-py3-none-any.whl" {
t.Errorf("filename = %q", reqs[0].Wheels[0].Filename)
// Local packages skipped; dev-only package excluded.
for _, bad := range []string{"app", "virtual-pkg", "linter"} {
if _, ok := got[bad]; ok {
t.Errorf("%q should not be installed", bad)
}
}
if len(reqs[1].Wheels) != 2 {
t.Fatalf("native wheels = %+v", reqs[1].Wheels)
if len(reqs) != 4 {
t.Fatalf("got %d reqs, want 4: %v", len(reqs), keys(got))
}
if len(got["native"].Wheels) != 2 {
t.Errorf("native should keep both wheels, got %d", len(got["native"].Wheels))
}
if got["alpha"].Wheels[0].Filename != "alpha-1.0.0-py3-none-any.whl" {
t.Errorf("alpha filename = %q", got["alpha"].Wheels[0].Filename)
}
}
@ -70,7 +129,15 @@ func TestParseAnyPrefersUVLock(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(reqs) != 2 {
t.Fatalf("got %d", len(reqs))
if len(reqs) != 4 {
t.Fatalf("got %d, want 4", len(reqs))
}
}
func keys(m map[string]Requirement) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}