vix.app Manifest Design
vix.app is designed around one idea:
a C++ project should be describable before it becomes a build-system programThe manifest is intentionally small.
It does not try to expose every CMake feature.
It describes the common structure of a C++ target in a way that Vix can parse, validate, generate, and later optimize.
The design problem
A C++ project usually needs a build description.
Even for a small executable, the build system needs to know:
target name
target type
C++ standard
source files
include directories
definitions
libraries
compiler flags
resourcesCMake can describe all of that.
But CMake also has a much larger surface area:
functions
macros
custom commands
generator expressions
install rules
package exports
toolchain logic
platform branches
dependency fetching
multi-target graphsThat power is useful.
But it is not always necessary for a simple app.
vix.app separates the common project description from the full build-system language.
One manifest, one target
The central rule is:
one vix.app = one targetA target can be:
executable
static library
shared libraryThis avoids turning vix.app into a complex multi-target language.
A simple executable:
name = hello
type = executable
standard = c++20
sources = [
src/main.cpp,
]A simple static library:
name = mathlib
type = static
standard = c++20
sources = [
src/add.cpp,
src/mul.cpp,
]
include_dirs = [
include,
]This model is predictable.
The parser reads one target.
The generator emits one target.
The build planner knows the default target name.
vix run knows what executable to search for.
Why not multi-target immediately
A multi-target manifest could look attractive:
targets = [
app,
tests,
examples,
libs
]But that creates many questions:
How are targets linked together?
How are public and private includes represented?
How are per-target packages handled?
How are tests discovered?
How are examples grouped?
How are install rules expressed?
How is ordering represented?At that point, the manifest starts becoming a second CMake.
That is not the goal.
The V1 design keeps the boundary clear:
simple target -> vix.app
complex graph -> CMakeLists.txtFor tests and examples, the recommended model is:
project/
vix.app
tests/
vix.app
examples/
basic/
vix.appEach target stays explicit.
The manifest shape
A vix.app file is a key-value manifest.
The simplest form is:
key = valueArrays can be written inline:
sources = [src/main.cpp, src/app.cpp]or across multiple lines:
sources = [
src/main.cpp,
src/app.cpp,
]Comments use #:
# Application target
name = helloInline comments are also supported:
type = executable # executable targetValues can be quoted when needed:
sources = [
"src/with space.cpp",
]The format is intentionally boring.
That is a feature.
A build manifest should be easy to read in a terminal, easy to review in a diff, and easy to generate from tools.
Required fields
A useful target needs at least two things:
name
sourcesExample:
name = hello
sources = [
src/main.cpp,
]type and standard have defaults.
The default target type is:
type = executableThe default C++ standard is:
standard = c++20For clarity, most examples still write them explicitly:
name = hello
type = executable
standard = c++20
sources = [
src/main.cpp,
]Explicit manifests are easier to understand.
Target name
The name field defines the target name.
name = helloThis name is used for several things:
generated target name
default build target
executable lookup in vix run
artifact identity
diagnosticsThis is why the name should be stable and simple.
Recommended:
name = myappname = mathlibname = softadastra_serverAvoid:
name = "my app"A target name is not a display title.
It is part of the build identity.
Target type
The type field defines what is generated.
type = executabletype = statictype = sharedThe common mapping is:
executable -> add_executable(...)
static -> add_library(... STATIC ...)
shared -> add_library(... SHARED ...)This is the most important build-system decision the manifest makes.
If the target has main(), it should usually be:
type = executableIf the target is reusable code, it should usually be:
type = staticor:
type = sharedC++ standard
The standard field chooses the language standard.
standard = c++20Supported values are intentionally explicit:
c++17
c++20
c++23The manifest does not use forms like:
cpp20
20
std20The goal is to keep the syntax close to what C++ developers already recognize.
Sources are explicit
vix.app uses an explicit source list.
sources = [
src/main.cpp,
src/app.cpp,
]This avoids hidden glob behavior.
Globs look convenient at first, but they can make builds less predictable.
An explicit list makes it clear what is part of the target.
It also helps Vix validate the project before generating the internal build files.
If a source file does not exist, Vix can fail early with a useful error instead of waiting for a lower-level build tool to fail later.
Include directories
Include directories are listed with include_dirs.
include_dirs = [
include,
]This maps to the include paths needed by the target.
Recommended layout:
myapp/
include/
myapp/
app.hpp
src/
main.cpp
app.cppManifest:
include_dirs = [
include,
]Code:
#include <myapp/app.hpp>The manifest stays clear:
sources compile files
include_dirs expose header search pathsDefinitions
Preprocessor definitions go under defines.
defines = [
MYAPP_VERSION="1.0.0",
MYAPP_ENABLE_LOGGING=1,
]This keeps compile-time configuration visible in the manifest.
It also avoids hiding important build configuration inside ad-hoc compiler flags.
Compile options
Compiler flags go under compile_options.
compile_options = [
-Wall,
-Wextra,
]These are target-level options.
They are useful for warnings and local tuning.
The manifest does not try to normalize every compiler flag across every compiler.
That is a deliberate boundary.
For portable projects, keep this list small.
For advanced compiler-specific logic, use CMake.
Link options
Linker flags go under link_options.
link_options = [
"-Wl,--as-needed",
]This is separate from compile_options.
The split matters because compiler options and linker options have different meanings.
Incorrect:
compile_options = [
"-Wl,--as-needed",
]Correct:
link_options = [
"-Wl,--as-needed",
]Packages and links are separate
This is one of the most important design choices.
packages is for discovery.
links is for linking.
packages -> find_package(...)
links -> target_link_libraries(...)Example:
packages = [
fmt:REQUIRED,
]
links = [
fmt::fmt,
]Vix does not automatically link fmt::fmt just because fmt was found.
That would be convenient in some cases, but fragile in others.
Different packages expose different target names.
Examples:
fmt -> fmt::fmt
Boost -> Boost::system, Boost::filesystem
Threads -> Threads::Threads
OpenSSL -> OpenSSL::SSL, OpenSSL::CryptoThe manifest keeps this explicit.
Package syntax
The package syntax is compact:
<name>
<name>:REQUIRED
<name>:COMPONENTS=a,b
<name>:COMPONENTS=a,b:REQUIREDExamples:
packages = [
Threads:REQUIRED,
]packages = [
"Boost:COMPONENTS=system,filesystem:REQUIRED",
]Values with commas should be quoted.
This prevents the parser from splitting the package into multiple array items.
Resources
Runtime files go under resources.
resources = [
assets,
"data/config.json=config/config.json",
]The first form copies a file or directory using its basename.
resources = [
assets,
]The second form copies to a custom destination.
resources = [
"data/config.json=config/config.json",
]Resources are copied next to the built target.
This is useful for:
assets
config files
templates
public files
test dataOutput directory
output_dir controls the target output location inside the build tree.
output_dir = binFor an executable, this usually gives a layout like:
build-ninja/
bin/
myappFor a library, a common value is:
output_dir = libThis field is not an install prefix.
It only controls the build output layout.
Why the manifest is not JSON
A JSON manifest would be easy for machines, but less pleasant for humans.
This is noisy:
{
"name": "hello",
"type": "executable",
"standard": "c++20",
"sources": ["src/main.cpp"]
}The vix.app form is closer to a build manifest:
name = hello
type = executable
standard = c++20
sources = [
src/main.cpp,
]It is easier to edit by hand.
It also avoids JSON quoting everywhere.
Why the manifest is not YAML
YAML is powerful, but it has a large and sometimes surprising syntax surface.
vix.app uses a smaller format on purpose.
The goal is not to support every data-shaping feature.
The goal is to describe a C++ target clearly.
Why the manifest is not TOML
TOML would also work.
But vix.app uses a smaller custom format because the domain is narrow.
The manifest needs:
key = value
array = [...]
comments
quoted valuesThat is enough for the current design.
A smaller parser also gives Vix tighter control over diagnostics.
Validation is part of the design
The manifest is not only parsed.
It should be validated.
Examples of validation:
missing name
missing sources
invalid target type
invalid standard
unknown field
malformed array
invalid package syntax
source file not foundEarly validation gives better errors.
It also keeps generated CMake cleaner because invalid states are rejected before generation.
Generated CMake as an implementation detail
Today, the manifest is translated into an internal CMake project.
Example flow:
vix.app
-> parse manifest
-> validate manifest
-> generate .vix/generated/app/CMakeLists.txt
-> configure
-> buildThe generated CMake is not the source of truth.
The source of truth is:
vix.appThis separation matters.
It means the user edits a small manifest, while Vix handles the lower-level build details.
The manifest as future build input
The manifest is also useful beyond generated CMake.
Because it is structured, Vix can later use it to build a native graph directly.
Future path:
vix.app
-> native BuildGraph
-> ObjectCache
-> Scheduler
-> LinkThat is harder with arbitrary CMake because CMake is a full language.
But with vix.app, Vix already knows the target structure.
This makes the manifest a possible fast path.
A complete manifest
A realistic manifest can look like this:
name = myapp
type = executable
standard = c++23
output_dir = bin
sources = [
src/main.cpp,
src/app.cpp,
src/network/client.cpp,
]
include_dirs = [
include,
third_party/asio/include,
]
defines = [
MYAPP_VERSION="1.2.3",
MYAPP_ENABLE_LOGGING=1,
]
packages = [
Threads:REQUIRED,
fmt:REQUIRED,
"Boost:COMPONENTS=system,filesystem:REQUIRED",
]
links = [
Threads::Threads,
fmt::fmt,
Boost::system,
Boost::filesystem,
]
compile_options = [
-Wall,
-Wextra,
-Wpedantic,
]
link_options = [
"-Wl,--as-needed",
]
compile_features = [
cxx_std_23,
]
resources = [
assets,
"data/config.json=config/config.json",
]This is still a small file.
It describes the target without exposing the full complexity of CMake.
What belongs in vix.app
Good fields for vix.app are fields that describe the target directly:
name
type
standard
sources
include_dirs
defines
links
compile_options
link_options
compile_features
packages
resources
output_dirThese are stable concepts.
They map well to common C++ projects.
What does not belong in vix.app
Some things should stay out of the manifest.
Examples:
complex install rules
custom code generation
large dependency graphs
FetchContent
CTest discovery
toolchain-specific branches
custom CMake functions
package export generationThese belong in CMake.
The manifest should not grow until it becomes another build-system language.
The boundary
The design boundary is:
vix.app describes one target.
CMake describes arbitrary build logic.That boundary keeps both tools useful.
vix.app makes the simple path simpler.
CMake remains the escape hatch for full control.
Conclusion
The vix.app manifest is designed to be small, explicit, and predictable.
It focuses on the common shape of a C++ target:
name
type
standard
sources
includes
packages
links
resourcesIt avoids hidden magic.
It keeps package discovery separate from linking.
It uses generated CMake today for compatibility.
It can become native BuildGraph input later.
That is the main reason the manifest exists as a separate layer.
vix.app is not a second CMake.
It is a structured target description for Vix.