stdlib@0.3.2, any engineer building it will
use exactly that version, producing identical results regardless of when or where
the build runs.
Principles
The package system is built on three principles: compatibility, repeatability, and cooperation. These principles reinforce each other and guide every design decision.Compatibility
The meaning of a package version should not change over time.
- Patch versions (0.3.1 → 0.3.2): Metadata changes only. No changes to connectivity, layout, or electrical behavior. Examples: updating documentation, fixing a typo in a property value, adding manufacturer aliases.
- Minor versions (0.3.x → 0.4.0): New functionality that doesn’t break existing designs. Examples: adding optional pins, new configuration options, additional footprint variants.
- Major versions (0.x → 1.0): Breaking changes that may require design updates. Examples: changing pin definitions, restructuring interfaces, removing deprecated features.
Repeatability
The result of a build should not change over time.
- Minimal Version Selection (MVS): Select the minimum version that satisfies all constraints. The selected version is determined entirely by the dependency graph, not by what versions happen to exist on remote servers.
-
Lockfiles (
pcb.sum): Record the exact versions and cryptographic hashes of all dependencies. Subsequent builds verify against the lockfile rather than resolving again. - Content hashing: Every package version is identified by a BLAKE3 hash of its contents. If the contents change (even with the same version tag), the hash changes, and the build fails verification.
Cooperation
To maintain the package ecosystem, we must all work together.
- Follow semantic versioning conventions
- Test changes before releasing new versions
- Avoid breaking changes within a semver family
- Communicate clearly when breaking changes are necessary
- Gradual migration: Multiple major versions can coexist in a single build, allowing dependent packages to upgrade at their own pace
- Publishing workflow: The
pcb publishcommand validates packages before release, catching common mistakes early
Coexisting Versions
Sometimes breaking changes are necessary. A component library might need to restructure its pin definitions, or the stdlib might change how units are represented. When this happens, packages that depend on the old API cannot immediately upgrade—they need time to adapt. The package system supports multiple major versions in a single build. If your workspace contains:stdlib@0.3.x and stdlib@1.0.x, each used by the boards that require them.
Types and values from different major versions are distinct and cannot be mixed.
This approach has two key benefits:
No flag day upgrades. Teams can migrate board-by-board over weeks or months,
validating each migration before moving to the next. The old version remains
available as long as any board needs it.
Diamond dependencies work. If Board A uses Library X (stdlib 0.3) and Library Y
(stdlib 1.0), both libraries function correctly. Library X sees stdlib 0.3 types,
Library Y sees stdlib 1.0 types, and the board sees whatever version it declares.
The limitation is that types from different major versions may be incompatible. A
Net from stdlib 0.3 cannot be passed to a function expecting a stdlib 1.0 Net.
Major versions represent potential breaking changes, and the system cannot guarantee
compatibility across that boundary.
Minimal Version Selection
Our approach to dependency resolution is heavily inspired by Go modules, which pioneered Minimal Version Selection. Most package managers (npm, Cargo, pip) use SAT solvers to find the newest versions that satisfy all constraints. This approach has a fundamental problem: the result depends on what versions exist at resolution time. If a new version is published between two builds, the resolver might select it, changing your dependencies without any change to your code. SAT solvers also introduce complexity. When constraints conflict, the solver must backtrack and try alternative versions. This can be slow, and when it fails, the error messages are often inscrutable—the solver tried thousands of combinations and none worked. Minimal Version Selection (MVS) takes a different approach: instead of finding the newest versions that work, find the minimum versions that each package explicitly requires. This simple change has profound consequences.How MVS Works
Consider this dependency graph:stdlib >= 0.3.0. The regulator requires stdlib >= 0.3.2.
MVS selects stdlib@0.3.2—the minimum version that satisfies both constraints.
Even if stdlib@0.3.9 exists and is compatible, MVS chooses 0.3.2 because that’s
what the dependencies explicitly require. The existence of newer versions is
irrelevant.
Why Minimum, Not Maximum?
This seems counterintuitive. Wouldn’t you want the newest compatible version with all its bug fixes? The key insight is that the author tested with specific versions. When the regulator author published version 1.0.0, they tested it withstdlib@0.3.2.
They know it works with that version. They hope it works with 0.3.9, but they
haven’t tested it. By selecting the minimum, MVS chooses the versions that were
actually tested together.
If you want newer versions, you explicitly upgrade:
Properties of MVS
Deterministic. The selected versions depend only on the dependency graph, not on what versions exist remotely. Two engineers building the same code get the same versions, even if one builds months later when new versions exist. No backtracking. MVS never needs to “try” a version and backtrack if it fails. It computes the answer directly: for each package, find the maximum of the minimum versions required by all dependents. Understandable. You can compute the result by hand. For each package, look at every place it’s required and take the highest version. That’s it. Fast. Linear in the size of the dependency graph. No exponential search space.Resolution Algorithm
- Seed: Collect direct dependencies from all workspace members. Group by (package path, semver family). Initialize each family to the highest version explicitly required.
- Discover: Fetch manifests for selected versions. For each transitive dependency, if it requires a higher version within an existing family, upgrade. Repeat until no changes (fixed point).
- Build closure: Trace the dependency graph from workspace roots using final versions. This filters out any versions that were superseded during discovery.
Import Paths as Identity
Import paths serve as globally unique package identifiers:pcb.toml
manifest declares which version of each package to use:
- Import statements remain stable across version upgrades
- Version changes are localized to manifest files
- The same source file can work with different versions in different contexts
Lockfiles
The lockfile (pcb.sum) records exact versions and cryptographic hashes:
- Content hash: BLAKE3 hash of the package’s canonical tar archive
- Manifest hash: BLAKE3 hash of the package’s
pcb.tomlfile
pcb update --tidy to remove unused entries.
Commit the lockfile to version control. This ensures all engineers and CI
systems use identical dependencies.
Pseudo-Versions
Sometimes you need to depend on unreleased code—a bug fix that hasn’t been tagged, or a feature branch under development. Pseudo-versions provide a way to reference specific commits while maintaining version ordering. Format:v<base>-0.<timestamp>-<commit>
stdlib@0.3.14
and another requires the pseudo-version above, MVS selects the pseudo-version
(it’s higher). This enables testing unreleased fixes without breaking version
resolution.
Use pseudo-versions sparingly. They represent unreleased, potentially unstable
code. For production builds, prefer tagged releases.
Commands
pcb migrate
Converts a V1 workspace to V2 format. Updatespcb.toml manifests and
rewrites .zen files to use the new import syntax.
- Detects the workspace root and git repository
- Converts
pcb.tomlfiles to V2 format (addspcb-version, converts[packages]to[dependencies]) - Rewrites
.zenfiles (expands aliases to full URLs, fixes paths)
git diff before committing.
pcb build
Builds a board or workspace member. Automatically resolves dependencies, fetches packages, and verifies the lockfile.pcb.sum is created with resolved versions and content hashes.
Subsequent builds verify against the lockfile.
pcb update
Updates dependencies to newer versions.pcb publish
Publishes packages by creating annotated git tags. Discovers which packages have changed since their last published version and tags them.- No version tag exists (unpublished)
- Content hash differs from the published tag
- Manifest (
pcb.toml) hash differs from the published tag
- Unpublished: starts at
0.1.0 - 0.x packages: bumps minor (
0.3.2→0.4.0) - 1.x+ packages: bumps patch (
1.2.3→1.2.4)
pcb.toml files are updated.
pcb info
Displays workspace and package information.pcb vendor
Copies dependencies to a localvendor/ directory for hermetic builds.
pcb.toml: