CellScript Wiki
Tutorial 04: Packages and CLI Workflow
Tutorial 04: Packages and CLI Workflow
Small experiments can be compiled as single .cell files. Once a contract has
more than one source file, a dependency, or a release target, use a package.
A package gives the compiler a stable place to find the entry file, build settings, dependencies, and lockfile. That makes builds repeatable for you, and reviewable for someone else.
What You Will Learn
- how to create a package;
- what belongs in
Cell.toml; - how to build, check, format, and document a package;
- which reports are useful during review;
- where the current package workflow intentionally stops.
Create a Package
Create an application-style package:
cellc init my_contract
cd my_contractThis creates a Cell.toml manifest and a source entry. Use this form when you
want a contract package with a concrete entry.
Create a library-style package:
cellc init my_lib --libAsk for a machine-readable summary when scripting:
cellc init my_contract --jsonRead The Manifest
A minimal manifest looks like this:
[package]
edition = "2026"
name = "my_contract"
version = "0.1.0"
cellscript_version = ">=0.26.0"
entry = "src/main.cell"
source_roots = ["src"]
[build]
target = "riscv64-elf"
target_profile = "ckb"
out_dir = "build"
[dependencies]
my_lib = { path = "../my_lib" }Read the manifest as a build promise:
edition = "2026"selects the source-language semantic epoch. It is mandatory; CellScript does not infer, migrate, or accept any other edition, and the year does not imply an annual release cadence;cellscript_versionis the SemVer range ofcellcreleases allowed to load the package. Legacy omission means*; new packages record an explicit minimum;entrytells the compiler where the package starts;source_rootstells the compiler which package directories contain.cellmodules;targetchooses assembly or ELF-style output;target_profilechooses the runtime assumptions;out_dirchooses where artifacts are written;- path, git, and registry source-package dependencies keep package inputs explicit and lockable.
Production Registry source-package resolution selects an accepted version from
the public API, filters out versions incompatible with the active compiler,
downloads its immutable source snapshot, and verifies object
SHA-256, safe paths, per-file BLAKE2b, Cell.toml, Edition/profile identity,
and the whole-tree source_hash. registry.json plus tag-pinned Git remain the
explicit offline/mirror authority. Local path dependencies remain the fastest
repeatable development workflow, and non-CellScript registry artifact profiles
still fail closed until they have their own resolver contracts.
The edition is one input to the emitted compatibility profile. Target, primitive assurance, metadata schemas, and wire ABIs keep independent version identities, so they can advance without creating a new source edition. The profile hash commits to the complete combination in every downstream build/deployment identity. See CellScript Edition Policy.
As a rule of thumb, cellscript_version answers “which compiler releases may
load this package?”, exact compiler/build evidence answers “which
implementation produced this output?”, Edition answers “how is this source
understood?”, and the resolved compatibility profile answers “which complete
source/target/ABI/schema contract was used?”. See
Package Compiler Requirements.
Understand Package Identity And Unification
CellScript identifies a package by its declared namespace and package name.
The local dependency key is only an alias. Two aliases may point to the same
package instance, while alpha/shared and beta/shared are different package
coordinates.
One selected runtime or test graph has one instance per coordinate. Every
incoming edge must agree on the selected version, exact Path/Git/Registry
source, feature root, and chain-identity-bound environment. Compatible Registry
ranges reuse the first selected candidate. Incompatible ranges, source
substitution, divergent features, or divergent environments fail during
resolution with E2601 before Cell.lock is written.
Feature roots are exact in the current resolver: it does not merge features = ["audit"] from one parent with features = ["metrics"] from another. Align
the declarations deliberately. Likewise, changing a Registry dependency to a
path checkout requires every incoming edge to name that path and either an
explicit cellc lock or a reviewed transactional upgrade; an alias alone is
not an override.
Cell.lock v5 records
resolver_model = "single-package-coordinate-v1". Locked and frozen commands
apply that same model without choosing replacements. Multiple versions of one
coordinate are rejected because source imports do not yet carry a
package-instance qualifier.
Multi-file Packages
Package builds are entry-driven, but the frontend loads the full package source
set before compiling the entry artifact. The compiler walks source_roots
(defaulting to src), parses every .cell file it finds, registers each file's
module declaration, and validates every use path::Symbol import against the
loaded module graph. Path dependencies are loaded the same way, so shared schema
packages can provide common Cell types without copying them into every contract.
There is no mod keyword and no implicit basename lookup. The module declared
inside the file is the source identity. Duplicate module declarations fail, bad
imports fail, and invalid package modules fail during build or check even
when the entry file does not reference them directly.
This is not a contract linker. Each CKB script remains an independent RISC-V artifact. Cross-file helper calls are resolved at compile time and inlined into the entry artifact, but there is no ELF linker and no cross-script runtime coupling. Use multi-file packages for schema reuse, shared helper functions, reviewable module organization, and repeatable source/package hashes.
For registry resolution, cellc add must remain a dependency
resolver, not a code-snippet finder. Anything reachable by cellc add must be
safe to participate in the package, build, deployment, or declared TCB identity
chain. Template-only material belongs behind copy/scaffold commands instead.
Build
Run the package build:
cellc buildUseful flags:
cellc build --target riscv64-asm
cellc build --target riscv64-elf
cellc build --target-profile ckb
cellc build --locked
cellc build --frozen
cellc build --offline
cellc build --features audit,metrics
cellc build --all-features
cellc build --no-default-features
cellc build --environment mainnet
cellc build --production
cellc build --jsonDependency builds are lock-authoritative. Run cellc lock for direct lock
creation, or cellc update-plan followed by cellc update --apply-plan for a
reviewed dependency change; build, check, and test otherwise consume only
the existing graph. --locked makes that assertion explicit,
--frozen also disables network access and every lockfile write, and
--offline permits only already materialized exact source pins.
build reads Cell.toml, compiles the current package entry, and writes the
artifact plus metadata sidecar under the configured output directory. A CKB
ELF build also writes canonical verified-artifact sidecars:
build/main.elf
build/main.elf.meta.json
build/main.elf.lowering.json
build/main.elf.sourcemap.jsonThe lowering record and source map are checked against final ELF bytes during compilation. They are structural/binding evidence, not a complete source-equivalence or chain-execution claim.
For a one-off source file, use the top-level compiler form instead:
cellc path/to/file.cellThat form is great for quick experiments. Packages are better when you need repeatability.
Build A Workspace
Use an explicit workspace when several independently built packages share one repository:
[workspace]
members = ["app", "right", "left", "shared", "experiments"]
exclude = ["experiments"]Member and exclude entries are literal directories. Included canonical paths
and package names must be unique. Every member keeps its own authoritative
Cell.lock; a virtual workspace root must not have one.
cellc check --workspace --frozen --offline
cellc build --workspace
cellc build -p appCellScript resolves the full member graph before compiling, rejects cycles and
stale member locks, and orders dependencies before dependents. -p app includes
the transitive members needed by app. A failed dependency blocks its
dependents. Successful non-frozen builds refresh each member's own build
identity and never encode artifact hashes as dependency nodes at the workspace
root. See Canonical Workspace Graph.
Inspect Resolution And Build Units
Use the package inspection commands before compiling when CI, an editor, or a reviewer needs the exact selection:
cellc resolve-graph . --environment testnet --offline --json
cellc build-plan . --target riscv64-elf --target-profile ckb --offline --jsonThese commands read existing locks and local sources without updating locks or
cache recency. resolve-graph shows aliases, runtime/test scope, features,
environment identity, source hashes, and stale lock nodes. build-plan adds
entry, target/profile, compatibility, VM/codec, expected outputs, direct units,
and cache status. A later cellc build --json reports the same unit identity.
See Package Resolve Graph And Build Plan.
Plan And Apply Dependency Upgrades
Use the transactional planner when an existing package or workspace lock must change:
cellc update-plan . --offline --output target/upgrade-plan.json
cellc update-plan . --package math --precise 2.4.1 \
--output target/math-upgrade.json
cellc update --apply-plan target/upgrade-plan.jsonThe first two commands resolve the candidate in memory and leave every
Cell.lock byte-identical. The receipt contains exact old/new lock hashes,
node and edge changes, reverse-dependent compilation, independent API/layout/
runtime/effects/builder/deployment results, ProtocolBundle input identities,
and deployment-authorization status. Package-scoped updates preserve unrelated
node records exactly.
Apply rejects a tampered plan, a different compiler version, a changed old lock, an escaping or symlink path, a non-canonical candidate lock, or a missing policy acknowledgement. Supply required codes only after review:
cellc update --apply-plan target/upgrade-plan.json \
--acknowledge UPG2003,UPG3102Only the planned lockfiles are replaced. This workflow never edits
Deployed.toml, signs, deploys, publishes, proves TYPE_ID authority, or runs a
state migration. See Transactional Upgrade Plans.
Execute Package Scenarios
Executable tests are versioned *.scenario.json files under tests/. Name a
backend explicitly:
cellc test --backend simulator
cellc test --backend ckb-vm
cellc test --backend all --jsonsimulator is fast development evidence. ckb-vm executes the emitted ELF and
is local authoritative runtime evidence. Use cellc test --no-run only when
compile-only checking is intentional. Without --no-run, an omitted backend
or an empty scenario set is an error rather than a false pass.
The v1 scenario format rejects unknown fields and validates named live Cells,
replacement steps, Scripts, deps, headers, since, witnesses, capacity and
size limits, and exact runtime error code/name pairs. Its multi-step Cell set
is a local bookkeeping oracle; the CKB-VM backend currently supports
no-argument entries and does not inject those declared Cells into syscalls.
Transaction-syscall scenarios remain with the repository's stateful CKB
oracle. See Verified Artifacts and Executable Tests.
Check Without Writing Artifacts
Use check when you want fast feedback:
cellc check
cellc check --all-targets
cellc check --target-profile ckb
cellc check --production
cellc check --deny-runtime-obligations
cellc check --jsoncheck --all-targets is useful before committing. It catches source and profile
problems without producing build artifacts.
Diagnostic Output Formatting
Use the global --json flag when a CI job or agent loop needs structured
results without parsing human text:
cellc check --target-profile ckb --json
cellc build --jsonColour is controlled separately:
cellc check --color=auto
cellc check --color=always
cellc check --color=never
NO_COLOR=1 cellc check--json and --color are global flags and may appear before or after every
subcommand. --json emits one stdout document for success or failure, so a
caller can always parse the same stream. The old --message-format=json
spelling remains a hidden deprecated alias during the compatibility window.
Structured failures include an error category and the process exit code.
Usage errors exit with 2, ordinary compilation failures with 1, I/O with
74, network availability failures with 69, authentication failures with
77, and internal failures with 70.
Backend failures use stable E2xxx codes. cellc explain E2202 --json returns
the rule name, description, and recovery hint; LSP diagnostics expose the same
code and a codeDescription link.
Format And Generate Docs
Format the package:
cellc fmt
cellc fmt --check
cellc fmt --jsonGenerate package docs:
cellc doc
cellc doc --jsonGenerated docs summarize modules, actions, resources, receipts, locks, flow rules, and lowering metadata.
Audit And Evidence Reports
When a package is ready for review, ask the compiler for the facts it already knows:
cellc metadata . --target riscv64-elf --target-profile ckb -o build/main.metadata.json
cellc expand . --target riscv64-elf --target-profile ckb --json -o build/main.semantic.json
cellc constraints . --target riscv64-elf --target-profile ckb -o build/main.constraints.json
cellc abi . --target-profile ckb
cellc scheduler-plan . --target-profile ckb --json
cellc opt-report . --target riscv64-elf --target-profile ckb --jsoncellc expand exposes the canonical semantic foundation used by the 0.26b
checker boundary. Its JSON form is machine-checkable; the default text form is
only a diagnostic rendering and is not a semantic hash input.
To request a bounded, non-mutating Edition 2027 candidate from an Edition 2026 package:
cellc migrate . --to 2027
cellc --json migrate . --to 2027 -o build/migration-report.jsonThe preview recognizes only a self-contained module with one final entry. A
Type Script must already be an exact sequence of source require conditions,
exhaustive std::lifecycle::transfer, and matching
std::cell::preserve_capacity; a Lock Script must contain only source
require conditions and explicit protected, lock_args, or witness
parameters. The command preserves every byte outside the entry and emits
nothing until the old and candidate CoreSemanticId values and generated
RISC-V ELF bytes match. It does not edit Cell.toml, Cell.lock, source,
deployment state, or dependencies. Unsupported input stops with a diagnostic;
there is no partial migration. Explicit visibility and mutable/reference role
forms also stop until the native container can preserve those interface
semantics exactly.
For CKB-specific builder and deployment review:
cellc constraints . --target riscv64-elf --target-profile ckb --json
cellc abi . --target-profile ckb --action transfer
cellc entry-witness . --target-profile ckb --action transfer
cellc ckb-hash --file build/main.elf
cellc verify-artifact build/main.elf --expect-target-profile ckb --verify-sources --productionBuilder-facing contract commands expose the metadata that transaction builders consume. Prefer the canonical 0.21 nested forms:
cellc action build . --action transfer --json
cellc entry-witness . --target-profile ckb --action transfer
cellc explain assumptions . --target-profile ckb --json
cellc tx solve . --target-profile ckb --json
cellc tx validate --against build/main.elf.meta.json --tx tx.json --json
cellc tx trace --against build/main.elf.meta.json --tx tx.json --json
cellc deploy plan . --target-profile ckb --json
cellc deploy verify --plan Deployed.toml --json
cellc registry verify --json
cellc package verify --json
cellc auth capability create --principal-id <principal_id> \
--scope publish:cellscript/my_contract \
--expires 90d --json
cellc gen-builder . --target typescript --target-profile ckb --jsonpackage verify checks build identity as well as the dependency graph. A
freshly cloned example intentionally carries a graph-only Cell.lock; run
cellc build --locked first to populate [package.build]. A frozen build
cannot add that local evidence because --frozen suppresses every lockfile
write.
Legacy flat aliases such as solve-tx, deploy-plan, and
explain-assumptions remain executable for compatibility, but they are hidden
from public discovery. Prefer --json where a command offers it, and reserve
human summaries for interactive review.
0.21 builder/deployment review also records action-aware scan selector
evidence, variable-length args_parts, and manifest-backed CellDep completion
where the adapter has enough deployment metadata to resolve them. Missing or
mismatched live-cell scan evidence fails closed.
These reports are not busywork. They answer questions reviewers will ask:
- what is the entry ABI;
- what witness layout is expected;
- what capacity or runtime obligations remain;
- what CKB hash policy is being used;
- whether the artifact still matches the source and metadata.
They do not replace chain acceptance reports, builder-generated transactions, occupied-capacity evidence, or CKB production gates.
Local Dependencies
Add a local dependency:
cellc add my_lib --path ../my_libadd --path records the dependency in Cell.toml. To resolve the dependency
graph and write Cell.lock, run:
cellc lockYou can also add and lock a local dependency in one command:
cellc install my_lib --path ../my_libThe current CLI can record a Git dependency URL:
cellc add math --git https://example.com/math.git
cellc install math --git https://example.com/math.gitFor reviewable package identity, a manifest may name a branch or tag during
development, but cellc lock/update immediately normalizes it to a full
40-hex commit and immutable cache. A later branch movement does not affect
builds until the next explicit repin.
Remove it:
cellc remove my_libadd, install, and normal dependency removal refresh the lockfile so direct
and transitive local path dependencies stay consistent. update instead emits
a plan unless --apply-plan names a reviewed receipt.
Cell.lock v5 is a graph rather than a flat list. It declares the
single-package-coordinate-v1 resolver model and binds the exact root
manifest digest, each dependency manifest and whole source tree, package
compiler requirements, the compiler release that resolved the graph, outgoing
alias-to-node edges, feature/test modes, and named CKB environments. Local
projects should commit it to version control: the lockfile is reviewed build
input, not a local cache, and normal build/check/test commands do not silently
repin it. Locks from versions 1 through 4 require an explicit cellc lock, or
an upgrade plan followed by explicit apply. Dependency aliases can differ from
declared package names:
[dependencies.math]
package = "canonical_math"
version = "^1.2.0"Optional dependencies are activated through versioned feature roots:
[dependencies.audit]
version = "^1.0.0"
optional = true
[features]
default = []
auditing = ["dep:audit"][dev_dependencies] are present only in the cellc test graph. Feature
cycles, unknown features, alias collisions, and unknown dep: targets fail
closed. [build.dependencies] is reserved until CellScript has an isolated
build-script execution contract.
For chain-dependent selection, declare the chain, not an implicit label:
[environments.mainnet]
chain_id = "ckb"
genesis_hash = "0x...32-byte-genesis-hash..."
[dependency_overrides.mainnet.registry_types]
version = "=2.0.0"
namespace = "cellscript"When overrides exist, --environment mainnet is mandatory. The environment
root in Cell.lock binds both chain_id and genesis hash.
For a transitive package, the name mainnet has no special meaning and is not
inherited. CellScript selects the unique dependency-local environment with the
same chain identity, or you can make the edge policy explicit:
[dependencies.protocol]
path = "deps/protocol"
use_environment = "production"
[dependencies.codec]
path = "deps/codec"
environment_independent = trueThe first mapping is accepted only when production has the same chain_id
and genesis hash as the root selection. The second skips dependency-local
overrides while preserving the root identity for later transitive edges.
cellc add exposes the corresponding --use-environment NAME and
--environment-independent flags.
The portable checked-in example exercises these inputs together:
cd examples/package_graph
cellc check --frozen --offline --environment mainnet
cellc check --frozen --offline --environment testnet --features full
cellc test --no-run --frozen --offline --environment testnet --all-featuresIts local dependency alias is distinct from the declared package name, and its
testnet override resolves a different exact version of the same declared
package. Omitting --environment is an intentional fail-closed example.
Advanced ecosystems may declare a hash-pinned bounded resolver. It runs only during explicit lock/update, without a shell or inherited environment, and must normalize its versioned JSON response to an exact Registry version or Git commit. Locked builds never invoke it:
[resolvers.vendor]
command = "/absolute/path/to/vendor-resolver"
sha256 = "sha256:<resolver-executable-digest>"
args = ["resolve"]
[dependencies.math]
package = "canonical_math"
version = "^1.2.0"
resolver = "vendor"Registry Resolver Boundaries
CellScript's registry design follows the same split as the package identity model:
- package identity answers which source was referenced;
- build identity answers which artifact and metadata were produced;
- deployment identity answers which CKB Cell, CellDep, or runtime artifact is being used.
Registry discovery is broad. It indexes CellScript source packages,
runtime verifiers, deployed CKB artifacts, reproducible artifacts, and even
external CKB tooling artifacts such as bootstrapper outputs. Resolver profiles
must stay narrower: an object can be discovered without being installable by
cellc add.
That means registry resolution is stricter than discovery. The versioned
cellscript-registry-profile-catalog-v1 marks only the
cellscript_source + dependency contract as dependency-resolving. cellc add
and cellc install reject every other profile.
Other profiles use explicit cellc artifact commands and fail closed on
unknown fields, identities, roles, or lifecycle state:
| Kind | cellc add |
Current explicit boundary |
|---|---|---|
source_library / profile_library |
yes | Compiler-backed source and API identity are pinned in Cell.lock. |
runtime_verifier |
no | artifact fetch, verify, and pin; verifier ID, IPC ABI, artifact, build, security, and production CellDep remain explicit TCB facts. |
deployable_contract |
no | artifact fetch, verify, pin, record-deployment, and cell-dep bind build and live mainnet deployment identity; artifact ls-idl validates, binds, bundles, or resolves a Lock Script interface without making it a source dependency. |
reproducible_binary |
no | artifact reproduction-evidence binds independent builders to source, recipe, environment, executable, and logs before verified use. |
template |
no | artifact copy authenticates a bounded file map, rejects traversal and overwrite, and then leaves local project source. |
The rule is intentionally blunt:
Discovery can be broad; dependency resolution is narrow.
Anything reachable by cellc add must be dependency-safe, artifact-safe,
deployment-fact-safe, or declared-TCB-safe.
Anything scaffold-only must be copied, not resolved.For example, a BIP340 verifier package can have no business parameters and still be resolver-safe because it is a runtime verifier artifact. Its manifest or registry record must identify the verifier capability, IPC ABI, artifact hashes, build profile, TCB/security status, and any production CellDep pins.
A NovaSeal starter project, by contrast, is not dependency-safe merely because
it contains useful .cell code. If users are expected to copy it and edit terms,
authorities, manifests, or deployment pins, it belongs in a cookbook or template
flow, not in dependency resolution. Use cellc artifact copy, then treat the
authenticated result as local project source.
It should not be installed with:
cellc add novaseal/mvb-starterThis keeps the registry as a verifiable dependency and artifact discovery layer, not a general examples marketplace.
For mixed projects, keep the records separate. A CellScript app may depend on a
CellScript library, reference a deployed verifier as TCB evidence, use a
reproducible bootstrapper artifact during its build process, and copy a cookbook
starter into local source. Those are four different profile boundaries. They
may share one registry service and one namespace/name style, but they must not
share one unchecked dependency path.
Package Information
cellc info
cellc info --jsonUse info when you want a quick view of the package boundary before building or
debugging dependency resolution.
Registry Commands
Registry source-package installation and registry-backed update-plan are
supported for the CellScript source-package profile. The preferred interactive first-use
path is cellc publish --authorise: it creates a 15-minute browser session,
authorises a wallet-rooted delegated key, and resumes the publish after the
Registry returns the matching key ID. --no-open supports remote terminals.
Later cellc publish calls use the active scoped key.
For CI, recovery, or an external-wallet handoff, cellc auth capability create --principal-type <joyid_ckb|ckb_secp256k1> --principal-id <principal_id> creates
the wallet payload; submit the wallet signature and claim the namespace before
publishing. Inside a package directory, omitting --scope infers only the exact
publish scope. Add deployment or availability scopes explicitly when that
delegated key genuinely needs those actions; none implies another.
The principal_id is cryptographically derived from the signer, not from a
display label. The same metadata can still be
mirrored with cellc publish --offline to registry.json and Git tags for
audit, local fixtures, and offline fallback. cellc registry add manages discovery/claim metadata rather than
ordinary version publication.
Non-CellScript profiles publish with Artifact.toml and
cellc publish --artifact-manifest Artifact.toml. Consumers use the explicit
cellc artifact fetch, verify, pin, copy, reproduction-evidence,
record-deployment, cell-dep, commitment, and set-availability commands;
none silently turns an executable, TCB object, or template into a source
dependency. run, repl, and cryptographic audit-signature verification
retain their separate documented assurance boundaries.
For LS-IDL Lock Scripts, cellc artifact ls-idl validate|bind|bundle prepares
the byte-exact interface contract and fetch resolves it by chain-verified
Script identity. The raw IDL SHA-256/executable-suffix relationship is an
identity check, not proof of implementation correctness.
Next
With a repeatable package workflow in place, continue with CKB Target Profiles.