Packages and platforms — how a project grows
The decision this page settles: how code is organized past one file. Manifest keys in table form are in ref.md; targeting more than one OS is platforms.md; the commands are tooling.md.
1. Modules are files; imports are paths
Every .cplus file is a module. There is no module declaration — the file's
place on disk is its identity, and an import names it by path:
import "stdlib/io" as io; // module `io` in the dependency `stdlib`
import "./catalog" as catalog; // file catalog.cplus next to this one
import "stdlib/str" as _; // discard alias: extension methods only
The alias is mandatory and is the only name the import introduces —
io::println, catalog::model(). as _ pulls in a module for its
extension methods (the blessed impl str block) without binding a name.
Privacy is the underscore. Items, fields, and methods are public by
default; a leading _ makes them module-private (E0403 across modules).
There is no pub keyword and no visibility tree — one character, one rule.
Extensions of another package's types apply only where that module is
imported.
A file compiles only if some import reaches it from the entry — an orphan
.cplus under src/ is a warning naming exactly that.
2. The manifest
Cplus.toml, and only [package] is required:
[package]
name = "myapp"
version = "0.0.1"
edition = "2026"
[dependencies] # the portable tier — resolved at vendor/<name>/
stdlib = "*"
facet = "*"
[macos.dependencies] # platform-scoped: exists only on this platform
facet_appkit = "*"
Dependency resolution is flat and deliberate: the resolver validates every import in the build against this one manifest — it does not read a dependency's own manifest. A backend's transitive closure is therefore named here too. Clear and noisy beats magic: the manifest is the complete bill of materials.
Dependencies are directories the resolver finds project-first:
<project>/vendor/<name> wins, then the per-user store
(~/.cplus/<tier>/vendor/<name>) that cpc pm install fills. A bare
stdlib = "*" is the toolchain's own package at the toolchain's version;
third-party packages use the pinned tree-URL form
(https://…/tree/main/<pkg>@1.2.3), and cpc pm add writes a package
plus its declared closure for you. Monorepo work symlinks vendor/ as
before.
3. Apps: the entry names you, the target shapes you
A package with an entry is an application. Three ways to have one:
[package]
entry = "src/main.cplus" # explicit — or omit it: src/main.cplus is the default
[ios]
entry = "src/main_ios.cplus" # platform override
[ios.dependencies]
facet_uikit = "*"
What a build produces is the target's fact, not the manifest's:
| Platform class | Platforms | cpc build produces |
Entry shape |
|---|---|---|---|
| self-linked | macos, linux, windows | an executable | fn main() -> i32 (E0414 if missing) |
| external-builder | ios, android, esp32 | lib<name>.a + C header |
export extern fn the platform shell calls (fn main is E0409) |
One source tree, each platform its own path to an artifact:
cpc build on the Mac links the binary; cpc build --target ios-arm64-simulator stops at the archive and Xcode owns the link.
Cross-target artifacts land in target/<target-name>/<mode>/.
Declared platform entries scope the app. The moment any [<platform>] entry exists, the src/main.cplus default stops applying elsewhere —
building for a platform you didn't name is E0413, never a silent guess. An
iOS-only app is exactly two manifest lines and a clean error everywhere
else.
4. Libraries: no entry, no section
A package with no entry is a library. Its consumers compile it from source;
cpc build inside it archives the whole src/ tree (every module, so the
archive and the generated headers can never disagree). stdlib is just
[package] and nothing else.
Two special cases earn keys:
[library]— a C-ABI product:kind = "staticlib" | "cdylib" | "both", and an optionalentrywhose top-level names become the bare C symbols (that entry's import tree is the library). This is for shipping to C consumers; a C+-consumed library never needs it.[build]— prebuild is the default (2026-08-16): a library package is compiled once intolib/<triple>/<name>.a+lib/include/headers on the first consumer build, and later builds link instead of recompiling. The content fingerprint (source + triple + debug/release + compiler version) rebuilds the slice the moment anything changes; atouchchanges nothing, a one-character edit rebuilds. Two knobs:prebuild = falseopts a package out (e.g. one side of a dependency cycle — mutually-dependent packages cannot be compiled standalone);dev = trueoverrides everything while you work on the package — always-from-source, restated on stderr every build. Apps are never prebuilt (they aren't dependencies). A package must declare its own[dependencies]to be prebuildable: the slice is compiled standalone, and its own manifest is the world there.
5. The link surface
An app's own linker inputs live in [link]:
[link]
frameworks = ["Metal", "Foundation"] # -framework X (Apple platforms)
libs = ["objc", "z"] # -lX
search-paths = ["/usr/local/cuda/lib64"] # -L + rpath; ${VAR} expansion allowed
extra-objects = ["shaders.o"] # prebuilt .o appended to the link
A dependency's [link] travels automatically: depending on metal is
enough to get -framework Metal. The dep walk validates
manifest-versus-filesystem for bundled binaries (a declared file that is
missing, or an undeclared one present, is an error — the manifest is truth).
6. Platform-variant code
Three mechanisms, three axes — they compose and don't compete:
- File override (compile-time, import-level): a sibling
<module>_<platform>.cplusshadows<module>.cpluswhen building for that platform — same public surface, different implementation. This is the only way to vary imports per platform (kqueue vs epoll, AppKit vs UIKit), because C+ has no in-source#if: the unit of platform variation is the whole file. The base file is optional, and anandroidbuild falls back to a_linuxvariant when there is no_androidone. [<platform>.dependencies](manifest-level): a package that exists only on that platform.#platform()/#arch()/#target()(value-level):strconstants naming the active target. Both branches of anifon one still compile, so they can pick a padding or a port, never an import.
Off-platform imports fail honestly: importing a [macos.dependencies]
package in an iOS build is E0866 naming the platform it was declared for.
Android has one more, for the Java side: [android.maven] pins a
third-party AAR by Maven coordinate, "group:artifact" = "version", exact.
cpc pm add . --maven com.google.android.gms:play-services-maps:19.0.0
writes it, resolves the POM closure and downloads it — no Gradle anywhere,
because Gradle is only the resolver and resolution over pinned coordinates
is reading XML. cpc pm maven classpath then hands d8 the jars. Price a
coordinate before taking it: cpc pm maven price <coord> prints the
artifact count and the megabytes, which is what the decision turns on.
The rule of thumb from the framework work: OS decides files (backends, syscalls), form factor decides values (a phone-shaped shell is portable facet code — pick it at runtime, not per-OS).
The full model — the resolution order, the Android fallback, which variants a library archive keeps, the target list, and the external-builder handoff — is platforms.md.
7. Tests
cpc test discovers #[test] functions across the resolved import tree
and runs them; the entry is resolved by a ladder, first match wins:
src/test_main.cplus— a dedicated test root (imports the surface the suite should cover); by convention, no manifest key needed- the app entry for the current platform
- the
[library]target src/<package-name>.cplus— the root module, for plain library packages
House discipline: every module ships unit, e2e, and negative tests; a
package is testable from its own directory with cd <pkg> && cpc test.
Discovery covers the whole resolved import tree, so a dependency's tests
run alongside yours — see testing.md.
8. The commands that read all this
cpc build # this platform's artifact
cpc build --target ios-arm64 # that platform's artifact
cpc check # whole-project front-end, no codegen
cpc test # the ladder above
cpc graph / query / mcp # the resolved code graph (use it over grep)
cpc explain E0413 # any code, offline