Project lifecycle reference¶
This page is the language-facing reference for Incan project lifecycle concepts: project roots, incan.toml metadata, version bumps, and named environments. For the CLI flag reference, see CLI reference.
Project root¶
An Incan project root is the nearest ancestor directory containing incan.toml.
greeter/
|-- incan.toml
|-- src/
| |-- main.incn
| `-- greet.incn
`-- tests/
`-- test_main.incn
Project-aware commands use the project root for metadata, dependencies, source-root resolution, lock files, and lifecycle configuration. Nested projects are allowed; the nearest incan.toml wins.
Single-file commands can still run without a project:
incan run hello.incn
incan run -c "import this"
Project-level features such as manifest dependencies, version management, and named environments require incan.toml.
Workspaces¶
An Incan workspace is a bounded collection of ordinary Incan projects. It coordinates which projects are members, their effective shared dependencies, and one root-owned incan.lock; it does not turn the repository into one package or make member versions and publication lockstep.
A root that has both [project] and [workspace] is a rooted workspace: the root project is a member without listing ".". A root that has only [workspace] is virtual and must list at least one project member.
[workspace]
members = ["packages/*"]
default-members = ["api", "worker"]
exclude = ["packages/experimental"]
[workspace.rust-dependencies]
serde = { version = "1", default-features = false }
[workspace.envs.ci]
env-vars = { CI = "true" }
Each non-root member has its own incan.toml and opts into a shared declaration explicitly:
[project]
name = "api"
version = "0.1.0"
[rust-dependencies]
serde = { workspace = true, features = ["derive"] }
[tool.incan.envs.ci]
extends = ["workspace:ci"]
The workspace owns dependency identity—version, source, package rename, and default-feature baseline. A member may add Rust features and choose its own optional use. Shared declarations are inactive until a member declares { workspace = true }; a member-local incan.lock is never authoritative.
Selecting members¶
Workspace-aware commands resolve their scope before compiling, testing, formatting, running, or mutating a member:
# Current member when invoked below packages/api.
incan test
# The root's default-members, or every member for a virtual root without defaults.
incan check --format json
# Every member in deterministic workspace order.
incan build --workspace --report json
incan fmt --workspace --check
# One explicitly named or root-relative member.
incan run --member api
incan version patch --member packages/worker
check, build, test, and fmt fan out across a selected set. Their JSON output includes the workspace root, selection origin, and member attached to each result. run and version require exactly one member and explain how to choose one when a scope selects several. incan lock is always workspace-wide: it resolves every member and writes one canonical root lock regardless of the member that started the command.
Use incan workspace inspect to see the validated graph rather than reconstructing membership from directory names:
incan workspace inspect
incan workspace inspect --workspace --format json
incan workspace inspect --member packages/api --format json
The JSON projection includes members, selection origin, inherited dependency provenance, explicit workspace environment extensions, lock state, stale member-local locks, and currently-unused shared declarations. Workspace capability application remains member-local in this release: cross-member mutation needs a scoped plan and policy evaluation, neither of which is approximated by this foundation.
Workspace lock publication uses the same crash-safe staging, synchronization, atomic replacement, and parent-directory synchronization sequence documented by std.fs. Publishers coordinate through a stable compiler-private guard under ignored target/incan_lock state rather than creating a new sidecar in the project root. If a legacy .incan.lock.incan.lock sidecar already exists, the compiler acquires that identity before its active guard so an older compiler using the existing inode remains serialized. Whenever the legacy sidecar is absent—whether it never existed or was removed—old and new compilers must not publish concurrently because an older compiler cannot discover the hidden guard. This preserves a prior complete root lock or a new complete root lock for cooperative readers; it is not a multi-file workspace transaction.
incan.toml¶
incan.toml is the project manifest. It is intended to be edited and committed.
Common sections:
| Section | Purpose |
|---|---|
[project] |
Project metadata: name, version, description, authors, license, readme, toolchain requirement |
[project.scripts] |
Named Incan entry points such as main = "src/main.incn" |
[build] |
Build settings such as source-root |
[dependencies] |
Incan library dependencies |
[rust-dependencies] |
Rust crate dependencies available to production code |
[rust-dev-dependencies] |
Rust crate dependencies available only to tests |
[tool.incan.envs.<name>] |
Named lifecycle environments for incan env |
[workspace] |
Explicit multi-project topology, defaults, and exclusions |
[workspace.*] |
Reusable dependency, environment, policy, and source declarations for workspace members |
Minimal application manifest:
[project]
name = "greeter"
version = "0.1.0"
requires-incan = ">=0.5.0-0,<0.6.0"
[project.scripts]
main = "src/main.incn"
Project scaffolding¶
incan new creates a new project directory. With no positional name, it prompts interactively when stdin is a terminal:
incan new
For scripted use, pass a name or --dir and use --yes:
incan new greeter --yes
incan new --dir apps/greeter --yes
Both incan new and incan init accept metadata flags:
| Flag | Meaning |
|---|---|
--description <text> |
Write [project].description |
--author <author> |
Add one [project].authors entry |
--license <license> |
Write [project].license |
--name <name> |
Override the project name for incan init |
--version <version> |
Override the initial version for incan init |
--yes / -y |
Skip prompts and use defaults/flag values |
incan new derives the project name from NAME, then from --dir, then from an interactive prompt. In non-interactive mode it requires either NAME or --dir.
The generated scaffold includes src/main.incn, tests/test_main.incn, README.md, .gitignore, a main script, and a release-line requires-incan constraint. The starter test imports the generated public greeting() function, so the project is immediately runnable and testable instead of containing only a placeholder assertion.
[project]¶
[project] is the canonical metadata table.
| Key | Type | Notes |
|---|---|---|
name |
string | Stable project name |
version |
string | SemVer-compatible project version |
description |
string | Short human-readable description |
authors |
list of strings | Author names, optionally with email addresses |
maintainers |
list of strings | Maintainer names, optionally with email addresses |
license |
string | SPDX identifier or expression |
license-files |
list of strings | License file paths, relative to the project root |
readme |
string | Path to README, relative to project root |
homepage |
string | Project homepage URL |
repository |
string | Source repository URL |
documentation |
string | Documentation URL |
issues |
string | Issue tracker URL |
keywords |
list of strings | Search/discovery keywords |
classifiers |
list of strings | Future-facing classifier strings |
requires-incan |
string | SemVer requirement for the Incan toolchain |
private |
bool | Marks a project as not intended for publishing |
incan init --name greeter --version 0.1.0 writes the core project keys, readme = "README.md", and a default main script. Metadata flags or interactive answers populate optional fields such as description, authors, and license.
Toolchain requirements¶
requires-incan is an executable compatibility guard. Project-aware execution commands enforce it before doing build, test, lock, or env-script work:
[project]
name = "greeter"
version = "0.1.0"
requires-incan = ">=0.5.0-0,<0.6.0"
If the active compiler is outside the range, incan run in project mode, incan build, incan test, incan lock, and incan env run fail early with a diagnostic that names the active compiler version and the contributing constraint layers. Single-file and inline commands without a discovered incan.toml remain manifest-free and do not infer a requirement.
Development compilers identify themselves with prerelease versions such as 0.5.0-dev.N. Generated 0.5 projects therefore use a prerelease-aware lower bound (>=0.5.0-0,<0.6.0) so local development builds and final 0.5 releases both satisfy the starter constraint.
[project.scripts]¶
[project.scripts] maps script names to Incan source files:
[project.scripts]
main = "src/main.incn"
migrate = "src/migrate.incn"
These are Incan entry points, not shell commands. Use [tool.incan.envs.<name>.scripts] for shell-style command argv lists.
Source root¶
The source root controls how local imports resolve.
Resolution order:
- Use
[build] source-rootwhen it is set. - Otherwise use
src/when the project has that directory. - Otherwise use the project root.
[build]
source-root = "src"
Tests resolve imports against the same source root as production code, so tests/test_main.incn can import src/greet.incn as from greet import greet.
incan version¶
incan version updates the project version in incan.toml.
incan version patch
incan version minor --dry-run
incan version --set 1.2.0
Supported bump names:
| Bump | Result |
|---|---|
major |
Increment the major version and clear prerelease metadata |
minor |
Increment the minor version and clear prerelease metadata |
patch |
Increment the patch version and clear prerelease metadata |
alpha |
Add or advance an -alpha.N prerelease |
beta |
Add or advance a -beta.N prerelease |
rc |
Add or advance an -rc.N prerelease |
dev |
Add or advance a development prerelease |
Useful flags:
| Flag | Meaning |
|---|---|
--dry-run |
Print the old version, new version, and modified files without writing changes |
--set <version> |
Set an explicit SemVer-compatible version |
--keep-prerelease |
Keep prerelease metadata when applying a release-core bump |
This command changes the project version only. It does not update the compiler, Cargo package versions in the Incan repository, or the requires-incan toolchain requirement.
incan env¶
incan env runs named scripts inside named project environments. The ambient default environment is always available, and other environments include it unless they set detached = true.
Mental model:
- An env is a named command context, not a Python-style virtualenv.
- Env scripts are explicit argv lists stored in
incan.toml. incan envis for repeatable workflows such as local test commands, CI commands, docs builds, or release checks.- Plain
incan run,incan test, andincan buildremain valid direct commands; envs are an overlay for named workflows, not a replacement for the base CLI.
Subcommands:
| Command | Purpose |
|---|---|
incan env list |
List available environment names |
incan env show [env] |
Show an overview table or print one resolved environment |
incan env run <env> <script> |
Run one configured script inside one environment |
Example configuration:
[tool.incan.envs.default]
env-vars = { INCAN_NO_BANNER = "1" }
[tool.incan.envs.default.scripts]
run = ["incan", "run"]
test = ["incan", "test"]
[tool.incan.envs.unit]
env-vars = { INCAN_FANCY_ERRORS = "1" }
[tool.incan.envs.unit.scripts]
test = ["incan", "test", "tests/"]
[tool.incan.envs.ci]
extends = ["unit"]
requires-incan = ">=0.4,<0.5"
[tool.incan.envs.ci.scripts]
test = ["incan", "test", "--locked", "tests/"]
build = ["incan", "build", "src/main.incn", "--locked"]
[tool.incan.envs.docs]
detached = true
cwd = "workspaces/docs-site"
[tool.incan.envs.docs.scripts]
build = ["python3", "-m", "mkdocs", "build", "--strict"]
Example commands:
incan env list
incan env show
incan env show default
incan env show unit
incan env show ci
incan env show docs
incan env run default run
incan env run unit test -- -k "greet"
incan env run ci build
incan env run docs build
incan env run unit test --dry-run -- -k "greet"
Arguments after -- are appended to the configured script argv.
incan env show with no env name prints a compact overview table, similar to Hatch. incan env show default works even when [tool.incan.envs.default] is not declared. In that case, default exposes the project base overlay with no extra overrides.
Typical pattern:
defaultfor shared baseline commands and environment variables- a local developer env such as
unit - a stricter automation env such as
ci - a detached env for a separate subtree such as
docs
Environment fields¶
| Field | Type | Meaning |
|---|---|---|
extends |
list of strings | Other environments to merge before this one |
requires-incan |
string | Additional Incan toolchain requirement for this env |
detached |
bool | Do not include default automatically |
cwd |
string | Working directory for scripts, relative to the project root unless absolute |
env-vars |
table | Environment variables to inject into the process |
scripts |
table of string lists | Script names mapped to argv lists |
An env-level requires-incan is combined with the project requirement and any inherited env requirements. This can make an automation env stricter than day-to-day development without weakening the project baseline. Use incan env show <env> or incan env run <env> <script> --dry-run to inspect the effective requirement and current compatibility before running the script.
RFC 073 also reserves declarative environment matrices, but matrix expansion is not part of the 0.3 lifecycle implementation. Named envs resolve one configuration unless a later release documents matrix support.
Dependency overlay tables may also be used for environment-specific dependencies:
[tool.incan.envs.integration.rust-dev-dependencies]
testcontainers = "0.15"
Environment merge rules¶
Overlay order:
project base -> default -> extends entries -> target environment
Rules:
| Field | Merge behavior |
|---|---|
scripts |
Merge by name; later overlays replace earlier scripts with the same name |
env-vars |
Merge by key; later overlays replace earlier values with the same key |
cwd |
Last configured value wins |
| Dependencies | Additive; same dependency key replaces version/source and unions features |
default is always present conceptually; declaring [tool.incan.envs.default] customizes it rather than creating it from nothing. Duplicate environment inclusion and inheritance cycles are errors. Use incan env show <env> to debug the resolved overlay chain.
Practical implications:
- Use
defaultfor shared baseline behavior such asINCAN_NO_BANNER=1or commonrun/testscripts. - Use
extendswhen one env is a stricter refinement of another, for exampleciextendingunit. - Use
detached = truewhen an env should ignore the default baseline entirely, such as a docs build rooted in another directory. - Prefer shallow inheritance. If you need a diagram to explain your env graph, it is probably too complex.