Repository navigation
fix(build): build a uv workspace member project, with the interpreter it requires - #73
Merged
Merged
Conversation
… it requires
A service whose project (code/) is a member of a uv workspace keeps its one
uv.lock, and its sibling members, at the workspace root in the service. `uv
sync` from code/ resolves against that root on the host, which is what the
native runner does, but the image recipe copied code/pyproject.toml and
code/uv.lock only, so the build failed on a lock that is not in code/. A
library whose members name each other `{ workspace = true }` has to be laid
out this way: uv honours that source only under an ancestor root.
The recipe now detects the enclosing workspace, copies it whole (ignoring
virtual environments and caches), and syncs the project from its own
directory. A root outside the service, a root without a lock, or a path source
leaving the service is refused with the reason. `--no-editable` installs
members and path sources into the venv as packages, since the runtime stage
carries only the venv.
A project whose requires-python the base image's Python does not satisfy gets
the interpreter uv installs for it, as on the host; it is now installed where
the runtime stage copies it from, so the venv's interpreter exists at runtime.
Without that copy the venv's python dangled and the base interpreter ran
without the project's packages.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Why
A composed module's python-fastapi service keeps its project in
code/as a member of a uv workspace rooted at the service directory. The oneuv.lockand the sibling members live at that root. The layout is required, not a preference: the vendored packages name each other{ workspace = true }, which uv honours only under an ancestor root.uv syncfromcode/resolves it on the host, and the agent's native runner does exactly that. The image recipe copiedcode/pyproject.toml code/uv.lockand nothing else, so the image build failed on a lock that isn't incode/. The recipe did not build what the host resolves, contradicting the rule this agent states for[tool.uv.sources]("resolve in the builder stage exactly as it resolves on the host").Which side is wrong: the agent. The layout is valid uv, the agent's own runtime path honours it, and only the build recipe didn't.
Building that project exposed a second defect in the same recipe. The project requires Python
>=3.11,<3.12, and the base image carries 3.13. uv installs 3.11 for it in the builder stage, as it does on the host, but under/root, which the runtime stage never copied. So the venv'spythondangled, and the base interpreter ran without the project's packages.Change
enclosingUVWorkspace: whencode/has nouv.lock, it walks up (within the repository) to the first[tool.uv.workspace]and checks themembers/excludeglobs. A listed member of a root inside the service is built as a workspace member. Refused, with the reason:uv.lock;COPY <root> <root>,WORKDIR /app/<root>/<member>, and a dockerignore excluding the ephemeral directories the assembled context already skips (.venv, caches,.git, …). A standalone project's recipe keeps its shape.uv sync ... --no-editable: members and path sources become packages in the venv rather than links to builder-stage sources.UV_PYTHON_INSTALL_DIR=/opt/uv/python, copied into the runtime stage. For a project the base Python satisfies, the directory is empty.Classification: fix, at the owner (the agent's image recipe).
Verified
exclude, unlisted, no lock, root outside the service);{ workspace = true }library andrequires-python >=3.11,<3.12builds, and its image runs Python 3.11 and imports the library.ModuleNotFoundError, because the base 3.13 ran).go test -race ./...passes (193s).Not fixed here: the next failure for that service, and its root cause
The same build then stops on a third-party dependency,
headroom-ai==0.37.0. It publishes only manylinux (glibc) wheels, and this agent's base image is the Alpine (musl) companion. So uv has to compile it from source (maturin, which needs Rust and a C toolchain), and the companion deliberately carries no compiler. Its Dockerfile says services needing native builds should overridedocker-image:. ButBuildrendersFROM {{.Builder}}from the package-levelruntimeImageand ignores that override (only the runtime path resolves it), so a service cannot choose a base image with wheels or a toolchain for its image build. That is a platform decision (a glibc companion, or honouring the override for the build image), not part of this fix.🤖 Generated with Claude Code