|
| 1 | +# Packaging |
| 2 | + |
| 3 | +The CLI is a single Go binary. Homebrew, Scoop, deb and rpm are produced by |
| 4 | +goreleaser directly; npm and PyPI are not registries goreleaser publishes to, so |
| 5 | +they are built here from the binaries goreleaser has already produced. |
| 6 | + |
| 7 | +Nothing in this directory compiles anything. Both builders take a directory of |
| 8 | +extracted release binaries and repackage them, so every channel ships the same |
| 9 | +bytes for a given tag. |
| 10 | + |
| 11 | +``` |
| 12 | +artifacts/ |
| 13 | + darwin_amd64/modelslab |
| 14 | + darwin_arm64/modelslab |
| 15 | + linux_amd64/modelslab |
| 16 | + linux_arm64/modelslab |
| 17 | + windows_amd64/modelslab.exe |
| 18 | + windows_arm64/modelslab.exe |
| 19 | +``` |
| 20 | + |
| 21 | +## npm — `packaging/npm/build.mjs` |
| 22 | + |
| 23 | +```bash |
| 24 | +node packaging/npm/build.mjs v1.2.3 artifacts dist/npm |
| 25 | +``` |
| 26 | + |
| 27 | +Produces seven packages: one entry package (`modelslab-cli`) whose only content |
| 28 | +is a launcher shim, plus one package per platform holding just the binary and |
| 29 | +the `os`/`cpu` fields npm filters on. The entry package declares the six as |
| 30 | +`optionalDependencies`, so npm installs exactly the one that matches. |
| 31 | + |
| 32 | +The alternative — one package with a postinstall that downloads a binary — was |
| 33 | +rejected deliberately. It needs network at install time and produces a silently |
| 34 | +broken install under `npm ci --ignore-scripts`, which many CI and agent sandboxes |
| 35 | +set. Six small packages buy an install that cannot half-work. |
| 36 | + |
| 37 | +**Publish platform packages before the entry package.** The entry package pins |
| 38 | +exact versions of all six; publishing it first leaves a window where every |
| 39 | +install fails. |
| 40 | + |
| 41 | +## PyPI — `packaging/pypi/build.py` |
| 42 | + |
| 43 | +```bash |
| 44 | +python3 packaging/pypi/build.py v1.2.3 artifacts dist/pypi |
| 45 | +``` |
| 46 | + |
| 47 | +Produces one wheel per platform, each containing the binary and a console script |
| 48 | +that `execv`s it. Wheels are written directly rather than through a build |
| 49 | +backend: there is nothing to compile, so a backend would only add a dependency, |
| 50 | +and writing them here keeps the platform tag explicit instead of inferred from |
| 51 | +whatever host ran the build. |
| 52 | + |
| 53 | +Two things that are easy to get wrong and are covered by CI: |
| 54 | + |
| 55 | +- **Version normalisation.** Tags are semver, wheel filenames are PEP 440. A |
| 56 | + `v1.2.3-rc1` tag naively becomes `modelslab_cli-1.2.3-rc1-py3-none-*.whl`, |
| 57 | + which pip reads as version `1.2.3` with build tag `rc1` — and build tags must |
| 58 | + start with a digit, so the file is invalid. `normalise_version()` converts it |
| 59 | + to `1.2.3rc1`. |
| 60 | +- **The executable bit.** pip decides whether to mark an unpacked file executable |
| 61 | + with `stat.S_ISREG(mode) and mode & 0o111`, so the zip entry's mode has to |
| 62 | + carry the file-type bits, not just permissions. A bare `0o755` fails `S_ISREG`, |
| 63 | + the binary lands `0o644`, and the first run dies with `EPERM`. `twine check` |
| 64 | + passes either way; only installing and running catches it. |
| 65 | + |
| 66 | +## Releasing |
| 67 | + |
| 68 | +`.github/workflows/release.yml` runs both builders on a tag and publishes if the |
| 69 | +corresponding token is configured. Missing tokens skip that registry rather than |
| 70 | +failing the release. |
| 71 | + |
| 72 | +| Secret | Registry | |
| 73 | +| --- | --- | |
| 74 | +| `NPM_TOKEN` | npm (automation token with publish rights) | |
| 75 | +| `PYPI_TOKEN` | PyPI (project or account API token, used as `__token__`) | |
| 76 | + |
| 77 | +`.github/workflows/ci.yml` builds both on every PR and installs and *runs* the |
| 78 | +result, because both of the failure modes above pass every static check. |
0 commit comments