Skip to content

Ship fbuild as a raw native binary, drop the Python wrapper (cmd → fbuild.exe directly) #746

Description

@zackees

Problem

fbuild is distributed via PyPI with [project.scripts] fbuild = "ci.bin_launcher:main". That makes the fbuild on PATH a pip-generated console-script .exe that imports ci.bin_launcher and os.execvs the real native binary bundled at ci/bin/fbuild[.exe].

On POSIX, execv replaces the process — semantically equivalent to cmd → rust binary. On Windows, os.execv is emulated as CreateProcess + parent exit (no true exec). The Python shim returns to cmd.exe before the spawned Rust binary finishes flushing its output, so the next shell prompt races ahead of fbuild's stdout. Repro:

C:\Users\niteris\dev\fbuild>fbuild port scan

C:\Users\niteris\dev\fbuild>COM25     303A:1001    USB Serial Device (COM25)    ser=80:F1:B2:D1:DF:B1
          └─ Espressif Systems / ESP32-S3 USB-CDC
...

Notice the next C:\Users\niteris\dev\fbuild> prompt prints before the scan output. Looks like output went to stderr; it didn't — it's the launcher exiting before the child's stdout drains.

Running the bundled binary directly (ci\bin\fbuild.exe port scan) preserves ordering perfectly. The problem is purely the Python shim.

Root cause

The Python shim exists at all only because [project.scripts] in PEP 621 is strictly for Python entry points. To ship a native binary through a wheel, the project used a Python launcher (ci/bin_launcher.py) that execvs into the staged binary — which doesn't work as advertised on Windows.

Fix

Drop the Python shim entirely. Ship the cargo-built binary directly via setuptools scripts= (legacy interface, well-supported, used by maturin/cargo-dist/scikit-build for exactly this). Files in scripts= land in <name>-<version>.data/scripts/ inside the wheel and pip copies them straight into the venv Scripts/ (Windows) or bin/ (POSIX) directory as-is — pip does NOT generate a wrapper for .exe files. Result: cmd → fbuild.exe → stdout, no Python in the chain.

Plan

  1. pyproject.toml:
    • Remove [project.scripts] fbuild = "ci.bin_launcher:main"
    • Remove ci from [tool.setuptools] packages (no longer needs to be importable for the entry point; keep it as a directory only if other Python helpers in ci/ are still imported by anything — they aren't from end users)
  2. setup.py:
    • Pass scripts=[str(STAGED_BINARY_PATH)] to setup(). The BuildWithCargo build_py cmdclass already stages the binary into ci/bin/ before packaging, so the path resolves at sdist/wheel build time.
    • Verify ordering: build_py must run before the build_scripts/install_scripts phases read the script list. Setuptools' default ordering handles this, but document it.
  3. Delete ci/bin_launcher.py and its __pycache__.
  4. Update pyproject.toml [tool.setuptools.package-data] — drop ci = ["bin/fbuild*"] since the binary is no longer part of a package, it's a script.
  5. Verify the wheel layout (unzip -l dist/fbuild-*.whl): expect fbuild-2.2.31.data/scripts/fbuild.exe, NOT ci/bin/fbuild.exe.

Acceptance criteria

  • pip install --force-reinstall . produces a fbuild on PATH that is a real native exe (not a pip console-script stub).
    • Windows: file \$(where fbuild) reports PE32+ executable, not Zip archive, with extra data prepended.
    • POSIX: file \$(which fbuild) reports ELF/Mach-O, not a Python shebang.
  • cmd.exe /c 'echo === BEFORE === & fbuild port scan & echo === AFTER ===' shows === AFTER === after the scan output, not before.
  • All existing fbuild functionality keeps working (smoke: fbuild --version, fbuild port scan, fbuild build).
  • Release workflow (.github/workflows/release-auto.ymlci/publish.py) still produces installable platform-tagged wheels.

Why this is better than the current subprocess.run workaround (#TBD)

#TBD landed a Windows-specific subprocess.run + wait fix in ci/bin_launcher.py that keeps the shim alive until the child exits. That fixes output ordering but still incurs Python startup on every fbuild invocation (~80–150 ms wasted) and keeps a non-trivial shim in the load path. Direct binary shipping removes both costs and the maintenance surface for the shim.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Status
    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions