Repository navigation
fix: assemble a build context for uv path sources and pin the uv that resolves it - #56
Merged
Conversation
antoinetoussaint-byte
force-pushed
the
fix/carry-uv-path-sources-and-pin-uv
branch
from
September 23, 2026 12:27
88cfcb6 to
fc55443
Compare
… resolves it
Two independent defects made this recipe emit an image build that resolved on
the developer's machine and failed inside the builder stage.
A `[tool.uv.sources]` path is resolved against the directory of the
pyproject.toml that declares it, and uv.lock records the same relative path. The
recipe's context was the service directory and nothing beside it, so a path
naming a sibling service was absent from the builder stage and `uv sync
--frozen` failed there with
error: Could not compute absolute path from workspace root and lockfile path
Two services sharing one Python project in a repository is a layout this recipe
has to build. It now assembles a context when, and only when, a path leaves the
project: the service's own tree and each escaping target are placed at their
repository-relative paths under one root, and the builder stage's working
directory is the project's own path under it, so every relative path resolves
there to what it resolves to on the host. Nothing in pyproject.toml or uv.lock
is rewritten. Containment is the rule: a path leaving the repository, or naming
a directory that is not there, is reported with the declaration rather than
skipped, because a silently dropped source would build the image against a
different dependency than the developer builds against.
That build claims the whole tree it assembled (RECIPE_INVENTORY_SCOPE_TREE),
which is what roots the docker context at the assembled output; a build that
only renders a Dockerfile and its ignore still claims exactly those and still
builds the service directory.
Second, the builder stage ran whichever uv the base image had been built with,
a version stated nowhere a project or this agent can read. A project written
against a newer `[tool.uv]` key was diagnosed there as an unknown field — the
base image carries uv 0.5.29, measured, and that uv rejects
build-constraint-dependencies. A recipe meant to be durable and reproducible
cannot leave its resolver implicit, and the Go recipe already states its Go and
Alpine versions outright. The builder stage now copies a pinned uv in before
anything resolves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
antoinetoussaint-byte
force-pushed
the
fix/carry-uv-path-sources-and-pin-uv
branch
from
September 23, 2026 12:29
fc55443 to
d1681ff
Compare
antoinetoussaint-byte
deleted the
fix/carry-uv-path-sources-and-pin-uv
branch
September 23, 2026 12:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent defects made this agent emit a build recipe that resolved on a
developer's machine and failed inside the builder stage. Both are fixes, in
the repository that owns the recipe — neither is a hack, and neither asks a
consumer to restructure a project around the recipe.
1. A
[tool.uv.sources]path leaving the project was not in the contextuv resolves such a path against the directory of the
pyproject.tomlthatdeclares it, and records the same relative path in
uv.lock. The recipe'scontext was the service directory and nothing beside it, so a path naming a
sibling service was simply absent from the builder stage and
uv sync --frozenfailed there withwhile the identical resolution succeeded on the host. Two services sharing one
Python project in a repository is a layout this recipe has to build.
uvsources.goassembles a context when — and only when — the project declares apath that leaves it: the service's own tree and each escaping target are copied
to their repository-relative paths under one root, and the builder stage's
working directory is the project's own repository-relative path under that root.
Every relative path then resolves in the image to what it resolves to on the
host, so nothing in
pyproject.tomloruv.lockis rewritten — a rewritewould have to keep two files agreeing about a path uv reads from both, and a
lockfile is a resolver's output, not an input a recipe gets to edit.
Containment is the rule: an escaping path must resolve inside the repository
that owns the service. One pointing outside it, or at a directory that is not
there, is reported as an error naming the declaration rather than skipped — a
silently dropped source would build the image against a different dependency
than the developer builds against.
The declared inventory scope now matches what the build did. A build that
assembled a context claims the whole tree (
RECIPE_INVENTORY_SCOPE_TREE), whichis what tells the CLI to root the docker context at the assembled output rather
than at the service directory. A build that only rendered a Dockerfile and its
ignore keeps claiming exactly those (
RECIPE_INVENTORY_SCOPE_EMITTED) and keepsthe service directory as its context — that path renders the recipe this agent
has always rendered, minus the uv pin below.
2. The recipe did not state the uv it resolves with
The builder stage ran whichever
uvthe base image had been built with. Thatversion is stated nowhere a project or this agent can read, so a project written
against a newer
[tool.uv]key resolves locally and is diagnosed in the builderstage as an unknown field:
Measured, not assumed —
codeflydev/python:0.0.1carries uv 0.5.29, andthat uv reports
build-constraint-dependenciesas an unknown[tool.uv]field(the run log of
TestRecipeBuilderStageResolvesCurrentUVKeysrecords it).A build recipe is meant to be a durable, reproducible artifact, and one whose
resolver version is a property of its base image is not: the recipe stated its
base image, its platforms and its install flags, and said nothing about the tool
that reads
pyproject.tomlanduv.lock. The Go recipe already keeps theopposite contract — it states its Go and Alpine versions outright. The builder
stage now copies a pinned uv (
uvVersioninmain.go) before anything resolves,which makes the resolver a declared input of the recipe.
This deliberately does not touch the module's
build-constraint-dependencies— the key is there to constrain build backends, and removing it would be a fix
in the wrong repository for a defect that is not the module's.
The base image is still worth bumping, separately
The image's own uv is pinned (by install-script URL) but is old, and it is
declared nowhere outside its Dockerfile — nothing can depend on a version. This
agent also names
codeflydev/python:0.0.1by hand while that companion'smanifest has moved on several versions. Both belong to whoever publishes that
image, and both need an image republish, which is not on this change's path: the
recipe's pin makes the build reproducible without one.
Tests
uv_sources_recipe_test.godrives the realuv: the context as the oldrecipe built it does not resolve the path source, and the context this recipe
assembles does, in a location where the original relative path leads nowhere.
Beside it: the no-op case renders the previous recipe and claims only what it
emitted; a reused recipe directory re-emits rather than tripping over its own
output; a path outside the repository and a path that is not there are both
reported, naming the declaration.
uv_toolchain_recipe_test.gopins the toolchain half: the recipe names anexact uv version and copies it in before resolving, and — against a real
Docker daemon — a project using a current
[tool.uv]key resolves in thebuilder stage, with the
uv --versionthere being the one the recipe names.The base image's own answer to the same project is logged as the evidence for
the pin rather than asserted, since a newer base image knowing the key would
be an improvement and never a regression.
Both new regression tests were confirmed to fail against the unfixed code.
Not verified
nix.fix — rooting a TREE-scope plan's docker context at the assembled output —
is on the CLI's
mainbut is not in any CLI release yet, so a consumerneeds a CLI carrying it before an assembled Python context is built from the
right root.
Builder.Updatepasses its template option where core'sTemplatesexpectsthe template object, so it renders nothing. That is pre-existing, unrelated
and untouched here, but it means
codefly updatedoes not refresh a service'sDockerfile;
Builddoes.🤖 Generated with Claude Code