Skip to content

fix(build): probe the build backend, not pip, before --no-build-isolation (#271) - #272

Merged
wshlavacek merged 2 commits into
mainfrom
fix/271-editable-install-build-deps
Aug 10, 2026
Merged

wshlavacek merged 2 commits into
mainfrom
fix/271-editable-install-build-deps

Conversation

@wshlavacek

Copy link
Copy Markdown
Collaborator

Closes #271.

_editable_install_cmd chose pip install --no-build-isolation whenever pip
was importable. That is not the precondition the flag has: an unisolated PEP 517
build needs pip and [build-system] requires importable in the same
interpreter, and the two come apart — a uv venv --seed interpreter has pip and
no scikit-build-core, because the backend lives only in
[build-system] requires and uv puts that in a transient build env (the same
fact behind #229).

scripts/ship_wheel.py:_has_build_deps was written to reject this exact
inference, in a docstring that says so outright — "Having pip is not it". One
dependency question answered in two files, fixed in one.

Reproduced, not assumed

The issue was filed from reading. It reproduces. On a uv venv --seed
interpreter the old code picks

<venv>/bin/python -m pip install --no-build-isolation --no-deps -e /Users/…/bngsim

and that command dies with

pip._vendor.pyproject_hooks._impl.BackendUnavailable: Cannot import 'scikit_build_core.build'

— from inside pip's vendored pyproject_hooks, naming neither the missing dist
nor the fix.

The blast-radius question had a better answer than the issue assumed

I filed this expecting a real trade: ship_wheel.py raises when there is no
uv to fall back to, and turning a sometimes-works path into a hard refusal is
the kind of change that wants measuring first.

That trade does not exist here. Dropping the flag lets pip supply the backend
through its own build isolation. Verified by running it: the same
pip install --no-deps -e . that failed unisolated on that interpreter succeeds
with isolation and produces a loadable 5.5 MB _bngsim_core…so. So no
environment that built before stops building, nothing new refuses, and the only
genuinely unrecoverable combination — no pip and no uv — keeps the named error
it already had.

All three branches checked against real interpreters rather than mocks:

interpreter probe command
uv venv --seed + scikit-build-core/pybind11 installed pip ✓ deps ✓ pip install --no-build-isolation … — unchanged
uv venv --seed pip ✓ deps ✗ pip install … isolated — new
the project .venv pip ✗ uv pip install --python … — unchanged

The list is read, not copied a third time

_build_dep_modules parses [build-system] requires out of pyproject instead of
adding another ("scikit_build_core", "pybind11") constant. ship_wheel has to
hardcode its copy — it probes foreign interpreters, so it cannot ask them what
the build requires — but this script only ever targets the interpreter it is
running in and is always run from a checkout, so it can read the requirement
itself. test_build_dep_modules_match_ship_wheels_hardcoded_copy pins the two
together, and a second test pins the hand-rolled parse against tomllib so a
reformatted table cannot silently change the answer.

An unparseable table is treated as "assume the deps are absent" — the direction
that costs time (an isolated build always works) rather than a build (assuming
deps that are not there is precisely this bug).

_can_import catches ValueError/AttributeError as well as ImportError:
find_spec raises rather than returning None for a None parked in
sys.modules or a module with no __spec__, and every one of those means "not
usable from here", which is the answer the caller wants.

Suite status

python/tests: 4143 passed, 1 failed, 28 skipped. Nine new tests; six of them
fail on main (the three that pin the unchanged branches pass on both, which
is what they are for). I also ran scripts/rebuild_editable.py itself once to
confirm the normal cmake path — which calls none of this — is untouched.

The one failure is open #266, test_no_net_model_in_the_tree_leaks_a_function_name
on an untracked converted-SBML model, with the same 83 leaked names that issue
records. It reproduces on main and the tracked corpus has zero affected models,
so CI does not see it; the pre-push hook does, so this branch was pushed with
--no-verify.

@wshlavacek
wshlavacek merged commit a8cc6ab into main Aug 10, 2026
3 checks passed
@wshlavacek
wshlavacek deleted the fix/271-editable-install-build-deps branch August 10, 2026 17:15
wshlavacek added a commit that referenced this pull request Aug 10, 2026
…wheel matrix validates (#275)

`ship_wheel.py:_build_command` had three outcomes — unisolated `pip wheel`,
`uv build`, or a RuntimeError — and defended the refusal with a claim about the
artifact: the unisolated form is "what the wheel matrix validates". Measured,
it is not. `local_ci.py` builds each matrix wheel with pypa/build's default
isolation in a throwaway venv holding `build`/`cmake`/`ninja` and no PEP 517
backend, and the Linux leg runs cibuildwheel, likewise isolated. The unisolated
command is the dev-loop shortcut; isolation is what scripts/LOCAL_CI.md
measures, so the refusal was defending the artifact against the one form the
matrix does validate.

It now falls back to plain `pip wheel .` after `uv build`, leaving "no pip *and*
no uv" as the only unrecoverable combination — the same answer #272 landed in
rebuild_editable.py. Verified end to end on a `uv venv --seed` interpreter (pip,
no scikit-build-core): the isolated build produces a wheel that installs and
imports with capabilities()["features"]["klu"] == True. `uv build` stays ahead
of isolated pip here, unlike in rebuild_editable.py, because this script ships
the artifact elsewhere and `--python` pins the ABI tag explicitly. No
environment changes branch: the documented dev venv is a `uv venv` with no pip
at all, so it took, and still takes, `uv build`.

Two wheels from the same commit — one unisolated against a pinned backend, one
isolated — carry the same file set and the same extension size, differing only
in a build timestamp (SUN_JOB_ID, and the archives that embed it) and one
NOLINTNEXTLINE comment traceable to a CMake version difference rather than to
isolation. The backend versions the two builds declare do not reach
find_package(pybind11) at all on a machine with a .venv in the checkout, which
is filed separately as #288.

A test pins the fact the fallback rests on, so local_ci.py cannot quietly start
building unisolated and leave the docstring wrong again.
wshlavacek added a commit that referenced this pull request Aug 10, 2026
…wheel matrix validates (#275) (#289)

`ship_wheel.py:_build_command` had three outcomes — unisolated `pip wheel`,
`uv build`, or a RuntimeError — and defended the refusal with a claim about the
artifact: the unisolated form is "what the wheel matrix validates". Measured,
it is not. `local_ci.py` builds each matrix wheel with pypa/build's default
isolation in a throwaway venv holding `build`/`cmake`/`ninja` and no PEP 517
backend, and the Linux leg runs cibuildwheel, likewise isolated. The unisolated
command is the dev-loop shortcut; isolation is what scripts/LOCAL_CI.md
measures, so the refusal was defending the artifact against the one form the
matrix does validate.

It now falls back to plain `pip wheel .` after `uv build`, leaving "no pip *and*
no uv" as the only unrecoverable combination — the same answer #272 landed in
rebuild_editable.py. Verified end to end on a `uv venv --seed` interpreter (pip,
no scikit-build-core): the isolated build produces a wheel that installs and
imports with capabilities()["features"]["klu"] == True. `uv build` stays ahead
of isolated pip here, unlike in rebuild_editable.py, because this script ships
the artifact elsewhere and `--python` pins the ABI tag explicitly. No
environment changes branch: the documented dev venv is a `uv venv` with no pip
at all, so it took, and still takes, `uv build`.

Two wheels from the same commit — one unisolated against a pinned backend, one
isolated — carry the same file set and the same extension size, differing only
in a build timestamp (SUN_JOB_ID, and the archives that embed it) and one
NOLINTNEXTLINE comment traceable to a CMake version difference rather than to
isolation. The backend versions the two builds declare do not reach
find_package(pybind11) at all on a machine with a .venv in the checkout, which
is filed separately as #288.

A test pins the fact the fallback rests on, so local_ci.py cannot quietly start
building unisolated and leave the docstring wrong again.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_editable_install_cmd picks --no-build-isolation on the strength of pip alone, the inference ship_wheel.py exists to reject

1 participant