bpm is the official package manager for the Bnlang programming language. It installs and publishes both pure-bnl libraries and native plugins (.dll / .so / .dylib), backed by a versioned registry with integrity-checked downloads.
- Single binary — Go-based, no runtime dependencies.
- Reproducible — every install writes a
bnl.lockthat pins direct + transitive deps to a concrete version plus a SHA-256 integrity hash. - Semver resolution —
^1.2.3,~1.2,>=1.0 <2, and exact1.2.3all work. - Native plugin friendly — publish a single source package; resolves per-platform on install (
windows-x64,linux-x64,darwin-arm64, …). - Tokens for CI — long-lived bearer tokens scoped per machine, listable and revocable from the CLI.
Download the latest bpm binary for your platform from the Bnlang releases page and put it on your PATH.
Requires Go 1.23 or newer.
git clone https://github.com/bnlang/bpm.git
cd bpm
go build -o bpm # produces ./bpm on Linux/macOS, .\bpm.exe on WindowsVerify:
bpm --helpbpm is pure Go (no cgo), so cross-compilation works from any host. The repo ships scripts that produce stripped, version-stamped binaries for every supported platform — each one packed into a .zip archive with a single bpm (or bpm.exe) executable at the root:
# Linux / macOS
./build.sh # all six targets
./build.sh windows-x64 # one target only
# Windows
.\build.ps1
.\build.ps1 windows-x64Output layout matches the platform strings bpm uses elsewhere:
| Target | Output | Archive contents |
|---|---|---|
windows-x64 |
dist/bpm-windows-x64.zip |
bpm.exe |
windows-x86 |
dist/bpm-windows-x86.zip |
bpm.exe |
linux-x64 |
dist/bpm-linux-x64.zip |
bpm |
linux-arm64 |
dist/bpm-linux-arm64.zip |
bpm |
darwin-x64 |
dist/bpm-darwin-x64.zip (Intel Macs) |
bpm |
darwin-arm64 |
dist/bpm-darwin-arm64.zip (Apple Silicon) |
bpm |
Each archive is ~3.3–3.7 MB compressed (~8–9 MB uncompressed). Users download one zip, unpack it, drop the binary onto PATH, done.
Override the version stamp with VERSION=v1.1.0 ./build.sh (or $env:VERSION = "v1.1.0"; .\build.ps1) when cutting a release. The scripts use CGO_ENABLED=0, -trimpath, and -ldflags="-s -w" so the resulting binaries are static.
build.sh requires the zip command (preinstalled on most Linux/macOS hosts; apt install zip / brew install zip if missing). build.ps1 uses the built-in Compress-Archive, no extra tooling required.
For a single ad-hoc build with no packaging, the underlying command is:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o bpm .# 1. (Optional) Point bpm at a different registry. The default is the
# official one at https://bpm.bnlang.dev — set BPM_REGISTRY to override.
export BPM_REGISTRY=https://bpm.bnlang.dev
# 2. Authenticate (interactive — email + password, stored as a bearer token
# under ~/.bnl/auth.json with mode 0600).
bpm login
# 3. Inside any directory:
bpm init # write a starter bnl.json
# 4. Add and install a dependency.
bpm install some-package
bpm install some-package@1.4.0 # pinned exact version
bpm install -g some-package # install globally under ~/.bnl/deps/
# 5. Use it from bnl code.
# src/main.bnl:
# import "some-package" as pkg;
# print(pkg.hello());
bnl src/main.bnlTo install everything declared in an existing project's bnl.json (e.g. on a fresh checkout):
bpm install| Command | Purpose |
|---|---|
bpm init |
Create a starter bnl.json in the current directory. |
bpm install |
Install every dependency listed in bnl.json. |
bpm install <name>[@version] |
Add and install one package; pin into bnl.json. |
bpm install -g <name> |
Install globally under ~/.bnl/deps/<name>/. |
bpm uninstall <name> [-g] |
Remove an installed dependency. |
bpm list [-g] |
Show installed packages with their versions. |
bpm signup |
Create an account on the active registry. |
bpm login |
Authenticate with the registry. Prompts for email + password. |
bpm logout |
Forget the stored token for the active registry. |
bpm publish |
Publish the current package. Auto-detects pure-bnl vs. native. |
bpm publish --platform <os-arch> --binary <path> |
Publish a single native-plugin platform manually. |
bpm token create --label <label> |
Mint a CI token scoped to a label. |
bpm token list |
List your active tokens (id + label only, never the secret). |
bpm token revoke <id> |
Revoke a token. |
Global flags accepted by every command:
--registry <url>— override the registry for this invocation (overridesBPM_REGISTRY).-q,--quiet— suppress non-error output.
-g,--global— install into~/.bnl/deps/instead of./deps/.-f,--force— reinstall registry packages even ifdeps/<name>/bnl.jsonalready reports the requested version. By default a no-opbpm installskips already-installed packages and prints→ name@version (cached);--forcere-downloads and re-unpacks every one. Localfile:dependencies are always re-unpacked regardless.--ignore-failed— keep going when an individual package fails. The failing package is skipped (nobnl.json/bnl.lockentry written for it) and a summary of skipped packages is printed at the end. The default is strict — any failure aborts the whole install.--platforms <list>— comma-separated platforms the dep applies to (e.g.windows-x64,darwin-arm64,darwin-x64). Writes the dep as a scoped object inbnl.json(see Platform-scoped dependencies below). On any platform outside the list the dep is ignored — not installed, not in the lockfile, no warning. On listed platforms it remains required, so a missing registry asset is still a hard error.-O,--optional— tolerate install failure (no version match, no asset for this platform, network error) with a warning instead of aborting. Writes the dep as a scoped object with"optional": true. Use this for best-effort deps where you actively want to be told it didn't install but don't want it to break the build. Pair with--platformsto be both scoped and lenient inside the scope.
{
"name": "mathx",
"version": "1.2.0",
"description": "Vector and matrix helpers.",
"license": "MIT",
"homepage": "https://github.com/example/mathx",
"repository": "https://github.com/example/mathx",
"main": "src/index.bnl",
"dependencies": {
"core-utils": "^1.1.0",
"logger": "~0.4"
}
}Required: name. Everything else is optional; only name, main, and native are read by the Bnlang runtime itself — bpm uses the rest for the registry.
A dependency value can be a plain version string or an object that scopes the dep to specific platforms and/or marks it as best-effort:
{
"dependencies": {
"torch-bnlang": "^1.1.0",
"onnxruntime-genai-bnlang": {
"version": "^0.1.0",
"platforms": ["windows-x64", "darwin-arm64", "darwin-x64"]
},
"fast-tokenizer-bnlang": {
"version": "^0.3.0",
"optional": true
}
}
}Two independent fields:
platforms— where this dep applies. Outside the listed platforms the dep is silently ignored: not resolved, not installed, not in the lockfile. On listed platforms it's required as normal.optional— whether failure is tolerable. Whentrue, an install failure (no asset for this platform, no version satisfying the spec, network error) becomes a warning instead of aborting. Both fields can be combined.
platforms set? |
current platform listed? | optional? |
behavior |
|---|---|---|---|
| no | — | no | required everywhere (default) |
| no | — | yes | tried everywhere; warn-skip on failure |
| yes | yes | no | required here; hard error on failure |
| yes | yes | yes | tried here; warn-skip on failure |
| yes | no | — | not applicable, silent |
Use --platforms / -O on bpm install <name> to write either field via the CLI — no need to hand-edit bnl.json.
When publishing a C/C++ FFI plugin, declare the freshly-built binary path per platform. bpm picks the right one at install time and renames it to a canonical filename:
{
"name": "mathx-plugin",
"version": "0.3.1",
"targets": {
"windows-x64": "build/windows/mathx.dll",
"linux-x64": "build/linux/mathx.so",
"darwin-arm64": "build/darwin/mathx.dylib"
}
}Every other regular file in the binary's directory is shipped alongside (sqlite3.dll, libssl.dylib, etc.). Use a .bpmignore file (gitignore syntax) to exclude .pdb, .ilk, intermediate build artifacts, and similar noise.
Supported platform strings: windows-x64, windows-x86, linux-x64, linux-arm64, darwin-x64, darwin-arm64.
The installed manifest (under deps/<name>/bnl.json) replaces targets with a single native: "<canonical>" entry — the runtime looks at exactly that field.
bpm install generates bnl.lock next to bnl.json. It pins every direct and transitive dependency to a concrete version plus a SHA-256 hash of the package tarball:
{
"packages": {
"logger": {
"version": "0.4.7",
"integrity": "sha256-abcd1234…",
"deps": { "core-utils": "^1.1.0" }
}
}
}Commit both bnl.json and bnl.lock. The lockfile is what makes someone else's bpm install reproduce yours; the manifest alone resolves to latest matching, which drifts over time.
bpm login stores a bearer token in ~/.bnl/auth.json (mode 0600), keyed by registry URL:
{
"https://bpm.bnlang.dev": {
"token": "bpm_…",
"email": "you@example.com"
}
}For CI / publish automation, mint a dedicated token instead of using your password-derived session token:
bpm token create --label github-actions
# bpm_eyJhbGciOi… ← copy this, store as a CI secret
# In CI:
export BPM_TOKEN=bpm_eyJhbGciOi…
bpm publishbpm token list and bpm token revoke <id> round out the lifecycle.
| Path | Purpose |
|---|---|
./bnl.json |
Project manifest. |
./bnl.lock |
Generated lockfile. Commit it. |
./deps/<name>/ |
Project-local installed packages. |
~/.bnl/deps/<name>/ |
Global installs (from bpm install -g). |
~/.bnl/auth.json |
Registry bearer tokens. chmod 600. |
./.bpmignore |
gitignore-syntax exclude list at publish time. |
bpm never writes outside these paths.
BPM_REGISTRY— default registry URL (overridden per command by--registry).BPM_TOKEN— bearer token used for the current invocation. Takes precedence over~/.bnl/auth.json. Useful for CI.
bpm/
├── cmd/ # cobra command handlers (one file per top-level subcommand)
├── internal/
│ ├── archive/ # tarball creation + extraction
│ ├── auth/ # ~/.bnl/auth.json read/write
│ ├── lockfile/ # bnl.lock read/write
│ ├── manifest/ # bnl.json read/write
│ ├── paths/ # ~/.bnl/, deps/, …
│ ├── platform/ # windows-x64 / linux-x64 / darwin-arm64 / …
│ ├── registry/ # HTTP client for the registry API
│ ├── resolver/ # semver constraint resolution
│ └── semvr/ # thin wrapper around Masterminds/semver
├── main.go
├── go.mod
└── README.md
Pull requests welcome. For larger changes, please open an issue first to discuss the approach.
- Stick to the existing Go style (
gofmt,goimports). - Each subcommand has its own file under
cmd/; new commands follow the same pattern. - New
internal/packages should have a doc.go-style comment at the top explaining the package's role.
MIT — see LICENSE.
Copyright © 2025 Bnlang | Mamun