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 group, name, and version.

Action

What can be executed?

Exposes named entry points such as build, test, or package.

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 git, archive, path, or system.

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

  • group organizes the project namespace.

  • name identifies the package inside the group.

  • version separates releases or published states.

  • The group/name/version tuple 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.list or ori.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, and version say which project or package must be resolved.

Artifact selection

artifact says 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

io.createFile(path)

Creates an empty file.

io.readFile(path)

Returns the file content as a string.

io.writeFile(path, content)

Writes content to a file.

io.exists(path)

Returns whether any entry exists.

io.fileExists(path)

Returns whether a regular file exists.

io.list(regexpFilter)

Recursively selects files under the project directory.

io.list(path, regexpFilter)

Selects files under an explicit root; the ECMAScript regex matches each path relative to the selection root.

io.createDir(path)

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>/.

  • -MMD dependency 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. --local removes the current project’s .origami directory, --global removes the artifact cache inside ORIGAMI_HOME (or ~/.origami) while preserving global config.ice, and --all removes 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.