Origami is a pragmatic build system for C++ projects that uses Ice files to describe identity, actions, dependencies, artifacts, and plugins explicitly. The central idea is to keep build intent inside the project while the CLI only selects an action and injects declared parameters.
Project identifies. Action executes. Param configures. Dependency requires. Origin locates directly. Repository searches. Artifact delivers. Plugin adapts. Cache materializes. CLI triggers.
Mental model
Before looking at commands, it helps to separate Origami concepts by responsibility. Each concept has its own semantics and participates in a different part of the build lifecycle.
| Concept | Question it answers | Responsibility |
|---|---|---|
Project |
Which package is this? |
Defines identity through |
Action |
What can be executed? |
Exposes named entry points such as |
Param |
What can change at runtime? |
Declares configurable values and their defaults. |
Dependency |
What does this project require? |
Describes external requirements and consumed artifacts. |
Origin |
Where does this dependency come from? |
Points directly to |
Repository |
Where should Origami search? |
Lists resolution locations when a dependency has no direct origin. |
Artifact |
What does this project deliver? |
Publishes binaries, headers, and consumable parameters for other projects. |
Plugin |
How is intent translated into tools? |
Adapts the Origami model to compilers, filesystems, and operating systems. |
Cache |
What has already been resolved or produced? |
Materializes sources, binaries, and headers to avoid repeated work. |
CLI |
How does the user trigger the system? |
Selects commands, actions, and declared parameters. |
Getting started
Origami expects the Ice parser suite as a sibling repository, because the build compiles Ice and links it into the executable:
cmake -S . -B build
cmake --build build
To use another Ice checkout, configure with -DICE_SOURCE_DIR=/path/to/ice.
Then run an action from the project root:
./build/origami build
Configuration file
The origami.ice file is the project’s contract with Origami. It should not be
just a list of shell commands. It describes project identity, available actions,
and how dependencies and artifacts connect.
@project {
group: "org.example"
name: "calculator"
version: "1.0"
}
@actions {
@build {
@params {
mode: "debug"
}
}
}
Main sections
@project-
Identifies the project. This identity also participates in the global cache layout and coordinate-based resolution.
@actions-
Declares executable entry points. An action can prepare values, call plugins, produce files, and publish artifacts.
@params-
Declares the configurable surface of an action. Defaults live in the build file; the CLI can only override declared parameters.
@dependencies-
Lists external requirements. Each dependency can request a specific artifact and may or may not declare a direct
origin. @repositories-
Configures where Origami searches for dependencies that do not declare their own origin.
@artifacts-
Describes what this project can deliver to consumers: a static library, dynamic library, headers, executable, or another consumable result.
Project
Project is Origami’s identity unit. It should be stable enough to name a package in the cache and in future resolution flows.
@project {
group: "org.example"
name: "mathLib"
version: "1.0"
}
Project semantics
-
grouporganizes the project namespace. -
nameidentifies the package inside the group. -
versionseparates releases or published states. -
The
group/name/versiontuple forms the base path in the artifact cache.
Actions
Actions are named executable contracts. The user calls an action from the CLI,
but the action logic lives in origami.ice.
origami build
Inside an action, statements are evaluated in order. Bindings create intermediate values, function calls perform work, and params define what the CLI is allowed to override.
@actions {
@build {
compiler: "g++"
sources: io.list("src/.*[.]cpp")
ori.genExe(
"app",
${sources},
${compiler},
{ compile: ["-std=c++20"] }
)
}
}
Pipelines
The pipe operator |> chains operations. Each stage receives the result of the
previous stage as its first argument, followed by its explicit arguments, and
each stage returns a typed Ice value that feeds the next operation. This lets
source selection feed the compiler without intermediate bindings:
"src"
|> io.list(".*[.]cpp")
|> ori.genExe("hello", "g++")
A named binding is equivalent to naming an intermediate stage; pipelines only remove the ceremony.
Action semantics
- Bindings
-
Local names used to organize the action and avoid repetition.
- Function calls
-
Operations executed by plugins, such as
io.listorori.genExe. - Evaluation order
-
File order matters: a value must exist before it can be used.
- Action boundary
-
Each action should express one coherent task: build, test, package, or publish.
Params
Params are the public runtime configuration interface. They make explicit what can vary without editing the build file.
@actions {
@build {
@params {
mode: "debug"
output: "app"
}
}
}
origami build --params mode:release output:calculator
Rules
-
Every param must have a default value.
-
The CLI can only override declared params.
-
Params belong to an action, not to the global CLI.
-
Params document and constrain runtime configuration.
Dependencies
A dependency is a requirement. It describes something the consuming project needs to compile, link, or execute an action.
@dependencies {
mathLib: {
group: "org.example"
name: "mathLib"
version: "1.0"
artifact: "staticLib"
}
}
Dependency forms
The object form declares each field explicitly. Dependencies produced by
another Origami project can also use the compact array form
[group, name, version, artifact]:
@dependencies {
@default {
glib: ["glib", "glib", 2.0, "sharedLib"]
}
}
The @default profile is mandatory when @dependencies exists. Host sections
@win, @mac, and @linux replace default entries that use the same alias:
@dependencies {
@default {
testeLib: ["orgcorp", "testeLib", "2.0", "staticLib"]
}
@win {
testeLib: ["orgcorp", "testeLibWin", "2.0", "sharedLib"]
}
}
Dependency semantics
- Identity
-
group,name, andversionsay which project or package must be resolved. - Artifact selection
-
artifactsays which output from the dependency project the consumer wants. - Transitivity
-
A resolved dependency may declare dependencies of its own.
- Resolution
-
Origami turns a requirement into a dependency catalog available to the action.
Consuming dependencies
Resolved dependencies are exposed to actions through ${dependencies.<alias>}.
System dependencies export compile and link flag arrays, and GCC profiles
can use those arrays directly. A full dependency object may also be placed in
link:
flags: {
release: {
compile: ["-std=c++20", ${dependencies.openssl.compile}]
link: [${dependencies.openssl}]
}
}
Project dependencies expose the selected artifact, including its published
path. GCC link arrays accept dependency objects directly:
link: [${dependencies.mathLib}]
Origami builds each selected dependency artifact before executing the consuming
action. Dependency build state is recorded under
.origami/state/dependencies, so a later build skips the artifact action when
the artifact exists and the dependency signature is unchanged.
Version rules
Version strings preserve dotted components and are recommended. A numeric whole
version such as 2.0 is accepted and matches "2.0", but fractional numeric
values are rejected because Ice cannot distinguish 2.10 from 2.1 after
numeric parsing.
Origin
Origin is the direct source of a dependency. When a dependency declares
origin, it does not rely on repository lookup to discover where it comes from.
Git
Use a Git origin when the dependency lives in a Git repository. A Git origin
must declare exactly one of branch or ref. Branch origins track a remote
branch and refresh the cached clone before resolution. Ref origins are checked
out detached and are intended for tags, commits, or other Git refs:
origin: {
type: "git"
url: "https://example.com/math.git"
branch: "main"
}
origin: {
type: "git"
url: "https://example.com/math.git"
ref: "v2.0.0"
}
Archive
Use an archive origin to download or read a compressed file. Archives accept
url for a remote download or path for a local archive file, and the content
is verified with sha256 before extraction. format is optional and defaults
to tar.gz; declare format: "zip" for zip archives. stripComponents
defaults to 1, which matches archives that contain one top-level project
directory, including common GitHub tag archives:
origin: {
type: "archive"
url: "https://example.com/math-1.0.tar.gz"
format: "tar.gz"
sha256: "0123456789abcdef0123456789abcdef"
stripComponents: 1
}
Local archive paths are resolved relative to the consuming project’s
origami.ice, and the SHA-256 is verified again before Origami reuses the
extracted cache.
Path
Use a path origin when the dependency points directly to a project already open on the filesystem.
origin: {
type: "path"
path: "../math-lib"
}
System
Use a system origin for libraries already installed on the machine. Origami does
not build those libraries; it only exposes flags to the consumer. Shared search
paths can be declared once in @repositories.system and @repositories.headers
for every system dependency, or declared directly on one dependency when they
should not be shared:
origin: {
type: "system"
libs: ["ssl", "crypto"]
libraryDirs: ["/opt/openssl/lib"]
includeDirs: ["/opt/openssl/include"]
compile: ["-DOPENSSL_API_COMPAT=30000"]
link: ["-pthread"]
}
Repositories
Repositories are search locations. They answer where Origami should look for a
dependency when the dependency itself does not declare an origin.
@repositories {
local: ["../math-lib"]
system: ["/usr/local/lib"]
headers: ["/usr/local/include"]
}
local lists direct project directories containing origami.ice; their
identity comes exclusively from that project’s @project section. system and
headers declare shared search paths used by system dependencies. Paths are
resolved relative to the root project when they are not absolute.
Origin vs repository
origin-
Belongs to a dependency and locates that dependency directly.
repository-
Belongs to the consuming project or environment and participates in coordinate-based lookup.
Artifacts
An artifact is a consumable output from a project. It is the contract that lets another project use the result without knowing the internal details of the action that produced it.
@artifacts {
staticLib: {
action: "build"
path: "libmath.a"
headers: "include"
params: { linkage: "static" }
}
}
Artifact semantics
- Action producer
-
Defines which action must run to produce the artifact.
- Path
-
Points to the file produced by the action.
- Headers
-
Declares which headers are exported to consumers.
- Params
-
Allows the artifact to inject configuration into its producing action.
- Linkage
-
Distinguishes variants such as static and dynamic libraries.
Cache
The cache materializes what has already been resolved or produced. It reduces
repeated work and gives dependencies a predictable layout. Origami keeps
user-wide storage under ~/.origami by default: the directory contains
config.ice for global configuration and artifacts/ for cached dependencies.
Tests and controlled environments may override the location with the
ORIGAMI_HOME environment variable.
~/.origami/
config.ice
artifacts/
org/
example/
mathLib/
1.0/
source/
bin/
headers/
Directories
source-
The resolved project, downloaded archive, cloned repository, or direct origin source.
bin-
Binary artifacts published for consumers.
headers-
Headers exported by the dependency project.
Plugins
Plugins adapt Origami’s semantic model to concrete tools. The core does not need to know every detail of each compiler, filesystem, or toolchain.
Plugin ori
Exposes high-level build operations such as generating executables and libraries.
ori.genExe("app", ${sources}, "g++", ${flags})
ori.genLib("math", ${sources}, "g++", ${flags})
Plugin io
The IO plugin is mandatory and loaded automatically; it does not need an entry
in @plugins. It exposes the filesystem operations used by builds:
| Operation | Responsibility |
|---|---|
|
Creates an empty file. |
|
Returns the file content as a string. |
|
Writes content to a file. |
|
Returns whether any entry exists. |
|
Returns whether a regular file exists. |
|
Recursively selects files under the project directory. |
|
Selects files under an explicit root; the ECMAScript regex matches each path relative to the selection root. |
|
Creates a directory, including missing parent directories. |
GCC
The GCC plugin is not called directly from a build file. It is reached through
the high-level ori build functions: ori.genExe for executables and
ori.genLib for libraries. Those functions receive the compiler selection and
the build profile, then delegate the native compilation and linking work to the
GCC implementation.
This keeps the action semantic and portable: the action says "generate an executable" or "generate a library", while the compiler plugin decides how that intent becomes concrete compile and link commands.
ori.genExe("app", ${sources}, "g++", ${gccProfile})
ori.genLib("math", ${sources}, "g++", ${gccProfile})
The GCC profile accepts raw compile and link arrays for arbitrary
compiler-driver arguments. It also accepts structured fields that Origami
expands before invoking the compiler: defines becomes -D…,
includeDirs becomes -I…, and libraryDirs becomes -L…. Values in
structured fields omit the compiler prefix:
{
compile: ["-std=c++20", "-O2"],
defines: ["NDEBUG", "VERSION=1"],
includeDirs: ["include", "vendor/include"],
libraryDirs: ["lib", "/usr/local/lib"],
link: ["-lm"]
}
While genExe links through the compiler driver, genLib creates a static
archive by default and creates a shared library when the selected profile
declares linkage: "shared".
Libraries and executables
ori.genLib and ori.genExe return the generated path as a string. A library
can therefore be generated first and included directly in a later executable
profile. This call order is the declarative dependency: Origami does not
construct a separate target graph.
coreSrc: io.list("src/core/.*[.]cpp")
core: ori.genLib("libcore.a", ${coreSrc}, ${compiler}, ${flags[params.mode]})
appSrc: io.list("src/app/.*[.]cpp")
ori.genExe(
${params.output},
${appSrc},
${compiler},
{ link: [${core}] }
)
Explicit library and object paths in link are tracked, so a changed library
relinks its consumers without recompiling their sources.
Incremental builds
ori.genExe and ori.genLib perform incremental GCC-style builds:
-
Each source is compiled independently into
.origami/obj/<compile-signature>/. -
-MMDdependency files track included headers, so editing a header rebuilds only the sources that include it. -
Compile flag changes select a new object cache.
-
Final operation signatures are stored under
.origami/state/. -
A current target executes no compiler, linker, or archiver process.
CLI
The CLI is Origami’s human-facing surface. It should trigger the build, not replace the configuration file.
origami build
origami build --params mode:release output:app
origami dependency-tree
origami clean-cache --all
Commands
build-
Runs the build action in the current project.
--params-
Overrides parameters declared inside the action.
dependency-tree-
Prints the resolved dependency tree, including origins and artifacts.
clean-cache-
Removes Origami cache directories without loading
origami.ice. With no option it removes both caches.--localremoves the current project’s.origamidirectory,--globalremoves the artifact cache insideORIGAMI_HOME(or~/.origami) while preserving globalconfig.ice, and--allremoves both.
Semantic roadmap
The next features should strengthen reproducibility, resolution explainability, and the artifact ecosystem.
Dependencies
-
A dedicated remote registry for coordinate-based dependency resolution.
-
A lockfile to freeze versions, origins, and hashes.
-
Semantic version ranges.
-
Conflict handling for transitive dependencies.
Artifacts
-
Export include directories, compile flags, and link flags.
-
Model runtime paths for dynamic libraries.
-
Publish artifacts to repositories.
-
Automatic transitive linking where it makes sense.
Toolchains
-
Support more compilers.
-
Add explicit plugin configuration with
@plugins. -
Support concurrent builds.
-
Improve Windows support.