SCPP project-scale build system design
English

SCPP project-scale build system design

Status: research + reviewed design draft before implementation

0. Scope and headline

This document proposes how scpp should grow from today’s single-file / single-module CLI into a project-scale build system suitable for real multi-package, multi-module codebases.

The core recommendation is:

  1. keep zero-config single-file operation as the non-project case;
  2. use an optional manifest (scpp.toml) as the boundary for package/workspace project mode;
  3. make the public UX Cargo-like and integrated (scpp build, scpp run, scpp package, workspaces, profiles, dependency declarations);
  4. keep the internal model artifact-first, built around scpp’s existing .scppm / .scppa split rather than around CMake-style hand-maintained target graphs.

In short: Cargo-like user experience, CMake-like project scale, scpp-native artifact semantics.

1. Current scpp starting point

Today’s reference state matters because the build-system design should extend it, not fight it.

1.1 Existing language/package facts

From the existing module/library chapter and binary-format specs:

This is a strong starting point: scpp already has the right artifact boundaries for scalable builds.

1.2 Existing CLI facts

The current CLI already has two useful layers:

That means project build does not need to invent a new low-level compiler primitive. It mainly needs an orchestrator above primitives that already make sense.

1.3 Existing manifestless directory-build precedent

The paused dev-agent/project-build-mode branch is also important context:

This is useful as a narrow manifestless directory-build precedent, but it cannot by itself express:

So the central design problem is not whether such convenience exists. It is whether manifestless convention is enough for large projects. I think the answer is no.

2. Research findings from mature systems

2.1 Cargo: the most relevant high-level precedent

Cargo’s shape is the strongest precedent for scpp’s public UX.

Verified from the official Cargo docs:

I also verified a minimal local Cargo workspace:

$ cargo build --workspace
   Compiling lib v0.1.0 (.../lib)
   Compiling app v0.1.0 (.../app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s

and Cargo created a single workspace-root Cargo.lock and shared target/ directory.16

Cargo lessons for scpp

Keep:

Do not copy directly:

2.2 CMake: the best precedent for composition and transitive native requirements

Verified from the official CMake docs:

I also verified the two-step CMake workflow locally:

$ cmake -S . -B build
-- Configuring done
-- Generating done
-- Build files have been written to: .../build

$ cmake --build build
[100%] Built target app

This matters because it demonstrates the core generator split very concretely.23

CMake lessons for scpp

Keep:

Do not copy directly as the primary user model:

Why not? Because CMake’s raison d’etre is cross-language, cross-toolchain, cross-IDE generation. scpp’s project build system is narrower: it is primarily about building scpp packages whose semantics already revolve around .scppm, .scppa, module imports, and package metadata.

2.3 Bazel / bzlmod: strict dependency visibility and lockable external resolution

Verified from Bazel’s official external-dependencies docs:

Bazel lessons for scpp

The most valuable Bazel idea for scpp is strict direct-dependency visibility:

That rule gives scpp more predictable project-scale behavior and matches both Bazel and Cargo instincts.

2.4 Go modules: decentralized identity and simple package-root manifests

Verified from the official Go module docs:

Go lessons for scpp

The useful lesson is not Go’s exact file format. It is the identity model:

That fits scpp well because scpp already distinguishes package metadata (.scppkg) from module names (mylib.math, org.example.net, etc.).

2.5 Zig build system: valuable, but the wrong v1 shape for scpp

Verified from Zig’s official docs:

Zig lessons for scpp

The attractive part is the DAG/caching mindset.

The unattractive part for scpp v1 is “build system as user code in the language itself”:

So: take Zig’s graph/caching ambitions, but not Zig’s “build configuration is a program” model.

2.6 Meson: good declarative ergonomics for native dependencies

Verified from Meson’s dependency docs:

Meson lessons for scpp

Meson’s best lesson is for native / foreign dependency declarations:

3. Design principles for scpp

From the research and scpp’s current architecture, I think the build system should follow these rules.

  1. Package-oriented, not file-oriented. Single-file scpp foo.scpp remains valid, but large-project build must think in packages/workspaces/profiles.

  2. Manifest-based project mode; manifestless non-project mode. Zero-config single-file use stays valid, but package/workspace builds are manifest-based.

  3. Integrated public UX, not generator-first UX. The user should run scpp build, not scpp gen && ninja.

  4. Artifacts are the build graph boundary. .scppm is the compile-time dependency boundary; .scppa is the link-time artifact boundary; .scppkg is the distribution boundary.

  5. Direct dependencies are the compile-time contract. Transitive packages may be linked transitively, but imported modules should come only from the current package or direct dependencies.

  6. No programmable build scripts in v1. Declarative manifests only.

  7. Do not invent features/conditional compilation in the build system before the language has a coherent story for it.

4.1 Public model

Introduce an optional manifest named scpp.toml.

This keeps project-scale behavior explicit without disturbing scpp’s existing direct-compiler feel for non-project use.

4.2 Execution model

Expose a Cargo-like integrated command set:

with bare scpp as a convenience alias for “build the default thing in this directory”.

4.3 Internal engine choice

Decision: use an integrated planner/executor in v1, not a mandatory Ninja/CMake-style generator.

Why:

V1 does not need a backend-abstraction layer or a generated-backend escape hatch. If that need becomes real later, it can be added later as its own command (scpp gen --backend ...) instead of shaping the v1 architecture around a capability the user does not want to require yet.

5. Project/package model

5.1 Package vs module

The build system should make the same distinction the existing .scppkg format already makes:

A single package may provide:

This matches Cargo/Go package thinking and also matches scpp’s existing package format.

5.2 Manifestless non-project mode

Absence of scpp.toml should be understood as non-project mode, not as an alternate fully-fledged project model.

Today’s non-project mode is the existing single-file CLI behavior:

The paused main.scpp / lib.scpp branch remains useful context as the closest thing scpp has explored for a directory-scale manifestless convenience, but this design does not broaden that into the main v1 project story, and specifically does not add src/main.scpp / src/lib.scpp recognition. If the main.scpp / lib.scpp convenience is retained at all, it should be documented as a narrow non-project convenience rather than as general project mode.34

Whether scpp later grows a stricter multi-file manifestless mode is explicitly deferred; for now, package/workspace builds are manifest-based.

5.3 Manifest-based mode

Recommended manifest file: scpp.toml

Example:

manifest-version = 1

[package]
name = "httpserver"
version = "0.1.0"          # required for packaging, recommended otherwise

[[lib]]
name = "httpserver"
sources = ["src/**/*.scpp"]

[[bin]]
name = "httpserver"
sources = ["src/**/*.scpp"]

[dependencies]
net = { path = "../net" }
json = { scppkg = "vendor/json.scppkg" }

[profile.dev]
opt-level = 0
debug = true
static = false

[profile.release]
opt-level = 3
debug = false
static = true

[native]
links = ["pthread"]
search = ["native/lib"]

Why TOML?

Because Cargo has already demonstrated that TOML is readable enough for hand-editing, structured enough for tools, and far less ceremony-heavy than XML or a new DSL.35

Why not CMakeLists.txt-style DSL?

Because scpp does not need a second language for project metadata. Declarative tables cover the real v1 needs better.

5.4 Manifest fields

Recommended initial fields:

Recommended v1 restriction: at most one library target per package.

If a repository needs many independently reusable libraries, it should use a workspace with many packages instead of many library targets inside one package. That keeps packaging and dependency resolution much simpler.

6. Workspace model

Adopt a Cargo-like workspace root.

Example root manifest:

manifest-version = 1

[workspace]
members = [
  "libs/net",
  "libs/json",
  "apps/httpserver",
]
default-members = ["apps/httpserver"]

[workspace.dependencies]
net = { path = "libs/net" }
json = { path = "libs/json" }

[profile.dev]
opt-level = 0
debug = true

[profile.release]
opt-level = 3
debug = false

Member manifest:

manifest-version = 1

[package]
name = "httpserver"
version = "0.1.0"

[[bin]]
name = "httpserver"
sources = ["src/**/*.scpp"]

[dependencies]
net = { workspace = true }
json = { workspace = true }

Workspace rules

Recommended rules:

Why workspaces instead of CMake add_subdirectory()?

Because workspaces model packages with identity, not merely directories with imperative script inclusion. That is a better fit for scpp’s future .scppkg ecosystem.

7. Dependency model

7.1 V1 dependency kinds

Recommended v1 dependency sources:

  1. path = "../foo"
    • a local package directory containing scpp.toml
  2. scppkg = "vendor/foo.scppkg"
    • a packaged dependency file

Reserve, but do not implement yet:

This avoids painting the design into a corner while keeping v1 aligned with what actually exists today.

7.2 Compile-time visibility rule

Recommendation: only direct dependencies are visible for module resolution.

If package app depends on package net, and net depends on tls, then:

Why:

Compile-time visibility is direct-only, but final linking should still use the transitive dependency closure, consistent with the existing scpp language spec’s linking model.39

That means:

This is exactly the place where CMake’s transitive usage-requirements idea is valuable.40

7.4 Future lockfile

I recommend defining the file name now: scpp.lock.

However, I do not recommend making it the central v1 feature.

Rationale:

So the recommended path is:

User review confirmed this exact direction: scpp.lock should be reserved now, but not made central for path-only v1 builds.

8. Source discovery and target graph construction

8.1 Manifest-declared source sets

Each target ([[lib]], [[bin]]) should declare:

Each [additional_objs.<name>] block declares:

The build tool then parses the listed source files to discover:

This avoids forcing users to manually duplicate the module graph in the manifest.

8.2 Grouping rule

Within one target’s source set:

This is simply the project-build generalization of the existing chapter-11 rules.42

8.3 Cross-package module resolution

During planning, the build tool constructs a table:

Resolution order:

  1. current target/package modules
  2. direct dependency packages’ exported modules
  3. stdlib built-ins / shipped package roots
  4. explicit overrides if the CLI later adds them for project mode

Ambiguity should be a hard error:

That matches scpp’s current language preference for avoiding hidden lookup magic.

9. Artifact model and .scppm / .scppa / .scppkg integration

9.1 Key observation

scpp’s existing artifact split is already the right incremental boundary:

This is a major advantage over header-style systems.

9.2 Local build outputs

Recommended workspace-local output root:

.scpp/
  build/
    <triple>/
      <profile>/
        <package>/
          modules/
            foo.scppm
            bar.scppm
          archives/
            libfoo.scppa
            libbar.scppa
          objects/
          package-metadata.json
  cache/
    build.db

Notes:

9.3 Package build outputs

A library package build should produce:

A binary package build should produce:

9.4 Packaging command

Introduce:

scpp package

Behavior:

Suggested default output location:

dist/<package-name>-<version>-<target-triple>.scppkg

The .scppkg spec already supports multiple target triples per module; v1 packaging may start with one triple per invocation and extend later.

The .scppkg spec already records native_link_requirements per module.44

For v1, target-level/package-level native requirements can be lowered into that schema conservatively:

That is slightly coarse, but correct, and can be refined later if per-module declarations become necessary.

10. Profiles and configuration

10.1 Built-in profiles

Adopt built-in profiles:

with optional custom profiles, mirroring Cargo.45

Recommended defaults:

dev

release

10.2 CLI/profile interaction

Recommended commands:

Existing one-off flags should map cleanly:

For project mode, these should become profile properties first, CLI overrides second.

10.3 Why no Cargo-style features in v1?

Because Cargo features are tightly tied to Rust conditional compilation and optional dependency activation.46

scpp explicitly has no preprocessor, and the language has no settled conditional-compilation feature system today.47

So adding a build-system feature matrix now would create pressure for hidden language semantics that do not exist yet.

11. Native / foreign dependency story

11.1 V1 recommendation

V1 should support a declarative manual native dependency model in the manifest.

Recommended fields:

[native]
links = ["m", "pthread"]
search = ["native/lib", "/opt/foo/lib"]

Later-expandable fields:

frameworks = ["Security"]
discover = ["openssl"]

11.2 Why manual first?

Because it is enough to replace today’s raw --link path world with something project-scale and version-controlled, without immediately forcing scpp to become a cross-platform system package discovery framework.

Meson’s example is still useful as evidence that declarative dependency descriptions are better than raw flag soup.48 But user review clarified an important scpp-specific constraint: the later evolution should be a scpp-native discovery mechanism, not a long-term plan to depend on pkg-config, CMake package discovery, or other third-party discovery tools.

11.3 Propagation rule

Native link requirements should propagate transitively like CMake PUBLIC usage requirements:49

12. CLI surface

12.1 Keep existing low-level commands

Retain:

build-module remains the expert / plumbing command for one module’s artifacts.

12.2 Add project-scale commands

Recommended new commands:

scpp build [--workspace] [-p <package>] [--bin <name>] [--lib] [--profile <name>] [--release]
scpp run   [--workspace] [-p <package>] [--bin <name>] [--profile <name>] [--release] [-- <program args>]
scpp test  [--workspace] [-p <package>] [--profile <name>] [--release] [-- <test args>]
scpp package [-p <package>] [--profile <name>] [--release]
scpp clean

Package-selection flags intentionally mirror Cargo because they are proven and unsurprising in multi-package repositories.50

scpp test should be named now as a first-class project command, but its actual mechanics are not fully designable yet because scpp does not currently have a language-level test function/attribute/discovery model analogous to Rust’s #[test] plus cargo test. In other words, the command name should exist in the build-system roadmap now, while its eventual execution model likely depends on a later language/tooling design for project-defined test targets (for example, manifest-declared test targets and/or a conventional tests/ layout once the language has a coherent way to mark runnable tests).

Separately, the future escape hatch for generating build files for another backend should be surfaced as an explicit subcommand:

scpp gen --backend ninja

rather than only as a flag hanging off scpp build. User review also clarified that scpp gen is not a v1 requirement; it is a later addition only.

12.3 Bare scpp

Recommended behavior:

This preserves the session’s zero-ceremony direction without making project mode ambiguous.

There is also a forward-looking reason to make that choice explicit now: once scpp eventually gains real registry / git dependency resolution, bare scpp can naturally be understood as scpp pull + scpp build – first resolve/fetch external dependencies, then build. Because no such pull/registry mechanism exists today, bare scpp should simply mean scpp build for now. That keeps today’s UX simple while leaving a clean conceptual path for future external-dependency workflows.

This rule applies specifically when a manifest-based project/workspace has been detected. Absence of a manifest remains non-project mode as described in §5.2.

13. Incremental and parallel build strategy

13.1 Why scpp’s artifact split makes this tractable

The build system can use two distinct invalidation boundaries:

That means:

This is one of the design’s strongest benefits and should be exploited explicitly.

13.2 Build database

Recommended internal state:

13.3 Scheduler

Recommended v1 scheduler behavior:

This is effectively Cargo’s integrated execution model, but with scpp-specific module/interface boundaries.

13.4 Why not mandatory Ninja generation in v1?

Because the difficult part for scpp is the graph semantics, not the shelling-out mechanics.

Once scpp already knows:

then executing the graph directly is a reasonable first implementation.

A later scpp gen --backend ninja style escape hatch can be added if it proves useful for IDEs, debugging, or external build-tool integration, but v1 should not be burdened with a backend-abstraction requirement ahead of that real need.

14. Alternatives considered

14.1 Pure filesystem convention, no manifest ever

Rejected as the main solution.

Why:

Keep it only as the non-project fallback.

14.2 Mandatory manifest for every project, even tiny ones

Rejected.

Why:

14.3 CMake-style generator as the primary model

Rejected for v1 public UX.

Why:

14.4 Build-system-as-scpp-code (build.scpp, Zig-style)

Rejected for v1.

Why:

Phase A — land the project model skeleton

  1. add project/workspace discovery (scpp.toml search upward);
  2. make manifest presence the boundary for project mode vs non-project mode;
  3. add scpp build as the explicit project-scale command;
  4. make bare scpp behave as scpp build in project/workspace mode.

Phase B — manifest parser + single-package builds

  1. parse scpp.toml
  2. support [package], [[lib]], [[bin]], [profile.*]
  3. build one manifest-based package with no external dependencies
  4. write local outputs under .scpp/build/...

Phase C — path dependencies + workspaces

  1. add [dependencies] path = ...
  2. add [workspace], members, default-members
  3. add -p/--package, --workspace
  4. share build output/cache at workspace root

Phase D — incremental graph + native metadata

  1. add build database
  2. distinguish compile vs link invalidation using .scppm vs .scppa
  3. add declarative [native] propagation
  4. parallelize ready nodes

Phase E — packaging / .scppkg

  1. add scpp package
  2. consume scppkg = ... dependencies
  3. map local package metadata to .scppkg MANIFEST.json
  4. reserve scpp.lock semantics for future non-path dependency resolution

Phase F — optional later enhancements

16. Final settled design decisions

User review has now resolved the remaining forks. The final design decisions are:

  1. Use scpp.toml as the boundary for package/workspace project mode.
  2. Treat absence of a manifest as non-project mode; today that means single-file use, with any retained main.scpp / lib.scpp convenience staying narrow and manifestless.
  3. Make the public build UX integrated and Cargo-like (scpp build, scpp run, scpp package, later scpp test), not generator-first.
  4. Make bare scpp mean scpp build when a manifest-based project/workspace is detected; later, once external resolution exists, that can grow conceptually into scpp pull + scpp build.
  5. Use packages/workspaces as the dependency model; use modules as the compilation artifact model.
  6. Allow only direct dependencies for compile-time module visibility.
  7. Propagate .scppa and native link requirements transitively for final linking.
  8. Do not add programmable build scripts in v1.
  9. Do not add Cargo-style features / conditional dependency activation in v1.
  10. Reserve scpp.lock now, but do not make it central in path-only v1 builds.
  11. Keep native dependencies manual in v1, and plan for a future scpp-native discovery mechanism rather than pkg-config / CMake-based discovery providers.
  12. Do not require scpp gen or backend abstraction in v1; add generated-backend support later only if real need emerges.
  13. Require package version for scpp package, but keep it optional for local-only builds.
  14. Exploit .scppm vs .scppa as separate incremental invalidation boundaries.

Appendix A — empirical findings worth preserving

Cargo

Local check performed in this workspace:

$ cargo build --workspace
   Compiling lib v0.1.0 (.../lib)
   Compiling app v0.1.0 (.../app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s

Observed results:

CMake

Local check performed in this workspace:

$ cmake -S . -B build
-- Configuring done
-- Generating done
-- Build files have been written to: .../build

$ cmake --build build
[100%] Built target app

Observed result:

scpp reference repo

The reference repo’s own stdlib CMake currently drives scpp build-module to produce:

from the stdlib’s module sources, which is exactly the kind of artifact-oriented project build this design generalizes.51

Sources


  1. scpp-reference/docs/book/en/ch11-modules-and-libraries.md, especially module/package/artifact sections and §11.12-§11.15.↩︎

  2. scpp-reference/docs/spec/en/scppm-format.md.↩︎

  3. scpp-reference/docs/book/en/ch11-modules-and-libraries.md, especially module/package/artifact sections and §11.12-§11.15.↩︎

  4. scpp-reference/docs/spec/en/scppkg-format.md.↩︎

  5. scpp-reference/docs/book/en/ch11-modules-and-libraries.md §11.15.↩︎

  6. scpp-reference/src/cli.cppm:603-691.↩︎

  7. scpp-reference/src/cli.cppm:603-691.↩︎

  8. scpp-reference remote branch origin/dev-agent/project-build-mode, especially src/cli.cppm:669-742 and related tests/docs discovered via git grep.↩︎

  9. Cargo Book, “The Manifest Format” — https://doc.rust-lang.org/cargo/reference/manifest.html↩︎

  10. Cargo Book, “Specifying Dependencies” — https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html↩︎

  11. Cargo Book, “Workspaces” — https://doc.rust-lang.org/cargo/reference/workspaces.html↩︎

  12. Cargo Book, “Profiles” — https://doc.rust-lang.org/cargo/reference/profiles.html↩︎

  13. Cargo Book, “Cargo.toml vs Cargo.lock” — https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html↩︎

  14. Cargo Book, “Build Scripts” — https://doc.rust-lang.org/cargo/reference/build-scripts.html↩︎

  15. Cargo Book, “Features” — https://doc.rust-lang.org/cargo/reference/features.html↩︎

  16. local command run in ~/scpp-agents/build-system-designer-agent/scratch-research during this session.↩︎

  17. scpp-reference/docs/book/en/ch11-modules-and-libraries.md, especially module/package/artifact sections and §11.12-§11.15.↩︎

  18. CMake manual cmake(1) — https://cmake.org/cmake/help/latest/manual/cmake.1.html↩︎

  19. CMake add_subdirectory() — https://cmake.org/cmake/help/latest/command/add_subdirectory.html↩︎

  20. CMake target_link_libraries() and cmake-buildsystem(7) — https://cmake.org/cmake/help/latest/command/target_link_libraries.html and https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html↩︎

  21. CMake find_package() — https://cmake.org/cmake/help/latest/command/find_package.html↩︎

  22. CMake cmake-presets(7) — https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html↩︎

  23. local command run in ~/scpp-agents/build-system-designer-agent/scratch-cmake during this session.↩︎

  24. Bazel external dependency overview — https://bazel.build/external/overview↩︎

  25. Bazel external dependency overview — https://bazel.build/external/overview↩︎

  26. Go Modules Reference — https://go.dev/ref/mod↩︎

  27. Go, “Managing dependencies” — https://go.dev/doc/modules/managing-dependencies↩︎

  28. Go, “Managing dependencies” — https://go.dev/doc/modules/managing-dependencies↩︎

  29. Zig, “Zig Build System” — https://ziglang.org/learn/build-system/↩︎

  30. Zig, “Zig Build System” — https://ziglang.org/learn/build-system/↩︎

  31. Meson, “Dependencies” — https://mesonbuild.com/Dependencies.html↩︎

  32. Meson, “Dependencies” — https://mesonbuild.com/Dependencies.html↩︎

  33. scpp-reference/src/cli.cppm:603-691.↩︎

  34. scpp-reference remote branch origin/dev-agent/project-build-mode, especially src/cli.cppm:669-742 and related tests/docs discovered via git grep.↩︎

  35. Cargo Book, “The Manifest Format” — https://doc.rust-lang.org/cargo/reference/manifest.html↩︎

  36. Cargo Book, “Workspaces” — https://doc.rust-lang.org/cargo/reference/workspaces.html↩︎

  37. Cargo Book, “Workspaces” — https://doc.rust-lang.org/cargo/reference/workspaces.html↩︎

  38. Cargo Book, “Profiles” — https://doc.rust-lang.org/cargo/reference/profiles.html↩︎

  39. scpp-reference/docs/book/en/ch11-modules-and-libraries.md:464-522.↩︎

  40. CMake target_link_libraries() and cmake-buildsystem(7) — https://cmake.org/cmake/help/latest/command/target_link_libraries.html and https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html↩︎

  41. Cargo Book, “Cargo.toml vs Cargo.lock” — https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html↩︎

  42. scpp-reference/docs/book/en/ch11-modules-and-libraries.md, especially module/package/artifact sections and §11.12-§11.15.↩︎

  43. scpp-reference/docs/spec/en/scppkg-format.md.↩︎

  44. scpp-reference/docs/spec/en/scppkg-format.md.↩︎

  45. Cargo Book, “Profiles” — https://doc.rust-lang.org/cargo/reference/profiles.html↩︎

  46. Cargo Book, “Features” — https://doc.rust-lang.org/cargo/reference/features.html↩︎

  47. scpp-reference/docs/book/en/ch11-modules-and-libraries.md, especially module/package/artifact sections and §11.12-§11.15.↩︎

  48. Meson, “Dependencies” — https://mesonbuild.com/Dependencies.html↩︎

  49. CMake target_link_libraries() and cmake-buildsystem(7) — https://cmake.org/cmake/help/latest/command/target_link_libraries.html and https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html↩︎

  50. Cargo Book, “Workspaces” — https://doc.rust-lang.org/cargo/reference/workspaces.html↩︎

  51. scpp-reference/stdlib/CMakeLists.txt:14-35.↩︎