Skip to content

Fail ordinary source installs when the native compute core cannot be built #445

Description

@Alek99

Summary

xy advertises the native Rust core as required and has no compute fallback, but an ordinary source wheel build quietly succeeds as py3-none-any when Cargo is missing or the Rust build fails. Installation appears successful and only fails when compute is first imported/used.

Evidence

  • The build hook explicitly defines missing Cargo/build failure as a successful coreless install:

    xy/hatch_build.py

    Lines 8 to 13 in 99eda6d

    - **Source builds** (`pip install .` / `-e .` from a clone) compile the core if
    a Rust toolchain is present. **If Rust is absent, the build still succeeds**
    but produces a pure-Python install with no native core — and since there is no
    NumPy fallback, importing the compute layer then raises a clear, actionable
    error (see `xy.kernels`). Install a Rust toolchain, or use a published
    wheel, for a working compute backend.
    and

    xy/hatch_build.py

    Lines 171 to 197 in 99eda6d

    require = os.environ.get("XY_REQUIRE_CARGO") == "1"
    native_src = self._provision_native(root, lib_name, dest, require)
    if native_src is not None:
    # Platform wheel carrying the compiled C-ABI core (one per platform,
    # every CPython version).
    build_data["pure_python"] = False
    build_data["tag"] = f"py3-none-{_platform_tag()}"
    build_data.setdefault("force_include", {})[str(native_src)] = (
    f"xy/_native_lib/{lib_name}"
    )
    else:
    # No toolchain / build skipped: ship a pure-Python wheel (the JS
    # client is included via committed package data). There is no NumPy
    # fallback, so this install imports fine but raises a clear error the
    # moment compute is needed (xy.kernels).
    print(
    "xy: building WITHOUT the native Rust core (cargo not "
    "found or build skipped). This install has no compute backend "
    "and will raise a clear error on first use. Install a prebuilt "
    "wheel or a Rust toolchain (https://rustup.rs) for a working "
    "install.",
    file=sys.stderr,
    )
    build_data["pure_python"] = True
    build_data["tag"] = "py3-none-any"
  • Missing Cargo or a failed build returns None unless XY_REQUIRE_CARGO=1:

    xy/hatch_build.py

    Lines 306 to 331 in 99eda6d

    if shutil.which("cargo") is None:
    if require:
    raise RuntimeError(
    "XY_REQUIRE_CARGO=1 but cargo is not on PATH — a "
    "published wheel must contain the native core."
    )
    return None # graceful: pure-Python wheel
    cmd = ["cargo", "build", "--release"]
    if target:
    cmd += ["--target", target]
    try:
    subprocess.run(cmd, cwd=root, check=True)
    except (subprocess.CalledProcessError, OSError) as e:
    if require:
    raise RuntimeError(f"cargo build failed: {e}") from e
    return None
    resolved = _resolve_built(built, target)
    if resolved is None:
    if require:
    raise RuntimeError(
    f"cargo build succeeded but {built} is missing, and no "
    f"xy_core.* artifact was found in {built.parent}"
    )
    return None
  • CI and release explicitly preserve and celebrate the coreless-success path:

    xy/.github/workflows/ci.yml

    Lines 644 to 690 in 99eda6d

    - name: Build and load native core from sdist
    shell: bash
    env:
    XY_REQUIRE_CARGO: "1"
    run: |
    uv venv smoke-native
    uv pip install --no-cache -p smoke-native dist/*.tar.gz numpy anywidget "reflex>=0.9.6"
    ./smoke-native/bin/python - <<'PY'
    import importlib.metadata as metadata
    import reflex_xy
    import xy
    import xy.kernels as kernels
    version = metadata.version("xy")
    assert xy.__version__ == version
    assert reflex_xy.__version__ == version
    assert kernels.BACKEND == "native", kernels.BACKEND
    print("sdist built and loaded the native Rust core")
    PY
    - name: Verify coreless sdist imports reflex_xy
    shell: bash
    env:
    XY_SKIP_CARGO: "1"
    run: |
    uv venv smoke-no-rust
    uv pip install --no-cache -p smoke-no-rust dist/*.tar.gz numpy anywidget "reflex>=0.9.6"
    ./smoke-no-rust/bin/python - <<'PY'
    import importlib.metadata as metadata
    import reflex_xy
    import xy
    version = metadata.version("xy")
    assert xy.__version__ == version
    assert reflex_xy.__version__ == version
    try:
    import xy.kernels # noqa: F401
    except ImportError as err:
    msg = str(err)
    assert "native Rust core" in msg, msg
    assert "rustup.rs" in msg, msg
    print("coreless sdist imports reflex_xy and errors clearly on compute")
    else:
    raise SystemExit("expected ImportError without the native core")
    PY
    and

    xy/CHANGELOG.md

    Lines 37 to 42 in 99eda6d

    ### Fixed
    - Source-distribution CI and release validation now exercise both installation
    contracts independently: a forced Cargo build must load the native backend,
    while a cache-isolated coreless build must still import `reflex_xy`, report
    the installed version, and raise the documented error only when compute is
    requested.
  • The public install page acknowledges that an apparently successful install is unusable for compute:
    A source build without Rust can finish installing, but it has no compute
    backend and fails with an actionable error when a chart first needs native
    compute. XY does not silently switch to a slower implementation. Building for
    an unsupported operating system or architecture may also require target-specific
    Rust tooling beyond the commands above.

Acceptance criteria

  • Default wheel/editable/sdist installs fail during build if no compatible native core can be produced or reused.
  • Ordinary builds no longer emit a misleading py3-none-any xy wheel with no compute backend.
  • If adapter-only/coreless packaging is still needed internally, require an explicit, clearly named opt-in and prevent that artifact from entering the normal release path.
  • CI covers both successful native source installation and expected early failure without a toolchain.
  • Error text identifies supported wheels/toolchain remediation at install time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions