Repository navigation
packaging: publish releases to pypi, npm and the homebrew tap - #32
Open
IsuraManchanayake wants to merge 4 commits into
Open
IsuraManchanayake wants to merge 4 commits into
IsuraManchanayake wants to merge 4 commits into
Conversation
Open
A package keeps the release's layout, bin/ beside libexec/superfluid, so installed_as() took a Homebrew, pip or npm copy for the install script's in-place one, and `self update` would have moved it under ~/.superfluid/versions behind the package manager's back. A package now carries an `installed-by` file at the release's root, one line `<manager>: <command>`. installed_as() reads such a copy as not ours, `self update` and `self list` name the manager and its command, and the line `serve` prints for a newer release gives that command instead of `superfluid self update`.
…token crates-verify packages every crate and builds each from its package, as crates.io will see it, with no token in the job, so the upload's --no-verify never runs a build script while holding CARGO_REGISTRY_TOKEN and a crate that does not build from its package stops the release before anything goes up. It runs on macOS, where the MLX adapter's crate builds too. Both jobs pin actions/checkout and dtolnay/rust-toolchain to commits, use Rust 1.99.1 rather than stable, and drop the checkout's credentials.
publish.yml repackages the tarballs release.yml attaches, when a release is published or by hand for a tag: one wheel per platform for superfluid-cli on PyPI, @basecompute/superfluid on npm with one optional package per platform, and a formula PR on basecompute/homebrew-tap. `superfluid` is taken on both PyPI and npm. Every run builds every package, checks the wheels with twine, and installs each kind on macOS and on Linux x86-64 and arm64 (packaging/try-install.sh). It uploads only with PUBLISH_ENABLED set, dry_run unticked, the release published rather than a draft or pre-release, and the run at the commit the tag names, so a branch run is always a dry run. Each upload job asks its registry first and skips what is there; PyPI uses trusted publishing with attestations, npm --provenance, and Homebrew gets a PR, never a push. Actions are pinned to commits and Python, Node and twine to versions. Each package carries the release tree unchanged plus its installed-by file. packaging/release.py checks a tarball against its .sha256, takes only regular files and folders under its one top folder, checks the binaries are complete and built for the target, and sets modes by Mach-O or ELF header. The wheel's platform tag (macosx_15_0_arm64, manylinux_2_28_*) is read from the binaries, so pip refuses it where they cannot run. tests/scripts/test_packaging.py builds every kind from made-up releases and checks the launchers and the names the daemon and release.yml share with packaging/; CI runs it.
IsuraManchanayake
force-pushed
the
feat/package-distribution
branch
from
October 26, 2026 00:55
bf62feb to
8c76efd
Compare
This branch has not been deployed
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.
What changed
Each release can now also go out on PyPI, npm and Homebrew. Nothing uploads until the repository variable
PUBLISH_ENABLEDistrue; until then every run is a full dry run.superfluid-climacosx_15_0_arm64,manylinux_2_28_x86_64,manylinux_2_28_aarch64): the release tree plussuperfluid,superfluid-workerdandsuperfluid-nodedlaunchers thatexecvthe binaries@basecompute/superfluid@basecompute/superfluid-{darwin-arm64,linux-x64,linux-arm64}, restricted by os/cpu/libcbasecompute/tap/superfluidlibexecand links the commands. It goes to the tap as a PR, never a pushsuperfluidis already taken on PyPI (a Superfluid Protocol SDK) and on npm..github/workflows/publish.ymlruns when a release is published (release: released, since release.yml only drafts one) and by hand for a tag. It downloads the tarballs, checks their.sha256, builds every package, runstwine check --strict, and installs each kind on macos-15, ubuntu-24.04 and ubuntu-24.04-arm (Homebrew on the first two). It uploads only when all of these hold:PUBLISH_ENABLEDistrueanddry_runis not ticked;git ls-remoteonrefs/tags/vXandrefs/tags/vX^{}), so a dispatch from a branch is always a dry run.Each upload job first checks what its registry has: all three wheels and all four npm packages, not just the version. A rerun only finishes what is missing. PyPI uses trusted publishing with attestations, npm uses
--provenance; both are possible because the repo is public. Actions are pinned to commit SHAs, and Python 3.12.15, Node 24.21.0 and twine 7.0.0 to exact versions.packaging/:release.pychecks a tarball's SHA-256 and takes only regular files and folders under its one top folder (no symlinks,.., or AppleDouble._entries).LC_BUILD_VERSIONminimum on macOS, the newestGLIBC_2.Non Linux.python/build_wheel.pywrites the wheel directly. That keeps the exec bits, makes builds reproducible and needs no network.npm/build_packages.pyandhomebrew/formula.pybuild the npm tarballs and the formula;build.pyruns all three.try-install.shinstalls one kind somewhere throwaway and checks it. publish.yml and local runs both use it.packaging/README.mdcovers how it fits together and the registry setup.self updatevs package managers (selfupdate.rs): a package keeps the release layout,bin/besidelibexec/superfluid. Soinstalled_as()read a Homebrew, pip or npm copy as the install script's in-place copy, andself updatewould have moved it under~/.superfluid/versions. Confirmed with the 0.1.4 binaries:self listfrom a pip install said "this superfluid is the install script's copy …".installed-byat the release root (pip: pip install --upgrade superfluid-cli, etc.).self update/self listthen say "… was installed by pip;pip install --upgrade superfluid-cliupdates it", and the daily "newer release" line names the same command.crates.io (
release.yml): a newcrates-verifyjob, with no token, runscargo package --workspace --lockedon macos-14, so every crate (MLX's included) builds from its packaged form. The upload job now needs it and keeps--no-verify, so no build script runs while it holdsCARGO_REGISTRY_TOKEN. Both jobs pinactions/checkoutanddtolnay/rust-toolchainto SHAs, use Rust 1.99.1 instead ofstable, and setpersist-credentials: false.Tests and docs:
tests/scripts/test_packaging.pyhas 20 tests on made-up releases: staging and its refusals, platform tags, wheel RECORD/modes/metadata, the npm manifests, the formula, both launchers (argument and exit-status passthrough, repair message), and contract checks thatLINKED/INSTALLED_BYinselfupdate.rs, release.yml's targets andlaunch.jsagree withpackaging/.packaging scriptsjob.installation.mdandupdating.mddescribe the new channels (published from 0.1.5 on), and the CHANGELOG has 0.1.5 entries.Why
So superfluid installs with the tools people already use (
brew install,pipx install,npm i -g) and updates through them, withoutself updatefighting the package manager. The shipped binaries are all Apache-2.0: llama.cpp, MLX and baseRT are installed at runtime, not bundled. So no platform needs its own licence gate.It does not touch basert's release-asset contract: the tarballs,
compat.jsonandsuperfluid-basert-abi-<N>.txtstay as they are, and basert never uses a superfluid on PATH.How it was tested
All on an M5 Pro Mac against the real v0.1.4 release assets. Nothing was uploaded anywhere.
python3 packaging/build.py <v0.1.4 assets> out --version 0.1.4built 3 wheels, 4 npm tarballs andsuperfluid.rb.twine check --strict(7.0.0) passes all three wheels.packaging/try-install.sh pip|npm|brew out 0.1.4, all three pass:macosx_15_0_arm64wheel by its tag.superfluid --versionprintssuperfluid 0.1.4.--help,superfluid-workerd/superfluid-noded, andpython -m superfluid_cliall run.runtimes --jsonfinds the llamacpp and mlx workers insidesite-packages/superfluid_cli/release/libexec/superfluid.npm install --global --prefix <tmp>(npm 11.16) of the darwin-arm64 package plus the main one passes the same checks, with the workers undernode_modules/@basecompute/superfluid-darwin-arm64/release.brew styleandbrew audit --strictare clean, thenbrew install(from the GitHub URL) andbrew testpass and the same checks hold underCellar/superfluid/0.1.4/libexec. It was uninstalled and untapped afterwards.Launchers: both pass through exit status 2 from an unknown command, and both put back exec bits that were stripped from the release (found by header).
installed-byend to end: this branch'ssuperfluidswapped into the 0.1.4 tree and installed as a wheel.self update --dry-runexits 1 with "… was installed by pip;pip install --upgrade superfluid-cliupdates it", andself listsays the same.The plan step's shell was run locally against v0.1.4: it uploads only with
PUBLISH_ENABLED=true,dry_runfalse and the run at abed852 (the tag's commit), and refusesmainas a tag.cargo package --workspace --locked(the new crates-verify step) packages and builds all 19 crates on macOS.python3 -m unittest discover -s tests/scripts: 20 pass on Python 3.9 and 3.14.cargo clippy -p superfluid --all-targets -- -D warningscargo test -p superfluid(622 passed, 0 failed)Changes a wire protocol, the WAL format or the tick ABI: no.
Not tested:
manylinux_2_28_x86_64/_aarch64, ELF arch, modes). publish.yml's ubuntu jobs will be the first real install, and the Linux Homebrew run depends on the ubuntu-24.04 image's/home/linuxbrew.release: releasedandworkflow_dispatchonly fire once it is onmain. No upload has been tried.crates-verifyjob on a hosted macos-14 runner, as opposed to locally.self updaterefusal insidetry-install.sh(it only applies to versions after 0.1.4) was checked by hand as above, not through the script.Before the first upload (registry setup, for an admin)
superfluid-cli: ownerbasecompute, reposuperfluid, workflowpublish.yml, environmentpypi.basecomputeorg on npmjs.com (shared with baseRT's planned@basecompute/basert). Make a granular token that can publish to@basecomputeand store it as secretNPM_TOKENin environmentnpm. Trusted publishing can replace the token once the four packages exist.brew install basecompute/tap/superfluidworks for anyone. Make a fine-grained token (Contents + Pull requests write, tap only) and store it as secretHOMEBREW_TAP_TOKENin environmenthomebrew.pypi,npmandhomebrew, each limited to tagsv*, with reviewers if wanted.PUBLISH_ENABLED=true.This targets
rc-0.1.5(#33), so 0.1.5 is the first release that can upload: the upload requires the run to be at the tag's commit, and 0.1.4's commit has no publish.yml. 0.1.5 is also the first release whose binaries readinstalled-by. Once this is onmain, a dispatch with tagv0.1.4dry-runs the whole pipeline.