Skip to content

Latest commit

 

History

History
1012 lines (892 loc) · 51.1 KB

File metadata and controls

1012 lines (892 loc) · 51.1 KB

Package Management

BPL includes a built-in package manager to help you organize code into reusable libraries and manage dependencies.

Package Structure

A BPL package is a directory containing a bpl.json configuration file and source files.

my-package/
  bpl.json
  index.bpl       # Entry point (optional, but recommended)
  src/
    lib.bpl

bpl.json

The configuration file defines the package metadata.

{
  "$schema": "https://raw.githubusercontent.com/pr0h0/bpl3/master/bpl-package.schema.json",
  "name": "my-package",
  "version": "0.1.0",
  "description": "A useful library",
  "main": "index.bpl",
  "dependencies": {
    "other-package": "1.0.0"
  }
}

name must use lowercase letters, digits, and hyphens only. version must be an X.Y.Z semantic version string whose numeric segments are either 0 or do not start with 0; for example, 1.2.3 is valid and 01.2.3 is rejected. main, entry, and exports must stay inside the package root, and optional metadata such as $schema, keywords, and repository is validated before packing or installing. Invalid manifests fail while loading bpl.json instead of later during path handling or archive creation. The checked-in bpl-package.schema.json mirrors these runtime rules so editors and CI schema validation catch the same invalid shapes early. bpl init and bpl new include the canonical $schema URI in generated manifests so editors can find the package manifest contract immediately. Object maps such as dependencies, devDependencies, scripts, and bin must be JSON objects when present; null is rejected instead of being treated as an absent field.

main, entry, exports, and bin path values are strict package-relative paths. They cannot be absolute and cannot contain empty, ., or .. path segments. Use normalized paths such as src/index.bpl or bin/tool.bpl; src//index.bpl, src/./index.bpl, and ../secret.bpl are rejected before packing or installing.

Dependency map keys use the same lowercase package-name rule as name, and dependency sources must be supported non-empty strings: package names, exact versions, valid version selectors, latest, *, or archive paths. The package resolver applies the same rule when reading installed package manifests during imports. Script names must be non-empty, script commands must contain at least one non-whitespace character, and bin command names must be plain command names without / or \ separators.

Creating a Package

The quickest path is the library template:

bpl new my-package --template library
cd my-package
bpl check src/index.bpl

The template uses the project name as the package manifest name, so bpl new accepts package-safe names only: lowercase letters, numbers, and hyphens. bpl init [name] follows the same rule for explicit names. Without a name, it derives a package-safe default from the current directory by lowercasing it and replacing unsupported characters with hyphens. Package init JSON reports are available with bpl init [name] --json. Successful init runs emit schemaVersion: 1, check: "package-init", success: true, package, version, and manifestPath. JSON-mode validation failures stay on stdout with success: false, package, manifestPath, error, and stable errorCode values including BPL_PACKAGE_INIT_NAME_INVALID for invalid explicit names and BPL_PACKAGE_INIT_MANIFEST_EXISTS when bpl.json is already present. Reproduce the focused JSON contract with bun test tests/PackageManagerCLI.test.ts -t "init success and failures as JSON".

This creates:

my-package/
  bpl.json
  src/
    index.bpl
  examples/
    usage.bpl

src/index.bpl is the public package entry point. Keep exports there small and intentional so editor tooling, package consumers, and cache invalidation all see a stable API surface. bpl pack validates each exports entry before writing an archive: exported paths must point to regular source files inside the package root, and missing files, directories, and symlinks are rejected. Explicitly exported non-.bpl sources such as .x files are included in the archive. bpl pack does not follow symlinked source files and rejects symlinked bin entries, including broken symlinks, so package archives contain only regular files from inside the package root. Package archive install paths reject both final symlinks and symlinked parent directories before extraction, so a path such as deps-link/math-core-1.0.0.tgz cannot redirect an install through a linked directory. After extraction, install revalidates exports against the archive contents before replacing any existing package directory, so third-party archives cannot publish missing or directory-only public subpaths. When installing package binaries, BPL only creates or replaces symlink entries in bpl_modules/.bin or the global BPL bin directory; an existing regular file or directory with the same command name is rejected and left untouched.

To create a package manually:

  1. Create a directory for your package.
  2. Create a bpl.json file.
  3. Write your code.
  4. Pack it into a distributable archive:
bpl pack
bpl pack --output dist/packages

The first command creates my-package-0.1.0.tgz in the current directory. When --output points to a missing directory, BPL creates that directory before writing the archive and provenance sidecar. Package pack JSON reports are available with bpl pack [dir] --json. Successful packs emit schemaVersion: 1, check: "package-pack", success: true, package, version, packageDir, outputDir, and archivePath. JSON-mode validation failures stay on stdout with success: false, packageDir, outputDir, error, and stable PackageManager errorCode values when the failure is classified, including BPL_PACKAGE_MANIFEST_MISSING for package directories without bpl.json. Reproduce the focused JSON contract with bun test tests/PackageManagerCLI.test.ts -t "pack success and failures as JSON".

Installing Packages

To use a package in another project, install it from a package archive:

bpl install ../path/to/my-package-0.1.0.tgz
bpl install file:deps/my-package-0.1.0.tgz

This extracts the package into the bpl_modules/ directory of your project. For local installs, BPL also writes bpl.lock with the exact installed package version, source archive, and content hash. Direct bpl install archive paths and manifest file: sources both accept / and \ as path separators. Existing archives are resolved relative to the project or declaring package directory and lockfile sources are recorded with stable / separators. When a locked local archive source is no longer present, bpl install can restore from the matching archive basename in the global package cache without rewriting the lockfile source. If bpl_modules/<package-name> already exists, BPL only treats a real directory as an upgrade target. Existing regular files or symlinks at the package install path are rejected and left untouched. Direct archive installs also revalidate the selected package root immediately before writing. If bpl_modules/ or the configured global package directory is replaced with a symlink after the package manager starts, install fails instead of writing package files through the link. Missing real install roots are recreated.

Project manifests can also declare dependencies directly:

{
  "name": "my-app",
  "version": "1.0.0",
  "dependencies": {
    "math-core": "file:../packages/math-core/math-core-1.0.0.tgz",
    "math-extra": "^1.2.0"
  },
  "devDependencies": {
    "test-tools": "file:../tools/test-tools-1.0.0.tgz"
  }
}

Supported dependency sources are:

  • file:../path/to/pkg-1.0.0.tgz or ../path/to/pkg-1.0.0.tgz for local archives. Root project paths are resolved from the project directory; transitive package paths are resolved from the archive directory of the package that declares them.
  • 1.2.3 for an exact version, resolved as package-name-1.2.3.tgz from the package cache.
  • ^1.2.3, ~1.2.3, >=1.2.0 <2.0.0, or latest for cache-backed range resolution. BPL selects the highest cached package version satisfying the selector and records that exact archive in bpl.lock.
  • package-name for the newest matching cached archive.

dependencies and devDependencies must be JSON objects whose keys are lowercase package names and whose values are non-empty, non-whitespace strings. Values must match one of the supported source shapes above: a package name, exact version, valid selector, latest, *, or package archive path. Malformed selector-like strings such as 01.0.0, ^01.0.0, or >=1.0 fail while loading bpl.json instead of falling back to the newest cached package. null, arrays, invalid package-name keys, and blank source strings also fail before install or lockfile commands create bpl_modules/ or rewrite bpl.lock.

{
  "lockfileVersion": 1,
  "packages": {
    "my-package": {
      "version": "0.1.0",
      "source": "../path/to/my-package-0.1.0.tgz",
      "hash": "..."
    }
  }
}

To re-resolve dependency selectors and write a fresh lockfile without spelling the install flag, use bpl lock; bpl lock --json emits the same package-install JSON shape as bpl install --update --json.

Commit bpl.lock for applications so repeated installs resolve the same package contents. Libraries may commit it when they need reproducible examples or test fixtures. Default project installs validate bpl.json before restoring packages from a non-empty lockfile, so a stale bpl.lock cannot hide malformed manifest dependency sources. Lockfiles are schema-validated before install, verify, doctor, or repair commands use them; malformed entries fail early with an Invalid bpl.lock diagnostic. Symlinked bpl.lock paths, including broken symlinks, are rejected before verification so package checks never follow a lockfile outside the project, and before project installs decide whether there is anything to restore or install.

To restore exactly what is recorded in bpl.lock, run:

bpl install

To verify CI is using the checked-in package contents without mutating bpl_modules/, run:

bpl install --locked

--locked fails if a package is missing, if bpl_modules or bpl_modules/<package> is a symlink or non-directory, if the installed manifest declares a different name or version than the lock entry, if the recorded source archive is missing, a symlink, or reachable only through a symlinked parent directory, or if its source hash no longer matches the lockfile. Missing bpl_modules roots still report package-missing issues for locked entries; symlinked roots are rejected before any locked package manifest or file content is read through the link. It also checks installed package manifests for missing or malformed exported subpaths, transitive dependency roots, and lock entries, so deleting bpl_modules/math-core, removing math-core from bpl.lock, or leaving an exported file path missing from an installed package will be reported even when only math-extra imports it. A transitive dependency root referenced by a checked lock entry is reported as dependency drift only once; it is not also summarized as an untracked bpl_modules/ root.

To re-resolve bpl.json dependency selectors and rewrite bpl.lock, run:

bpl install --update

Use this when a cache-backed selector such as ^1.2.0 should pick up a newer matching cached archive. Without --update, bpl install restores the exact archives already recorded in bpl.lock. Package-name, version selector, and exact cached archive lookup validate the global package cache directory itself and its parent path components before probing tarballs. If the cache root or one of its parents is a symlink, lookup is rejected; if the cache root is missing, install reports the package as unavailable instead of surfacing a raw filesystem error. Direct archive installs perform the same lstat-based package-root validation before writes to local bpl_modules/ or the global package directory. Existing package-manager roots and newly created package-manager roots also reject symlinked parent directories, so post-construction symlink swaps or symlinked cache parents cannot redirect package installation or global cache writes outside the selected package root.

To repair the lockfile from packages already installed in bpl_modules/, run:

bpl install --repair-lock

This updates recorded versions and hashes for installed packages and removes lock entries for packages that are no longer installed. --repair-lock refuses duplicate installed package names and installed packages with invalid exports or bin entries before writing bpl.lock; export failures and invalid installed package bin targets use the invalid-manifest issue kind, and duplicate failures use the duplicate-installed-package issue kind so CI can point users at ambiguous bpl_modules/ directories instead of accepting a collapsed lockfile. Those duplicate repair-lock issues include a paths array with every conflicting installed directory. Add --json to emit a package-install report for automation; JSON-mode validation failures stay parseable on stdout with success: false and an error field. Successful bpl install --locked --json reports include action: "verified" and packagesChecked, so automation can distinguish a lockfile verification run from a normal install. packagesChecked counts locked package entries plus untracked bpl_modules/ roots inspected for drift, so an empty lockfile with one installed untracked package reports 1. Lockfile verification validates installed package bin entries before trusting a lock entry, so missing, directory, or symlinked binary targets fail the same way as invalid installed package exports. It also rejects duplicate installed package names, including extra bpl_modules/ directories whose manifest name matches an already locked package and multiple untracked directories declaring the same package identity. Failed locked verification reports use BPL_PACKAGE_LOCK_VERIFY_FAILED with action: "verification-failed", packagesChecked, issuesFound, issueKinds, and compact issues entries containing the package name and issue kind, so CI can detect lock drift without scraping the formatted diagnostic. When verifier issues carry installed package paths or expected versions, compact issue entries also include path, source, expectedVersion, actualVersion, expectedName, actualName, expectedHash, actualHash, dependencyOf, and requestedSource when those fields apply. Reproduce the locked success and failure JSON contract with bun test tests/PackageManagerCLI.test.ts -t "should enforce --locked package verification". Reproduce lockfile bin validation with bun test tests/PackageManager.test.ts tests/PackageManagerCLI.test.ts -t "installed package bin files during locked verification|installed package bin files when repairing lockfiles|installed package bin files during lockfile repair". The API regression coverage explicitly exercises installed package bin targets that are directories or symlinks during locked verification and lockfile repair. When the underlying package error has a stable compiler code, the report also includes errorCode, such as BPL_LOCKFILE_UNSUPPORTED_VERSION for malformed lockfiles, BPL_PACKAGE_NOT_FOUND for package lookup misses, BPL_PACKAGE_INSTALL_PROJECT_OPTION_WITH_PACKAGE, BPL_PACKAGE_INSTALL_LOCKED_UPDATE_CONFLICT, BPL_PACKAGE_INSTALL_GLOBAL_PROJECT_CONFLICT, BPL_PACKAGE_INSTALL_LOCKED_REPAIR_CONFLICT, and BPL_PACKAGE_INSTALL_UPDATE_REPAIR_CONFLICT for project-mode option conflicts, BPL_PACKAGE_LOCK_VERIFY_FAILED for locked package verification drift, and BPL_PACKAGE_ARCHIVE_SYMLINK, BPL_PACKAGE_ARCHIVE_PARENT_SYMLINK, and BPL_PACKAGE_ARCHIVE_NOT_FILE for direct archive paths that are symlinks, pass through symlinked parents, or are not files. Reproduce the focused option/archive JSON contracts with bun test tests/PackageJsonFailureContracts.test.ts -t "package install option conflict|direct archive path". PackageManager manifest-loading failures also carry stable BPL_PACKAGE_MANIFEST_* codes: BPL_PACKAGE_MANIFEST_MISSING, BPL_PACKAGE_MANIFEST_SYMLINK, BPL_PACKAGE_MANIFEST_NOT_FILE, BPL_PACKAGE_MANIFEST_PARSE_ERROR, BPL_PACKAGE_MANIFEST_NOT_OBJECT, BPL_PACKAGE_MANIFEST_NAME_MISSING, BPL_PACKAGE_MANIFEST_NAME_INVALID, BPL_PACKAGE_MANIFEST_VERSION_MISSING, BPL_PACKAGE_MANIFEST_VERSION_INVALID, BPL_PACKAGE_MANIFEST_METADATA_INVALID, BPL_PACKAGE_MANIFEST_MAIN_INVALID, BPL_PACKAGE_MANIFEST_ENTRY_INVALID, BPL_PACKAGE_MANIFEST_EXPORTS_INVALID, BPL_PACKAGE_MANIFEST_KEYWORDS_INVALID, BPL_PACKAGE_MANIFEST_REPOSITORY_INVALID, BPL_PACKAGE_MANIFEST_DEPENDENCIES_INVALID, BPL_PACKAGE_MANIFEST_SCRIPTS_INVALID, and BPL_PACKAGE_MANIFEST_BIN_INVALID. Reproduce the focused JSON contract with bun test tests/PackageJsonFailureContracts.test.ts -t "package manifest error codes".

bpl doctor packages --json reports malformed or unsafe lockfiles as kind: "invalid-lockfile" issues with stable BPL_LOCKFILE_* codes, including BPL_LOCKFILE_INVALID_JSON, BPL_LOCKFILE_UNSUPPORTED_VERSION, BPL_LOCKFILE_INVALID_PACKAGES, BPL_LOCKFILE_INVALID_ENTRY_NAME, BPL_LOCKFILE_INVALID_ENTRY_HASH, BPL_LOCKFILE_NOT_FILE, and BPL_LOCKFILE_SYMLINK.

To inspect why packages are installed and which dependencies are missing, use:

bpl list --json
bpl list --tree
bpl list --tree --json
bpl doctor packages
bpl doctor packages --json

Use the JSON forms for CI and tooling. bpl list --json uses a stable top-level contract with schemaVersion: 1, check: "package-list", success, and installed package names, versions, paths, content hashes, and node-style problems. Package list entries revalidate declared exports and bin entries; missing, directory, or symlinked public subpaths and package binary targets are appended to the affected package's problems array as invalid exports or invalid bin entries and shown below that package in text output. Package listing revalidates the local or global package directory with lstat before scanning, including parent path components, so a symlinked bpl_modules, global package cache directory, or parent directory is rejected instead of followed. In JSON mode, unsafe package-root failures return success: false, the requested scope, an empty packages: [] payload, and error. Duplicate installed package names return errorCode: "BPL_PACKAGE_DUPLICATE_INSTALLED" with issuesFound, issueKinds, and compact issues entries whose kind is "duplicate-installed-package". Each duplicate issue keeps the compact path string for compatibility and also includes a paths array with every conflicting installed directory in deterministic order. bpl list --tree --json uses check: "package-list-tree" with the same schemaVersion and success fields, plus the dependency tree data used by the human tree output. Tree generation validates an existing local bpl.lock before choosing lockfile roots; symlinked, broken-symlink, malformed, or non-file lockfile paths are rejected instead of being treated as absent. Tree roots and nodes also classify bpl_modules and bpl_modules/<package> paths with lstat, so symlinked or non-directory package roots are reported as problems instead of being followed. Installed package nodes also revalidate declared exports and bin entries; missing, directory, or symlinked public subpaths and package binary targets are appended to node problems in both text and JSON bpl list --tree output while child dependency traversal is preserved. Duplicate installed package names return the same errorCode, issuesFound, issueKinds, and compact issues contract before tree roots are selected. List and tree duplicate failures also include issuesFound, issueKinds, and compact issues entries with kind: "duplicate-installed-package" so automation can point at the ambiguous installed directories. The duplicate issue payload includes both the compact path string and a paths array with every conflicting installed directory. Tree JSON validation failures return the requested scope, tree: [], and error.

Package list JSON failure codes are exported through PACKAGE_LIST_JSON_ERROR_CODES and the public CLI_JSON_ERROR_CODE_LISTS package-list entry for tooling that wants the complete stable inventory:

  • BPL_PACKAGE_SEARCH_DIR_SYMLINK
  • BPL_PACKAGE_SEARCH_DIR_NOT_DIRECTORY
  • BPL_PACKAGE_SEARCH_DIR_PARENT_NOT_DIRECTORY
  • BPL_PACKAGE_SEARCH_DIR_PARENT_SYMLINK
  • BPL_PACKAGE_DUPLICATE_INSTALLED

Reproduce the exported inventory guard with bun test tests/PackageJsonFailureContracts.test.ts -t "error-code lists". bpl doctor packages --json uses a stable top-level contract with schemaVersion: 1, check: "packages", success, the legacy ok boolean, lockfile details, cache verification, dependency tree data, and structured issues with severity, kind, message, path, and hint fields. Package doctor validates the current project package's declared exports and bin entries before pack/install, reporting missing, directory, or symlinked project export paths and package binary targets as kind: "invalid-project-package" with packageName, version, the project manifest path, and a fix-exported-files or Fix package bin files hint. It also validates each installed package's declared exports and bin entries even when no lockfile verification covers that package. Missing, directory, or symlinked installed export paths and package binary targets report kind: "invalid-installed-package" with packageName, version, the installed package path, and a reinstall hint. Duplicate installed package doctor issues preserve the compact joined path string and also include a paths array with every conflicting installed directory in deterministic order. Package-cache warning issues preserve packageName, version, the cache archive path, and provenancePath when a malformed, missing, or unsafe sidecar is involved, so automation can group warnings by package identity and point directly at the stale .bplmeta.json file. For doctor lock verification drift, each issue also carries code: "BPL_PACKAGE_LOCK_VERIFY_FAILED" and the verifier metadata needed by automation: packageName, source, expectedVersion, actualVersion, expectedName, actualName, expectedHash, actualHash, dependencyOf, and requestedSource when those fields apply to the drift kind. Duplicate lock-verification issues also include the verifier paths array with every conflicting installed directory. When lock verification and installed-package scanning find the same duplicate path set, doctor keeps the lock-verification issue and suppresses the redundant installed-package duplicate. Doctor issues include lockVerificationKind, which preserves the underlying verifier issue kind even when the doctor layer uses a clearer package-health label. For example, a lock entry whose package directory is missing is reported as stale-lock-entry with lockVerificationKind: "missing-package". This lets CI summarize stale lock entries, hash mismatches, version mismatches, unreachable sources, untracked installed packages (lockVerificationKind: "untracked-package"), invalid untracked package metadata, unsafe untracked package roots, and transitive dependency drift without parsing the human-readable message. Reproduce this contract with bun test tests/PackageManagerCLI.test.ts -t "lock verification drift". When the local package directory, global package cache directory, or one of its parent directories is unsafe to read, doctor reports an unsafe-package-directory issue and leaves the affected package lists empty instead of following the directory.

Example output:

Dependency tree (local):

  math-extra@1.0.0 [locked] <- 1.0.0
    math-core@1.0.0 [locked] <- 1.0.0

Missing packages are shown inline:

  math-extra@1.0.0 [locked] <- 1.0.0
    math-core@1.0.0 (missing) [locked] <- 1.0.0
      ! missing from bpl_modules

Using Packages

Once installed, you can import the package by its name in your BPL code.

import [MyStruct], myFunction from "my-package";

frame main() {
    myFunction();
}

The compiler resolves "my-package" to bpl_modules/my-package/index.bpl (or the file specified in main). Resolution starts from the importing file's directory and walks upward, so src/main.bpl can import packages installed at the project root even when bpl check /path/to/project/src/main.bpl is run from another working directory.

Package names that collide with standard-library module basenames are not reachable through bare imports. Bare imports that match standard-library module basenames resolve to the standard library before package lookup; for example, a package named math is shadowed by the built-in math module when imported as "math". Use a non-stdlib package name such as math-extra for packages that should be imported by name.

During package resolution, the requested package name must use the same lowercase package-name format as package manifests. The package directory name and manifest must agree: bpl_modules/my-package/bpl.json must declare "name": "my-package" and a valid X.Y.Z "version". Global versioned directories such as ~/.bpl/packages/my-package-1.2.3/ must also declare the same "version": "1.2.3" in bpl.json. Global versioned package directories must match their manifest version. Mismatches or malformed identity fields are treated as package metadata errors instead of silently importing a different package. Package import paths cannot contain empty, . or .. segments; use relative imports for filesystem traversal inside your own project.

Global versioned package directories use the <package>-X.Y.Z naming form. When resolving a global package import, BPL selects the highest semantic version whose directory name matches the requested package, then validates that root before reading bpl.json. Version segments are compared exactly, so large segments such as 9007199254740993 do not lose precision during package selection. The selected global versioned package root is still validated with lstat before its manifest is read, so a symlink, regular file, or other non-directory named like a higher version blocks fallback to lower versions. For example, ~/.bpl/packages/math-9.0.0 as a symlink or file blocks fallback to ~/.bpl/packages/math-1.0.0. Versioned global directory names with zero-padded segments such as math-01.0.0 are not considered semantic-version candidates; use math-1.0.0 and a matching manifest version instead. Case-mismatched global versioned package directories such as Math-9.0.0 are rejected before fallback to lower versions such as math-1.0.0; they report BPL_PACKAGE_ROOT_CASE_MISMATCH with both the requested lowercase path and the actual filesystem path. Focus this import stability surface with:

bun test tests/PackageResolver.test.ts -t "casing|case-mismatched global versioned"
bun test tests/ModuleResolver.test.ts -t "case-mismatched global versioned|filesystem casing"
bun test tests/CiTriage.test.ts -t "package import casing"

Packages can expose source files below their root through subpath imports:

import increment from "math-extra/features/increment";

For package subpaths, BPL searches for a file or directory entry under the package root. The example above resolves to either bpl_modules/math-extra/features/increment.bpl or bpl_modules/math-extra/features/increment/index.bpl. Extensionless package directory imports such as math-extra/features/increment may resolve to features/increment/index.bpl, which makes them useful for small submodule facades. Explicit package source-file imports such as math-extra/features/increment.bpl require a file at that exact path; explicit package source-file imports ending in .bpl or .x do not fall back to directory indexes. If features/increment.bpl is a directory, import math-extra/features/increment to allow index.bpl/index.x fallback, or create a real features/increment.bpl source file. The resolver does not follow symlinked package search directories, package roots, manifests, source parent directories, entry files, or subpath entries, so package imports cannot escape the installed package root through filesystem links. Symlinked package search directories such as bpl_modules/, workspace packages/, and the global package directory are rejected before child package candidates are probed. Nested package source paths such as src/index.bpl and features/add.bpl reject symlinked parent directories before the child file is read. Package search directories, package roots, manifest files, entrypoints, and subpath source candidates must also match filesystem casing exactly. Case-only mismatches are rejected with diagnostics that include the requested path and the actual path, so case-insensitive development machines cannot silently accept an import that fails on Linux. Symlinked and non-directory package search directories stop package resolution instead of falling through to lower-priority package roots: a symlinked local bpl_modules/ stops resolution before workspace packages/, a non-directory local bpl_modules/ stops resolution before workspace and global packages, and a symlinked or non-directory workspace packages/ stops resolution before global packages. A symlinked or non-directory global package directory is rejected before global package candidates are listed. Existing malformed package roots are terminal metadata failures, so a blocked local package root cannot fall through to a same-name workspace or global package. This includes symlinked package roots, non-directory package paths, and package directories missing bpl.json. Symlinked package entrypoint and subpath candidates are also terminal resolution failures; the resolver will not fall through from a blocked .bpl package candidate to a lower-priority .x file, including package directory index.bpl candidates before index.x. After a package root has been accepted, package source failures are terminal too: unsafe manifest entrypoint values such as ../outside.bpl, symlinked entrypoint files, and subpath source parents such as features/ are reported at the import site instead of falling back to alternate candidates or following the link. Unsafe main or legacy entry values also fail before any package entrypoint or subpath import from that package is resolved, including exported subpaths that would otherwise point at safe files. The resolver validates both fields independently, so an unsafe legacy entry still invalidates the package when a safe main is present. The package manager validates both main and the legacy entry field as package-relative paths before install or pack operations continue. Package import validation reports main and legacy entry failures before later manifest fields such as exports, keywords, repository, dependency maps, scripts, or bin, matching package-manager manifest validation order. The package import resolver also rejects malformed string metadata such as a non-string $schema, description, author, or license field before using the package entrypoint. It also validates keywords as an array of strings and repository as an object with type: "git" and a string url. Dependency, script, and bin maps are checked for the same object shape, key, and non-empty string rules during import resolution before the package entrypoint is used. exports entries are also validated as safe package-relative paths. During bpl pack, each exported path must exist as a regular source file from inside the package root; directories, missing files, and symlinks fail before an archive is created. Archive install performs the same extracted-package validation before writing the install target or lockfile. When exports is present, package subpath imports are restricted to the listed package-relative source paths; packages without exports keep permissive subpath resolution. Extensionless imports still use the normal resolver fallbacks, so exporting features/add.bpl allows import [...] from "pkg/features/add", and exporting features/math/index.bpl allows import [...] from "pkg/features/math". When an exports allowlist is present, those fallbacks only probe exported source paths. For example, if exports contains legacy.x, an extensionless pkg/legacy import resolves legacy.x and does not expose a hidden legacy.bpl; similarly, an exported features/math/index.x is not shadowed by an unexported features/math/index.bpl.

In bpl check --json and bpl build --json, package import diagnostics use the normal diagnostic object shape and include a stable code when the resolver can classify the failure. Import spelling and lookup failures use BPL_PACKAGE_IMPORT_INVALID and BPL_PACKAGE_NOT_FOUND. Source-safety failures use BPL_PACKAGE_ENTRYPOINT_UNSAFE, BPL_PACKAGE_ENTRYPOINT_SYMLINK, BPL_PACKAGE_ENTRYPOINT_CASE_MISMATCH, BPL_PACKAGE_ENTRYPOINT_NOT_FOUND, BPL_PACKAGE_SUBPATH_SYMLINK, BPL_PACKAGE_SUBPATH_CASE_MISMATCH, BPL_PACKAGE_SUBPATH_NOT_EXPORTED, and BPL_PACKAGE_SUBPATH_NOT_FOUND. Package search and metadata failures use BPL_PACKAGE_SEARCH_DIR_SYMLINK, BPL_PACKAGE_SEARCH_DIR_NOT_DIRECTORY, BPL_PACKAGE_SEARCH_DIR_PARENT_NOT_DIRECTORY, BPL_PACKAGE_SEARCH_DIR_PARENT_SYMLINK, BPL_PACKAGE_SEARCH_DIR_CASE_MISMATCH, BPL_PACKAGE_ROOT_SYMLINK, BPL_PACKAGE_ROOT_NOT_DIRECTORY, BPL_PACKAGE_ROOT_CASE_MISMATCH, BPL_PACKAGE_MANIFEST_MISSING, BPL_PACKAGE_MANIFEST_SYMLINK, BPL_PACKAGE_MANIFEST_CASE_MISMATCH, BPL_PACKAGE_MANIFEST_NOT_FILE, BPL_PACKAGE_MANIFEST_PARSE_ERROR, BPL_PACKAGE_MANIFEST_NOT_OBJECT, and BPL_PACKAGE_MANIFEST_INVALID.

Regular module import candidates use the same filesystem diagnostics: broken symlink candidates are reported as symlinks before extension fallback can import a lower-priority .x file, while valid symlink imports normalize to their real module path. Non-package import diagnostics use stable JSON code values too: BPL_MODULE_NOT_FOUND, BPL_MODULE_FILE_NOT_FOUND, BPL_MODULE_PATH_NOT_FILE, BPL_MODULE_PATH_SYMLINK, BPL_MODULE_PATH_CASE_MISMATCH, and BPL_IMPORT_STD_PATH_UNSAFE.

Workspace packages are supported without installing an archive. If an ancestor directory contains packages/<package-name>/bpl.json, imports can resolve through that workspace before falling back to global packages. The runnable example in examples/package_transitive_dependency/app/main.bpl demonstrates a workspace package, a transitive dependency, and a subpath import. The package/import docs examples are smoke-tested with bun test tests/CLIJsonParseability.test.ts -t "package/import docs examples": the workspace success path checks examples/package_transitive_dependency/app/main.bpl, including the explicit package source-file import math-extra/features/direct.bpl that resolves to examples/package_transitive_dependency/packages/math-extra/features/direct.bpl and the extensionless directory-index import math-extra/features/increment that resolves to examples/package_transitive_dependency/packages/math-extra/features/increment/index.bpl. The invalid pkg-math/../secret package import checks the JSON diagnostic code BPL_PACKAGE_IMPORT_INVALID.

Dependency Resolution

When you run bpl install, the package manager:

  1. Validates bpl.json, including dependency source shapes, before any package restore or install.
  2. Restores packages from bpl.lock when lock entries exist and --update is not used.
  3. Otherwise reads dependencies and devDependencies from bpl.json.
  4. Extracts packages to bpl_modules/<package-name>.
  5. Installs package dependencies recursively.
  6. Records each exact local install in bpl.lock.
  7. Resolves imports through the nearest bpl_modules/, workspace packages/, and then the global package directory.

Dependency cycles are rejected with the full package chain:

Cyclic package dependency detected: app-a -> app-b -> app-a

Dependency names are also checked during project installs. If bpl.json asks for math-core but the archive contains a manifest named other-core, install fails instead of writing a lockfile that can never satisfy imports.

For transitive file: dependencies, the path belongs to the package that declares it. If packages/math-extra/math-extra-1.0.0.tgz declares "math-core": "file:../math-core/math-core-1.0.0.tgz", BPL resolves that path relative to packages/math-extra/, not relative to the app installing math-extra. The resolved archive path must not traverse symlinked parent directories; BPL rejects it before extraction rather than installing package contents from outside the declared archive path.

Package Cache

The package cache stores .tgz archives used by exact-version dependencies, global installs, and restores from bpl.lock. bpl pack writes a provenance sidecar next to each archive:

math-core-1.0.0.tgz
math-core-1.0.0.tgz.bplmeta.json

The sidecar records the archive SHA-256, the extracted package content hash, the package name and version, and the manifest used to produce the archive. Global installs copy the archive into the cache and regenerate the sidecar from the extracted package so cached installs can be audited later.

Archive creation, inspection, and extraction use BPL_TAR, then TAR, then tar. Set one of these variables when the system tar is not on PATH or when CI should use a specific archive tool. Archive tool invocations are bounded by BPL_PACKAGE_TOOL_TIMEOUT_MS, defaulting to 300000 milliseconds.

bpl pack also verifies generated package LLVM IR with BPL_CC, CC, or clang when available. That verifier is bounded by BPL_PACKAGE_IR_VERIFY_TIMEOUT_MS and defaults to 30000 milliseconds so a hung compiler driver does not block package creation forever. Timeout environment variables must be positive integers; invalid values are ignored with a warning that says expected a positive integer. For package commands, BPL_PACKAGE_TOOL_TIMEOUT_MS invalid values fall back to 300000 milliseconds, and BPL_PACKAGE_IR_VERIFY_TIMEOUT_MS invalid values fall back to 30000 milliseconds.

bpl package-cache list
bpl package-cache list math-core --json
bpl package-cache verify
bpl package-cache verify math-core --json
bpl package-cache repair math-core --dry-run
bpl package-cache repair math-core --package-version 1.0.0
bpl package-cache clean math-core --package-version 1.0.0 --dry-run
bpl package-cache clean math-core --package-version 1.0.0 --json

package-cache list --json reports cached archives with schemaVersion: 1, check: "package-cache-list", success, and the existing cache entry payload under entries. Package-name and semver cache resolution use the same lstat-based archive filtering as list and verify, so symlinked cache archives cannot shadow real cached versions. Package-cache listing, verification, repair, and clean also validate the global cache root and its parent path components before touching archives or provenance sidecars, so a symlinked cache parent is rejected instead of followed. In JSON mode, unsafe cache-root failures from package-cache list return success: false, entries: [], and error; package-cache verify returns success: false, ok: false, entriesChecked: 0, issues: [], and error.

package-cache verify checks every matching cached archive. It verifies the sidecar schema, the archive hash, the archive file name, the manifest identity, the extracted package content hash, and the manifest exports entries against the extracted package files. package-cache verify also validates manifest bin entries from extracted cached archives. Missing or directory-only exported paths and every invalid cached bin target are reported as invalid-archive issues so broken public cache surfaces and broken package binaries are not treated as healthy. Missing sidecars are reported as missing-provenance so older caches remain visible instead of being silently trusted. Malformed sidecars and symlinked provenance paths are reported as invalid-provenance issues with the cache archive path and sidecar provenancePath, so automation can flag unsafe cache metadata without following it. The --json report uses schemaVersion: 1, check: "package-cache-verify", success, the legacy ok boolean, entriesChecked, and structured provenance issues. bpl doctor packages includes the same cache verification result in its JSON report and prints provenance warnings for stale or damaged cache entries. In JSON output, package-cache doctor issues use package-cache-<verification-kind> issue kinds such as package-cache-missing-provenance, preserving packageName, version, the original cache entry path, sidecar provenancePath, and the repair hint for CI annotations.

package-cache repair regenerates missing or malformed provenance sidecars for valid cached archives. Its JSON result uses schemaVersion: 1, check: "package-cache-repair", success, dryRun, repaired, unchanged, and issues. Entries with verified provenance are still extracted and validated before they are reported as unchanged, so current manifest, export, bin, and package hash rules are enforced even on older cache sidecars. It refuses to repair invalid extracted dependency sources, invalid extracted exports, archive hash mismatches, manifest mismatches, package hash mismatches, or invalid extracted bin target files because those states may indicate a stale or damaged archive; clean and repack those entries instead. package-cache repair refuses to regenerate provenance for an archive with a missing, directory, or symlinked package binary. --package-version filters expect one exact cached version in X.Y.Z form with no zero-padded segments; dependency ranges such as ^1.2.3 belong in bpl.json, not cache maintenance commands. Cached package versions and dependency range comparisons use exact integer segment ordering rather than JavaScript number precision. In JSON mode, clean and repair validation failures keep stdout parseable: clean reports removed: [], repair reports repaired: [], unchanged: [], and issues: [], and both include the requested dryRun value plus error. Package-cache validation failures that come from an invalid --package-version value also include errorCode: "BPL_PACKAGE_CACHE_VERSION_INVALID". Reproduce the focused contract with bun test tests/PackageJsonFailureContracts.test.ts -t "package-cache version filter". Reproduce package-cache bin validation with bun test tests/PackageManager.test.ts tests/PackageManagerCLI.test.ts -t "cached package.*bin files|symlinked cached package bin|package cache repair.*bin files|cached package bin files in verify". Directory bin targets are reported by package-cache bin validation. Symlinked binary archive members are rejected by the archive safety layer during package-cache verify and before provenance repair can trust them. The package-cache package filters must use the same lowercase package-name format as package manifests. Invalid package filters in package-cache list, verify, clean, and repair fail with BPL_PACKAGE_CACHE_NAME_INVALID; reproduce that contract with bun test tests/PackageJsonFailureContracts.test.ts -t "package-cache package filter".

package-cache clean removes cached archives only. It does not remove installed packages from bpl_modules/; use bpl uninstall <package> for that. When a cached archive has a provenance sidecar, package-cache clean removes both files together, including malformed sidecar directories and symlink sidecars whose targets may already be missing. Use --json to return schemaVersion: 1, check: "package-cache-clean", success, the removed archive list, and dry-run state in machine-readable form. Clean refuses to run through symlinked cache parents, so it cannot delete archives or sidecars from an unintended target directory.

Installing by an exact cached archive name such as my-package-1.0.0.tgz uses the same archive path validation as a direct file path. Symlinked or broken symlink cache entries are rejected as package archive symlinks rather than being treated as missing packages.

file: and relative archive dependencies use the same path classification. Broken symlink dependency archives are rejected as package archive symlinks instead of falling back to a package-name lookup.

bpl uninstall <package> only removes real installed package directories. If bpl_modules/<package> is a symlink, uninstall rejects it and leaves both the symlink and its target untouched. Package manifests are validated with the same lstat rules during uninstall, so symlinked or broken-symlink bpl.json paths are rejected as manifest symlinks instead of being treated as missing manifests. Package uninstall JSON reports are available with bpl uninstall <package> --json and the remove alias. Successful uninstalls emit schemaVersion: 1, check: "package-uninstall", success: true, the removed package, its version, and the requested global scope. JSON-mode failures stay on stdout with success: false, package, global, error, and stable errorCode values including BPL_PACKAGE_UNINSTALL_NAME_INVALID for invalid package names and BPL_PACKAGE_UNINSTALL_NOT_INSTALLED for missing local or global packages. Reproduce the focused JSON contract with bun test tests/PackageManagerCLI.test.ts -t "uninstall success and failures as JSON". Local uninstall also validates an existing bpl.lock before unlinking binaries or removing package files; symlinked, broken-symlink, malformed, or non-file lockfile paths are rejected instead of leaving package files and lock entries inconsistent. Local and global uninstall revalidate the selected package root before probing or removing bpl_modules/<package> or global package directories. If bpl_modules/ or the configured global package directory is swapped to a symlink after the package manager starts, uninstall fails before reading or removing package files through the link. When uninstalling packages with bin entries, BPL also revalidates bpl_modules/.bin or the global BPL bin directory before unlinking command symlinks. Symlinked or non-directory bin roots are rejected; a missing bin directory is tolerated because there are no command links to remove.

Package Scripts

Packages can define shell scripts in bpl.json:

{
  "scripts": {
    "check": "bpl check src/main.bpl",
    "build": "bpl build src/main.bpl -o app"
  }
}

List them with bpl run-script --list or as JSON with bpl run-script --list --json. Run one with bpl run-script <name> or bpl rs <name>. Script commands must be non-empty strings. Extra arguments are forwarded to the script as quoted shell arguments, so values containing spaces or shell metacharacters remain single arguments; pass option-looking values after --, for example bpl rs build -- --release. When --json is used, bpl run-script --list --json returns schemaVersion: 1, check: "run-script-list", success: true, and the scripts array. Manifest and script validation failures are emitted as machine-readable JSON with the same schemaVersion and check, plus success: false and error. Named-script validation failures from bpl run-script <name> --json use check: "run-script" with the same error shape, so CI and editor integrations do not need to parse logger text.

Run-script manifest loading uses the same filesystem safety posture as the package manager: bpl.json must be a real file, not a symlink, and its parent path components must not be symlinks. This check happens before parsing, listing, or executing scripts, so launchers that preserve a symlink-spelled working directory should run bpl run-script from the real project path.

bpl run-script --list
bpl run-script --list --json
bpl run-script check
bpl rs build -- --release

Best Practices

  • Entry Point: Use index.bpl to re-export the public API of your package.

    # index.bpl
    import [MyStruct] from "./src/structs.bpl";
    import myFunction from "./src/funcs.bpl";
    
    export [MyStruct];
    export myFunction;
  • Names: Use unique, lowercase names for packages (kebab-case recommended).

Release Checks

Before publishing or cutting a release, run:

bun run release:check

This type-checks the TypeScript code, verifies the generated bpl-v3/cli JSON registry shim, validates release metadata and workflow expectations, runs standalone ./bpl and packed npm tarball smoke tests, and runs the VS Code extension tests. It is intentionally smaller than the full compiler correctness matrix so it stays useful as a local pre-release gate. The packed npm smoke includes package/import diagnostic JSON coverage: it runs the installed CLI against a malformed package import and asserts the stable BPL_PACKAGE_MANIFEST_MISSING diagnostic code. Reproduce that focused contract with bun test tests/ReleaseMetadata.test.ts -t "packed package import diagnostic codes", or run the full packed smoke with bun run release:smoke. The packed smoke uses an isolated temporary npm cache. Its wasm artifact check is skipped when the compiler has no wasm backend unless BPL_REQUIRE_WASM_LD=1 makes wasm toolchain coverage mandatory.

To create a checksum manifest for the release artifacts, run:

bun run build
bun run release:manifest

This writes dist/release-manifest.json and records SHA-256 hashes for the standalone compiler binary, native and wasm runtime shims, lib/runtime_support.o, and the packed npm tarball. The npm artifact entry also includes the package integrity and shasum values emitted by npm pack --json, so downstream release jobs can compare the compiler package against the published archive. Release manifest usage errors, including Unknown release manifest option: --unknown, Missing value for --out, and Missing value for --repo-root, exit with status 2 before the helper writes a manifest or runs npm pack. bun tools/release_manifest.ts --help prints the release manifest helper usage without writing artifacts. Focus that contract with:

bun test tests/ReleaseMetadata.test.ts -t "release manifest CLI reports usage errors"

The helper also accepts inline value forms for scripted release jobs:

bun tools/release_manifest.ts --out=dist/release-manifest.json --repo-root=.

Malformed inline values such as --out=, --repo-root=, and --pack-npm=true fail with status 2 before manifest writes or npm pack. Focus both separated and inline value handling with:

bun test tests/ReleaseMetadata.test.ts -t "release manifest CLI reports usage errors|release manifest CLI accepts inline option values"

The generated CLI JSON registry helper is available as bun run release:cli-registry for the default sync check. Use bun tools/cli_json_registry_shim.ts --help for direct helper usage and bun tools/cli_json_registry_shim.ts --write to refresh the checked-in shim files. release:cli-registry rejects --check=true and --write=true with status 2 before checking or writing generated files. Focus that contract with:

bun test tests/JsonErrorCodeLists.test.ts

For source packages, keep these checks together:

bpl format --check src/*.bpl
bpl lint --json src/*.bpl
bpl check --json src/index.bpl
bpl pack

format --check verifies that generated archives contain formatted sources, while lint --json and check --json provide stable diagnostic ranges for CI annotations.