Documentation
Nobs manual
Open raw manual.mdInstallation
Nobs doesn’t require installation setup other than being available in your PATH. Before the first normal build, run:
nobs --find-compilers
This detects compiler toolchains and writes them to the machine-local registry. On Windows the registry is %APPDATA%\nobs\.nobs_registry.txt; on Unix-like systems it is ~/.config/nobs/.nobs_registry.txt. Normal builds use these registry entries to select a compiler. It is possible to specify custom compiler installation directories in the registry.
Quick start
Syntax of nobs is loosely based on Makefile. There are several differences, but the main difference is that nobs is declarative rather than imperative, meaning nobs defines “what to build”, rather than “what to do”. The “what to do” is composed by the build tool internally. The work of the developer is then to define “what is composed of what”.
For demo project, you can find explained sample here.
You can find more examples in section Common workflows.
Core concepts
The syntax examples in this section are written to be easy to understand, with complexity increasing gradually. Not all examples are intended for real-world use.
Nobs is case insensitive.
Nobs is scoped. Global context is intentionally limited.
Contexts
Context is a collection of variables. For now, you can think of it this way for a simple example:
target_name:
{
C_FILES = [P"(file1.c)", P"(file2.c)"]
INCLUDE = P"(include)"
VAR2 = 2
// .. etc
}
The “context” part of the above would be the part between curly braces.
Targets
Targets are named contexts with parameters, dependencies and their parameters. In the above example, target_name is a target that has no parameters and no dependencies.
How does nobs do the building?
Nobs builds a target by creating its context, adding the implicit context, and matching the result against registered tools from the Nobs registry. Each registered tool evaluates whether it can build the context and assigns a score based on matching variables. The best matching tool is selected, its registry context is added, dependencies are built, and then the selected builder runs.
Each target is built by at most one builder. Dependencies are separate targets and may be built by different builders. For more information, see sections Nobs registry, Builder selection, and Supported compilers.
Implicit context
In addition to user-defined contexts in nobs.txt, Nobs provides an implicit context that is applied to every target before it is built. This implicit context is assembled in this order:
Host system information (like OS family, date, project root, default output directory)
Variables defined in the
defaultproject profile, if the project defines oneVariables defined in currently selected project profile
Variables defined on the command line
Later items override or append to earlier items according to normal assignment rules. This means command-line assignments are the last layer and can override profile defaults for the current run. When an explicit profile is selected, Nobs also adds PROFILE with the selected profile name.
Command-line variables can be used to modify the current Nobs run.
For example, running nobs 'c_flags+=["/Zi"]' adds the /Zi flag to every target, which is useful for debug builds.
Profile contexts act as named shortcuts for this mechanism.
A project typically defines profiles such as Debug and Release, which usually differ in compiler flags and macro definitions.
By defining these variables in profiles, you avoid having to specify them manually on each run.
Profiles are defined in the .nobs_project.txt file, which is described later in this section.
After a registered tool is selected, variables from that tool’s registry entry are also added to the target context using assign-if-empty behavior. This is how detected toolchain paths and toolchain-specific defaults become available during the build without being written in every target.
Dependencies
Targets can depend on other targets and their outputs. Dependencies are built before the dependent target.
The dependent target can then access the dependency’s outputs through a context variable named after the dependency.
The following example describes how dependency output can be used in the dependent target.
Note: ^ is used to mark variables to be added to output context after building. Builders can also add output variables, such as OBJS, LIB, EXE, or BINARY.
target: dependency
{
cmdline = "echo Hello ${dependency.VAR1}"
}
dependency:
{
NO_TOOL
^VAR1 = "World"
}
Targets can have parameters, which are passed to dependencies as variables.
main_target: dependency(dep_param)
{
dep_param = {
var1 = "Hello"
var2 = "World"
}
cmdline = "echo ${dependency.hello}"
}
dependency(param):
{
NO_TOOL
^hello = "Dependency says: ${param.var1} ${param.var2}"
}
Units and projects
When working with multiple modules, it is common for one module to depend on another.
While it is possible to keep all targets in a single nobs.txt file and reference them directly, this approach quickly becomes hard to maintain as the number of targets grows.
A more structured approach is to use units and split modules into their own unit files.
A unit is simply a named nobs file that can be referenced by other units.
This allows you to group targets by module and reference them through a shared interface.
Instead of exposing individual targets, a unit can define a consistent build entry point, effectively letting you compose your own project-specific build structure.
For the next example, we need to introduce a small piece of new syntax.
A “+” placed before a dependency indicates that once the dependency is built, its output variables are merged directly into the dependent target’s variable set. The dependency output is still also available through its normal dependency context name, so + is useful both for direct consumption and for explicit qualified access when needed.
Targets that only export metadata, such as public include paths, declare NO_TOOL. This tells Nobs that no command or compiler should run for that target.
When appending to a variable that will also be supplied by a merged dependency, preserve the intended type explicitly. For list-like build variables such as C_DEFINES, C_FLAGS, INCLUDE, and LIB, use a list even for a single item:
app: +common
{
C_DEFINES += ["LOCAL_FEATURE"]
}
Writing C_DEFINES += "LOCAL_FEATURE" would create C_DEFINES as a string if it does not already exist in app. A later +common merge that contributes a list-valued C_DEFINES would then append that list to the string value instead of preserving C_DEFINES as a list of separate compiler arguments.
Main module nobs.txt file:
unit InstantMessanger
build: +core +api +NetworkingLib::build
{
c_files = P"(main.c)"
}
api:
{
NO_TOOL
^include = [
P"(api)"
]
}
core: +api +NetworkingLib::api
{
c_files = [
P"(user.c)",
P"(chat.c)",
// etc.
]
include = P"(.)"
}
tests: +core +api
{
c_files = [P"(test1.c)", P"(test2.c)"]
include = P"(.)"
}
NetworkingLib nobs.txt file:
unit NetworkingLib
build: +api +core
{
OBJS
OUTPUT_NAME = "NetworkingLib.lib"
^include
}
api:
{
NO_TOOL
^include = [
P"(api)"
]
}
core: +NetworkingLib::api
{
c_files = [
P"(socket.c)",
P"(connection.c)",
// etc.
]
c_flags = ["/c"]
include = P"(.)"
}
tests: +core +api
{
c_files = [P"(test1.c)", P"(test2.c)"]
include = P"(.)"
}
Where are the object files?
The object files are in variable called OBJS. This variable is produced by C/C++ builders after building a target with C_FILES or CPP_FILES.In order for nobs to have a way of finding units’ nobs files, nobs searches recursively parent folders until it finds .nobs_project.txt. A quick example of a project file respective to InstantMessanger module defined above:
name InstantMessangerProj
units {
InstantMessanger {
"relative/path/to/InstantMessanger/nobs.txt"
}
NetworkingLib {
"relative/path/to/NetworkingLib/nobs.txt"
}
}
profiles {
debug {
c_flags += ["/Zi", "/Od"]
}
release {
c_flags += ["/O2"]
}
}
A project file consists of up to four sections:
imports, which are used for references to other projects. Units from imported project files are read and can be referenced using the syntax
namespace/unit::target. Relative import paths are resolved relative to the importing.nobs_project.txt.tools, which define project-local build tools using the same syntax as registry tools. They are loaded with the project and are useful for project-specific command tools such as packaging, signing, code generation, or deployment steps.
units, which specify unit names and paths to their nobs.txt files
profiles, which specify variables used implicitly across all targets employed in the build (if the profile is selected)
Imports are semicolon-terminated. A string imports a project file by path and uses the imported project’s own name as the namespace. A string followed by an identifier imports by path and uses the identifier as a namespace override. A bare identifier imports a registered project from .nobs_registry.txt. Bare imports require the project to be registered in the user registry, for example with nobs -r; path imports do not.
An import can be limited to a profile of the importing project by prefixing it with profile:. It can also select an explicit profile of the imported project with (profile). If the imported profile is omitted, the imported project uses only its automatic default profile, if it defines one.
import {
"../libs/math/.nobs_project.txt";
"../vendor/logging/.nobs_project.txt" logging;
registered_common;
"../libs/zlib/.nobs_project.txt" zlib (release);
debug: "../tools/codegen/.nobs_project.txt" codegen (debug);
release: "../tools/codegen/.nobs_project.txt" codegen (release);
}
Syntax
Comments and whitespace
White space characters are space, \t, \r and \n. Whitespace characters are skipped by the parser.
Line comments
// this is a line comment
Block comments
/* this is
a block comment */
Values
Strings
Strings are written using double quotes.
They support variable expansion and list expansion, which makes them suitable for composing paths, flags, and other derived values.
"string"
"string ${VAR}"
"string #{ARRAY}"
"string #{ARRAY} ${VAR} #{ARRAY2}"
${VAR}expands to the value of a variable.#{ARRAY}expands to all elements of a list. For example:ARRAY=[1,2,3] TEXT="Number #{ARRAY}"After expansion TEXT will be:
["Number 1", "Number 2", "Number 3"]Multiple expansions can be freely combined in a single string.
Expansion happens after parameters are merged in and after dependencies are built and their outputs are merged in.
A string is still one value, even when it contains spaces. Variables that represent repeated command-line arguments should use a list. By convention, variables ending in
_FLAGSare lists of tool arguments. For example, writeC_FLAGS += ["/Od", "/Zi"]andLINK_FLAGS += ["/DEBUG", "/OPT:REF"], notC_FLAGS += "/Od /Zi"orLINK_FLAGS += "/DEBUG /OPT:REF".
String expansion
String expansion is performed in multiple phases and follows a deterministic order.
Variable expansion (${})
All ${VAR} expressions are expanded first.
Expansion is performed recursively, meaning that if the expanded value itself contains ${}, it is expanded again until no ${} remains.List expansion (#{})
Once all ${} expansions are resolved, list expansions are processed.#{VAR} expressions are handled from left to right.
For each element in the list referenced by VAR, a new string variant is created.
This results in a list of strings.
Re-evaluation of generated strings
Each generated string is then processed again:First for ${} expansion,
Then for the next #{} expansion (if any),
until no expansion expressions remain.
Identifier requirement
In both ${VAR} and #{VAR}, VAR must be a value of type identifier.
This process ensures predictable expansion while allowing complex combinations of variables and lists.
Raw strings
Raw strings are written using the R"( ... )" syntax.
Their contents are taken verbatim and no expansion is performed.
R"(raws\tring ${this_wont_expand} )"
Backslashes do not need to be escaped, the string will be left as is.
Path strings
Path strings are written using the P"( ... )" syntax.
They represent file system paths and are normalized internally. Path strings support both expansion and globbing. Expansion follows the same rules as described in String expansion.
P"(path\relative\${VARIABLE}\this/file.txt)"
P"(images/*.jpg)"
P"(out/#{PLATFORM}/${MODE}/*.exe)"
Both
/and\can be used as path separators.Paths are resolved relative to the file in which they appear.
Intended for file and directory paths, not general text.
The
*wildcard is supported and acts as a filesystem glob.Expansion and globbing may result in list of multiple paths.
After expansion, the resulting path(s) are normalized.
Numbers
Numbers are written using a decimal notation. Both positive and negative integers are supported.
6
-5
Lists
Lists are written using square brackets.
["string", identifier, 5]
A list can contain values of different types. When list is being resolved all its items are resolved one by one. This means e.g. strings inside of a list will get expanded.
Use lists for variables where each item is a separate tool argument. This is especially important for variables ending in _FLAGS: each compiler or linker option should be its own list item.
Identifiers
Identifiers refer to variables, context members, or indexed values.
_identifier1
identifier
myCtx.subVar[idx]
myCtx.subVar[myCtx2.subVar[idx2]].var
Dot (
.) is used to access members of a context.Square brackets (
[ ]) are used for indexed access.Indexed expressions may themselves contain identifiers.
Context
A context is a structured collection of variables.
{} // empty
{VAR="VAL"}
{
subCtx={VAR="SUBVAL"}
}
A context can be assigned to a variable and accessed via dot notation. In the example above, "SUBVAL" is accessed as subCtx.VAR.
Contexts are a core building block used to pass structured data between targets, dependencies, and outputs.
Assignments
Simple assignment (=)
Simple assignment assigns a value to a variable. If the variable does not exist, it is created and assigned the given value. If the variable already exists, its value is replaced.
VAR = "value"
COUNT = 5
LIST = ["a", "b"]
The previous value, if any, is discarded.
Append assignment (+=)
Append assignment combines the existing value with the new value.
If the variable does not exist, it is created and assigned the given value.
The behavior of += depends on the types of the left-hand and right-hand values.
That means the right-hand side also determines the variable type when the variable does not exist yet. For variables that are meant to stay lists, use a list literal even for one item:
C_DEFINES += ["LOCAL_FEATURE"]
Writing C_DEFINES += "LOCAL_FEATURE" creates C_DEFINES as a string when it does not already exist. If another context or merged dependency later appends list-valued C_DEFINES, the list is flattened into that string instead of preserving separate list items.
String append
STR += "text"
string : string Appends a space and then the right-hand string.
string : int
Appends the integer converted to a string, without a space.
string : list
Flattens the list into a space-separated string and appends it with a space.
Integer append
COUNT += 3
int : int
Adds the values arithmetically.
List append
LIST += ["a", "b"]
list : list
Appends all elements of the right-hand list to the left-hand list.
list : anything else
Appends the right-hand value as a single new list element.
Context append
CTX += OTHER_CTX
context : context
Performs += recursively on all matching keys.
Existing keys are appended according to their value types.
Missing keys are created.
context : string
Creates a new empty variable named by the string inside the context.
Identifier resolution
If the right-hand side is an identifier, its value is fetched first and the append operation is then evaluated using the resolved value.
A += B
This is equivalent to appending the value stored in B.
Invalid conversions
Any unsupported type combination results in an error.
anything : unsupported type → bad conversion exception
This prevents silent or ambiguous behavior.
Conditional assignment
STR?="string"
If the variable does not exist, it is created and assigned the given value. If the variable already exists, the assignment is ignored and the existing value is preserved. Conditional assignment is typically used to provide default values that may be overridden by other contexts or assignments.
Key definitions and assignment modifiers
An assignment can name a variable without assigning a value:
OBJS
DLL
MSVC
This creates the variable with no value. Such variables are often used as selectors or declarations. For example, declaring OBJS in a static-library target lets the library builder be selected before dependencies produce the actual object list.
An assignment can also use a modifier before the variable name:
^INCLUDE = P"(include)"
!files
^ marks the variable as part of the target output context. It can be used in normal nobs.txt files, project profiles, and registry tool definitions.
! marks a variable as required by a registry tool. It is accepted only in .nobs_registry.txt; using it in a normal unit or project file is an error.
Conditions
Conditional blocks append assignments to the current context when their condition is true:
if os_family = "windows"
{
C_FLAGS += ["/Zi"]
}
if GNU
{
C_FLAGS += ["-g"]
}
A condition can test whether a variable exists, or compare a variable with a value using =, !=, <, or >. Comparisons resolve identifiers before evaluating the condition. Conditional assignments are applied after builder selection but before dependencies are resolved, so do not use a conditional block to introduce the first variable that makes a target buildable.
Variables assigned inside if blocks do not participate in registered tool or concrete builder selection. If a variable must select or prefer a tool, declare it outside the conditional block, usually as a key-only variable, and use the conditional block only for values that are needed after the tool has already been selected.
Indexing variables
Variables can be indexed depending on their type.
Indexing allows extracting values from strings, lists, and contexts.
String indexing
Strings can be indexed using integers or ranges.
VAR = STRVAR[5]
VAR is assigned the 6th character of STRVAR.
VAR = STRVAR[0:2]
VAR contains the first two characters of STRVAR.
VAR = STRVAR[3:6]
VAR contains three characters, starting at the 4th character of STRVAR. The end index (6) is exclusive.
List indexing
Lists can be indexed using integers.
VAR = LIST[5]
VAR is assigned the 6th element of LIST.
Context indexing
Contexts can be indexed using identifiers only.
VAR = CTX[IDXVAR]
The identifier inside the brackets is resolved first. Its value is then used as a key in the context.
This is equivalent to a dynamic lookup:
CTX.<resolved_identifier_value>
This form is typically used to implement lookup tables.
Example
cpu_gcc_flags =
{
CM0PLUS=["-mthumb", "-mcpu=cortex-m0plus"]
CM3=["-mthumb", "-mcpu=cortex-m3"]
}
CPU = "CM0PLUS"
C_FLAGS = cpu_gcc_flags[CPU]
CPUresolves to"CM0PLUS".cpu_gcc_flags["CM0PLUS"]is fetched.C_FLAGSis assigned["-mthumb", "-mcpu=cortex-m0plus"].
Targets
Minimal target
A minimal target consists of a name and a body. If the target is intentionally a no-op, declare NO_TOOL in its body.
target_name:
{
NO_TOOL
}
Target with parameter
Targets can declare parameters. When the target is invoked, the parameter value is available inside the target body as a normal variable.
target_name(parameter_name):
{
NO_TOOL
// parameter can be used as if it was a variable
}
Parameters are typically used to configure a target for different modes, toolchains, platforms, or other variants.
Target with dependencies
A target may list dependencies after :.
Dependencies may optionally be parameterized.
target_name: dependency1 dependency2(parameter)
{
NO_TOOL
parameter="something"
var1=dependency1.someOutputVar
}
The value passed to a parameterized dependency is resolved from the caller context. It can be a scalar value, list, path, or context value. For structured parameters, assign the parameter name to a context value in the caller:
dll: +core(build_options)
{
DLL
OBJS
build_options =
{
C_DEFINES = "MYDLL_EXPORTS"
}
}
core(build_options):
{
C_FILES = P"(functions.c)"
C_FLAGS = ["/c"]
C_DEFINES = build_options.C_DEFINES
}
DLL selects DLL output. OBJS is declared before dependencies are built so a linker-capable builder can be selected; the actual object list is then supplied by +core.
Dependencies are built before the target. The outputs of a dependency become available to the dependent target as a context variable named after the dependency. This allows consuming dependency results without having to manually “re-find” them.
When the same target is used more than once with different parameters, give each dependency output an explicit alias with =>:
app: build_variant(debug_cfg) => debug build_variant(release_cfg) => release
{
NO_TOOL
DEBUG_BIN = debug.BINARY
RELEASE_BIN = release.BINARY
}
The alias changes only the name under which the dependency output context is stored in the caller. The target that is built is still build_variant. Without =>, Nobs keeps the original behavior and stores the output under the dependency target name.
Dependency names share the same case-insensitive namespace as variables in the dependent target. Avoid naming dependencies or targets the same as your own variables or build-tool variables such as OBJS, LIB, INCLUDE, C_FILES, CPP_FILES, C_DEFINES, OUTPUT_NAME, or LIBPATH.
For example, a target named objs can collide with the OBJS variable when used as a dependency, because dependency output would be stored under objs while object files are stored under OBJS.
Dependency format
A dependency can be referenced in a fully-qualified form:
project/unit::target
This is useful when splitting a project into multiple units and/or namespaces and you want explicit, unambiguous references.
When consuming the output of a qualified dependency without the + modifier, the dependency output is exposed under the dependency unit name, and then under the dependency target name:
app: mylib::build
{
NO_TOOL
LIB = mylib.build.LIB
INCLUDE = mylib.build.INCLUDE
}
If the dependency is imported through a project namespace (project/unit::target), the output context is still addressed by the unit name and target name inside the consuming target:
app: project/mylib::build
{
NO_TOOL
LIB = mylib.build.LIB
}
Use +unit::target when the output should be merged directly into the current context while still remaining available as unit.target.VAR.
Use unit::target => alias when the output should be exposed directly as alias.VAR instead of unit.target.VAR.
Dependency modifier
Prefixing a dependency with + means:
append (merge) the dependency’s output context directly into the current target’s context.
target_name: +dependency
{
NO_TOOL
VARIABLE="Hello"
}
dependency:
{
NO_TOOL
^VARIABLE="World!"
}
In this example, the dependency’s output is merged using the same += rules described in Append assignment:
If
VARIABLEexists in both contexts, the values are combined according to their types.If it exists only in the dependency output, it is created in the target context.
This is useful when you want a dependency to extend the current target context (flags, defines, include paths, libraries, etc.). The dependency output is still also stored under the normal dependency context name, so both forms can be used:
app: +mylib::build
{
NO_TOOL
// LIB from mylib::build is merged directly into app.
// The same output can still be addressed explicitly as mylib.build.LIB.
}
Dependency macros
Dependency macros allow generating dependencies dynamically based on a variable.
target_name: @DEP_VARIABLE(param)
{
NO_TOOL
DEP_VARIABLE = "dep1 dep2" // or ["dep1", "dep2"]
param = "something"
}
dep1(param):
{
cmdline="echo ${param}"
}
dep2(param):
{
cmdline="echo ${param} else"
}
@DEP_VARIABLE(param) expands into a list of dependencies defined by DEP_VARIABLE. The parameter(s) in the macro invocation are passed to each expanded dependency.
Putting . first in the parameter list, as in @DEP_VARIABLE(., param), also passes the source item that produced each dependency as the first parameter. Explicit parameters follow after it:
target_name: @DEPS(., shared_param)
{
DEPS = ["dep1", "dep2"]
shared_param = "value"
}
dep1(config, shared_param):
{
NO_TOOL
}
This is equivalent to invoking dep1("dep1", shared_param) and dep2("dep2", shared_param). The first parameter is the current item from DEPS; it is not resolved as a normal context variable.
When DEPS is a context, its keys name the dependencies and each corresponding value is passed for .. For example, DEPS = { dep1 = { mode = "debug" } } invokes dep1(DEPS.dep1).
If a dependency macro expands to exactly one target, its output can be given a stable name with =>:
target_name: @SELECTED_DEP(param) => selected
{
NO_TOOL
SELECTED_DEP = "dep1"
param = "something"
value = selected.out
}
This is useful when the actual dependency target name is selected from configuration and the caller should not need to know that final name. The target named by SELECTED_DEP is built, but its output is exposed as selected.
A single-output alias cannot be used when the macro expands to multiple dependencies:
target_name: @DEPS => selected
{
DEPS = "dep1 dep2" // invalid with => selected
}
Use no alias for multi-target macro expansion, so each dependency output keeps its own target name, list the dependencies explicitly with separate aliases, or use a merge alias with +>.
A merge alias appends the output contexts of all expanded dependencies into one named context:
target_name: @DEPS +> deps
{
NO_TOOL
DEPS = "dep1 dep2"
FLAGS = deps.C_FLAGS
INCS = deps.INCLUDE
}
+> uses the same append rules as dependency output merging, but the values are merged into the alias context (deps in the example) instead of directly into the current target context. This is separate from the +dependency modifier: +dependency merges a dependency output directly into the current target context, while dependency +> alias keeps the merged output under alias.
A common use-case is a shared build step → many consumers pattern. You build or generate a common input once (shared sources, generated headers, assets, codegen output, etc.). Multiple targets that need this input receive it through a parameter.
Example: building a set of samples that all depend on the same library, where each sample demonstrates one feature — the shared library build is referenced once, and each sample target receives the produced artifact via a parameter.
This keeps the configuration DRY while still allowing many outputs to reuse the same upstream build result.
Advanced
What happens when you build a target
This section provides a high-level overview of what happens internally when a target is built.
The goal is to explain the order of operations, not the exact implementation details.
Target context copy
A mutable copy of the target’s context is created.
This ensures the original target definition remains unchanged.Implicit context injection
An implicit context is merged into the target context.
This implicit context is composed of:the host context,
the
defaultprofile, if one exists,the selected build profile,
command-line variables.
The selected profile is applied after
default, and command-line variables are applied last. If an explicit profile is selected,PROFILEis also added to the implicit context.Parameter binding
Target parameters are added to the context and become available as regular variables.Builder selection
The most suitable builder is selected based on the current context and available build tools. This happens before dependencies are built, so variables produced only by dependencies are not visible for builder selection unless the current target declares them. Variables that participate in tool selection should be declared directly in the target body, not introduced only by a targetifblock. Target conditions are evaluated after builder selection, so variables added only by such conditions cannot make a tool applicable for the target.Conditional evaluation
Allifstatements are evaluated and applied to the context.Dependency resolution
The dependency list is created:dependency macros are expanded,
all dependencies are resolved into concrete target invocations.
Recursive dependency build
buildTarget()is called recursively for each dependency.- If a dependency uses the
+modifier, its output context is appended to the current context using the standard append rules.
- If a dependency uses the
Context variable resolution
All variables in the context are resolved and expanded.Build execution
The selected builder is finally given the resolved context and performs the actual build.Output collection
The builder output is merged with all variables marked as output (^).
The resulting context is returned as the output ofbuildTarget().
Nobs registry
Nobs keeps machine-local configuration in a registry file. On Windows the file is stored in:
%APPDATA%\nobs\.nobs_registry.txt
On Unix-like systems it is stored in:
~/.config/nobs/.nobs_registry.txt
The registry is not part of a project. It describes things available on the current machine, especially detected toolchains, registered projects, and user-defined command tools. Nobs loads this file before project discovery and creates the set of toolchain instances that can later participate in builder selection. nobs --find-compilers creates or updates compiler tool entries; nobs -r registers the current project for bare project imports.
The registry can contain registered projects and registered tools:
projects {
my_project "D:\work\my_project\.nobs_project.txt"
}
tools {
default_msvs_x64_14_43_34808 {
tool_type = "msvc"
toolset_version = "14.43.34808"
visual_studio_version = "2022"
toolchain_edition = "BuildTools"
arch = "x64"
vs_path = "C:\Program Files (x86)\Microsoft Visual Studio\"
toolset_path = "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.43.34808"
}
}
Project entries map a registered project name to its .nobs_project.txt file. They are resolved lazily when a project import uses a bare identifier:
import {
my_project;
}
The registered project file is loaded at that point and its project name becomes the namespace, unless the import uses an explicit path import with a namespace override.
Each entry inside tools has an arbitrary registry name. The mandatory tool_type value selects the kind of registered tool, for example msvc, gnu-gcc, or command. The remaining variables become part of that tool’s selection context. They can identify a concrete installation, such as a Visual Studio toolset path, or describe what kind of target the tool is interested in.
Required registry variables
Inside the registry file only, a variable can be prefixed with !:
tools {
testpkgr {
tool_type = "command"
!bins
!scripts
!files
!version
if os_family = "windows"
{
COPY="copy "
RMTEMP=R"(rmdir /S /Q temp)"
}
if os_family = "unix"
{
COPY="cp "
RMTEMP="rm -rf temp"
}
cmdline = [
"mkdir temp",
R"(mkdir "temp/bin")",
R"(mkdir "temp/scripts")",
R"(mkdir "temp/files")",
"${copy} ${BINS} temp/bin",
"${copy} ${SCRIPTS} temp/scripts",
"${copy} ${FILES} temp/files",
"cd temp && 7z a ../ptests-${VERSION}.7z bin scripts files",
"${RMTEMP}"
]
^package="ptests-${VERSION}.7z"
}
}
The ! marker means that the variable is required for this registered tool to be considered buildable. In the example above, testpkgr is ignored unless the target context contains BINS, SCRIPTS, FILES, and VERSION.
The same tool syntax can also be used in .nobs_project.txt:
tools {
packager {
tool_type = "command"
!bins
cmdline = "echo ${BINS}"
}
}
Project tool names are qualified by the project namespace internally, so imported projects can bring their own tools without colliding with tools from the main project or the registry. The ! marker is accepted inside project tools, but remains invalid in project profiles and normal unit files.
Custom command tools
A registry tool can be used to define project-independent custom behavior. The testpkgr example above uses tool_type = "command", which runs cmdline commands instead of invoking a compiler toolchain. That makes it suitable for local utility steps such as packaging tests, copying build artifacts, generating archives, or wrapping an external tool.
On Windows, Nobs also creates a built-in pwsh command tool. It is selected by the pwsh variable and runs commands through powershell.exe instead of the default system shell.
Such a tool is selected in the same way as compiler toolchains: the target declares the variables that describe what it wants to build, and the registry tool declares what variables it needs or prefers. For example, a target that provides BINS, SCRIPTS, FILES, and VERSION can be matched by the packaging tool without hardcoding the command sequence into every project.
Builder selection
Builder selection determines which registered toolchain or command tool will be used to build the current context.
The selection is driven by variables present in the build context and by tool definitions loaded from the Nobs registry and the current project graph. Nobs always creates a built-in default_basic command tool, then adds tools from .nobs_registry.txt, and finally adds tools from the main project and imported projects.
Registered tool matching
The first step is matching all registered tools.
A registered tool has a
tool_type, such asmsvc,gnu-gcc, orcommand.Registry variables without
!are variables the tool is interested in. If the build context contains matching variables, the tool receives a higher score.Registry variables with the registry-only
!marker are required. If the build context does not contain them, that registered tool is not buildable for the current target.The selected tool type can also apply its own checks. For example, compiler tools can verify architecture, toolchain version, or other toolchain-specific details.
Each registered tool inspects the current context and produces a match result. The buildable tool with the highest score is selected.
Tool selection inside a registered toolchain
Once a registered compiler toolchain is selected, it chooses the concrete tool that will build the target.
Examples of tools are gcc, g++, ar, ld, cl.exe, lib.exe, etc.
The registry chooses the toolchain instance; that instance chooses the concrete tool inside it. The concrete tool still has its own requirements. If a required variable is missing, the tool declares the context not buildable. Only tools whose requirements are fully satisfied are considered viable.
Examples:
gccis viable withC_FILESorOBJSg++is viable withC_FILES,CPP_FILES, orOBJS
From the viable tools, the registered tool selects the most appropriate one to handle the build.
For tool_type = "command", there is no compiler sub-selection: it runs the cmdline command or command list from the resolved context. On Windows, tool_type = "pwsh" similarly runs the pwsh command or command list through powershell.exe.
Why registered tools and builders are separated
Registered tools and builders serve different responsibilities by design.
Registered tools focus on the platform and toolchain selection.
Builders (tools) focus purely on how to build the given source inputs.
This separation allows builders to depend only on variables related to source texts or command inputs (such as C_FILES, CPP_FILES, cmdline, pwsh, etc.), while registered tools handle platform-specific and registry-specific decisions.
As a result, a target can define only what it builds (for example C_FILES or BINS), and the choice of how it is built can be provided externally by the registry, command-line assignments, or a build profile.
This avoids platform-specific branching inside targets and keeps build definitions clean, portable, and declarative.
MSVS toolchain
The MSVS toolchain is represented by registry entries whose tool_type is msvc.
Registry and discovery variables
These variables belong primarily in .nobs_registry.txt tool entries. They describe a concrete Visual Studio installation or constrain how one is found.
| Variable | Description |
|---|---|
tool_type | Must be msvc for MSVS registry tools. |
toolset_path | Direct path to a Visual C++ toolset directory containing bin, include, and lib. If present, it is used directly. |
vs_path | Visual Studio root or higher-level Visual Studio install root to search. Used when toolset_path is not provided. |
sdk_path | Optional Windows SDK path. If omitted, Nobs searches the default Windows Kits locations. |
toolset_version | Toolset version, for example 14.43.34808. Used as a discovery filter and as a match filter. |
visual_studio_version | Visual Studio version directory, for example 2019 or 2022. Used during discovery from vs_path. |
toolchain_edition | Visual Studio edition, for example Community, Professional, Enterprise, or BuildTools. |
arch | Target architecture for the registered toolchain, for example x86, x64, or arm64. |
HOST_ARCH | Host tool architecture. x86 and x64 are normalized to Hostx86 and Hostx64; if omitted, Nobs uses PROCESSOR_ARCHITECTURE. |
The current default detection writes registry entries with toolset_path, vs_path, toolset_version, visual_studio_version, toolchain_edition, and arch.
Target selection variables
These variables are normally used by targets, profiles, or command-line assignments to steer matching and concrete tool selection. They are also the usual way to disambiguate projects when more than one registered toolchain can build the same input. For example, a Windows application can set MSVC or WINDOWS in a target, selected profile, or command line to prefer MSVS over an unrelated GNU toolchain found in the registry.
| Variable | Description |
|---|---|
MSVC | Score hint for MSVS matching. No value is required. |
WINDOWS | Score hint for Windows/MSVS matching. No value is required. |
HOST_ARCH | Requires a matching host tool architecture. |
ARCH | Requires a matching target architecture. |
toolset_version | Requires an exact matching MSVC toolset version, or one of a list of exact allowed versions. |
visual_studio_version | Requires an exact matching Visual Studio version, or one of a list of exact allowed versions. |
toolchain_edition | Requires a matching Visual Studio edition. |
WIN_SDK_VERSION | Requires the selected Windows SDK full version. |
WDM or DRIVER | Requires a Windows SDK that contains WDK content. Also affects link.exe output mode. |
C_FILES, CPP_FILES, OBJS, RC_FILE, MC_FILE, PFX_FILE, SIGN_BINARY | Main input variables that make concrete MSVS tools buildable. Signing requires both PFX_FILE and SIGN_BINARY; INF_FILE is optional catalog input. |
OUTPUT_NAME sets the primary artifact file name. It does not select a tool by itself. Some tools have extension-specific disambiguation rules for common non-executable outputs:
*.libcan disambiguate MSVC static libraries towardlib.exe*.rescan disambiguate Windows resources towardrc.exe*.catcan disambiguate catalog signing
There is no *.exe disambiguation rule. To link an executable with link.exe, declare LINK together with OBJS; OUTPUT_NAME = "app.exe" only names the output.
For static libraries on MSVC, the practical selector is OBJS, usually together with OUTPUT_NAME = "name.lib" or STATIC.
Available tools
The tables below separate build selectors, score hints, command inputs, and output variables. Score hints influence builder selection, but not every score hint is emitted on the command line. Variables marked as output with ^ are not used as build selectors.
cl.exe
cl.exe is selected for C/C++ compilation and normal executable builds.
| Kind | Variables |
|---|---|
| Buildable when present | C_FILES, CPP_FILES, or OBJS |
| Score hints | CL, CPP, C_FLAGS, C_FILES, CPP_FILES, C_DEFINES, OBJS, INCLUDE, LIB, OUTPUT_NAME, LIBPATH, LINK_FLAGS |
| Command inputs | C_FLAGS, C_FILES, CPP_FILES, C_DEFINES, OBJS, INCLUDE, LIB, OUTPUT_NAME, LIBPATH, LINK_FLAGS |
| Outputs | OBJS when /c is present in C_FLAGS; otherwise BINARY and BINARY_NAME. DLL builds also output DLL/DLL_NAME and an import LIB/LIB_NAME. |
C_DEFINES values are prefixed with /D. INCLUDE values are prefixed with /I. LIBPATH and LINK_FLAGS are emitted after /link when linking is part of the cl.exe command.
Write _FLAGS variables as lists, with one compiler or linker option per item:
C_FLAGS += ["/Od", "/Zi"]
LINK_FLAGS += ["/DEBUG", "/DYNAMICBASE", "/OPT:REF"]
Do not put multiple options into one string. With cl.exe, a single LINK_FLAGS string is emitted as one quoted argument after /link, so the linker treats it as one option.
lib.exe
lib.exe creates static libraries.
| Kind | Variables |
|---|---|
| Buildable when present | OBJS |
| Score hints | STATIC, LIB, OBJS, OUTPUT_NAME, INCLUDE_SYMBOL, LIBPATH, DEF, SUBSYSTEM, MACHINE |
| Command inputs | OBJS, OUTPUT_NAME, INCLUDE_SYMBOL, LIBPATH, DEF, SUBSYSTEM, MACHINE |
| Outputs | LIB and LIB_NAME |
DEF is an optional input for lib.exe, not a required selector.
link.exe
link.exe links object files into binaries, DLLs, or drivers.
| Kind | Variables |
|---|---|
| Buildable when present | OBJS |
| Score hints | LINK, DLL, DRIVER, WDM, OBJS, LIB, INCLUDE_SYMBOL, LIBPATH, DEF, SUBSYSTEM, MACHINE |
| Command inputs | OBJS, LIB, INCLUDE_SYMBOL, LIBPATH, DEF, SUBSYSTEM, MACHINE, LINK_FLAGS, OUTPUT_NAME |
| Outputs | Always BINARY and BINARY_NAME; also DLL/DLL_NAME for DLL, SYS/SYS_NAME for DRIVER or WDM, otherwise EXE/EXE_NAME. DLL builds may also output an import LIB/LIB_NAME. |
Only one of DLL, DRIVER, and WDM should be present. WDM emits /DRIVER:WDM. DRIVER emits /DRIVER, unless DRIVER = "WDM" is used, in which case it also emits /DRIVER:WDM.
Use LINK when a target should be linked by link.exe directly:
app: +core
{
LINK
OBJS
OUTPUT_NAME = "app.exe"
}
core:
{
C_FILES = P"(main.c)"
C_FLAGS = ["/c"]
}
OUTPUT_NAME = "app.exe" does not select link.exe; it only sets the executable file name.
Write _FLAGS variables as lists, with one compiler or linker option per item:
LINK_FLAGS += ["/DEBUG", "/DYNAMICBASE", "/OPT:REF"]
rc.exe
rc.exe compiles Windows resource files.
| Kind | Variables |
|---|---|
| Buildable when present | RC_FILE |
| Score hints | RC, RC_FILE, INCLUDE, DEFINES, OUTPUT_NAME |
| Command inputs | RC_FILE, INCLUDE, DEFINES, OUTPUT_NAME |
| Outputs | RES and RES_NAME |
If OUTPUT_NAME ends with .res, rc.exe receives an exact-match score hint.
mc.exe
mc.exe compiles Windows message files.
| Kind | Variables |
|---|---|
| Buildable when present | MC_FILE |
| Score hints | MC, MC_FILE, OUTPUT_BASENAME |
| Command inputs | MC_FILE, OUTPUT_BASENAME |
| Outputs | RC_FILE/RC_FILE_NAME, H_FILE/H_FILE_NAME |
Generated .bin message resources are secondary files referenced by RC_FILE. Their names and count depend on the contents of the message file, so they are not exported as a single BIN_FILE artifact.
inf2cat.exe and signtool.exe
The signing tool can sign binaries and optionally generate catalog files for driver packages. Signing modifies existing files in place and is not currently cacheable as a normal artifact-producing step.
| Kind | Variables |
|---|---|
| Buildable when present | PFX_FILE and SIGN_BINARY |
| Score hints | SIGN, PFX_FILE, PFX_PWD, SIGN_BINARY, INF_FILE, OUTPUT_NAME |
| Command inputs | PFX_FILE, PFX_PWD, SIGN_BINARY, INF_FILE, OUTPUT_NAME |
| Outputs | none |
INF_FILE is optional. If OUTPUT_NAME ends with .cat, signing receives an exact-match score hint for catalog generation. Catalog generation may create a .cat file on disk, but the signing tool does not currently return CAT_FILE in the target output context.
C3 toolchain
The C3 toolchain is represented by registry entries whose tool_type is c3c. Nobs uses direct c3c file commands rather than generating a C3 project.json: compile for executables, compile-test for test executables, static-lib for static libraries, and dynamic-lib for dynamic libraries.
Registry and discovery variables
| Variable | Description |
|---|---|
tool_type | Must be c3c for C3 registry tools. |
toolchain_path | Path to a C3 install root, a directory containing c3c, or the c3c binary itself. |
toolchain_version | Requested or detected C3 compiler version. |
toolchain_min_version | Minimum acceptable C3 compiler version. |
Default detection stores toolchain_path and toolchain_version in the registry.
Target variables
| Kind | Variables |
|---|---|
| Buildable when present | C3_FILES, C3C_FILES, or C3_TEST_FILES |
| Score hints | C3C, EXE, STATIC, DLL, SO, C3C_FLAGS, C3_FILES, C3C_FILES, C3_TEST_FILES, LIB_PATH, LIBPATH, LIB, LINK_FLAGS, OUTPUT_NAME |
| Command inputs | C3C_FLAGS, C3_FILES, C3C_FILES, C3_TEST_FILES, LIB_PATH, LIBPATH, LIB, LINK_FLAGS, OUTPUT_NAME |
| Outputs | Always BINARY and BINARY_NAME; also EXE/EXE_NAME, LIB/LIB_NAME, DLL/DLL_NAME, or SO/SO_NAME according to the selected output kind |
EXE, STATIC, DLL, and SO select the C3 command. If no output selector is present, EXE is assumed. Only one output selector may be present in a target. C3_FILES is the preferred source-file variable; C3C_FILES remains supported as a compatibility alias. When C3_TEST_FILES is present, Nobs uses compile-test, emits an executable-style output, and passes C3_FILES, C3C_FILES, and C3_TEST_FILES to c3c. C3_TEST_FILES cannot be combined with STATIC, DLL, or SO.
For C3 targets, OUTPUT_NAME is used literally for the c3c -o argument and for output variables. Nobs does not append platform extensions such as .exe, .lib, .dll, .so, or .a.
LIB_PATH and the existing Nobs spelling LIBPATH are emitted as -L <path> linker search paths. LIB values are emitted as -l <library> libraries. LINK_FLAGS values are passed to the linker with -z <argument>.
app:
{
C3C
EXE
C3_FILES = [P"(src/main.c3)", P"(src/platform.c3)"]
C3C_FLAGS += ["-O0"]
LIB_PATH += P"(vendor/lib)"
LIB += "vendor"
LINK_FLAGS += ["stack=1048576"]
}
tests:
{
C3C
C3_FILES = [P"(src/lib.c3)", P"(src/platform.c3)"]
C3_TEST_FILES = [P"(test/lib_test.c3)"]
}
GNU toolchain
The GNU GCC toolchain is represented by registry entries whose tool_type is gnu-gcc. It creates tools for gcc, g++, ar, and ld from the selected GNU binary directory. It also creates an objcopy tool when that binary is available.
Registry and discovery variables
| Variable | Description |
|---|---|
tool_type | Must be gnu-gcc for GNU GCC registry tools. |
toolchain_path | Path to a GNU install root or directly to its bin directory. |
toolchain_version | Requested or detected exact GNU version. |
toolchain_min_version | Minimum acceptable GNU version. For example, 13 accepts 13, 13.1, and 15.1.1. |
prefix | GNU binary prefix/triplet, for example arm-none-eabi or x86_64-redhat-linux. |
arch | Parsed or requested GNU triplet architecture, for example x86_64, aarch64, arm, or riscv64. |
os | Parsed or requested OS part of the triplet. Plain Linux compiler names default to linux. |
abi | Parsed or requested ABI part of the triplet. |
Default detection stores toolchain_path, toolchain_version, prefix, arch, os, and abi in the registry.
Target selection variables
| Variable | Description |
|---|---|
GNU | Score hint for GNU matching. No value is required. |
ARCH | Requires a matching GNU architecture. |
TOOLCHAIN_TRIPLET | Requires a matching GNU binary prefix/triplet. |
toolchain_version | Requires an exact matching GNU version, or one of a list of exact allowed versions. |
toolchain_min_version | Requires at least the requested GNU version. |
C_FILES, CPP_FILES, OBJS | Main input variables that make concrete GNU tools buildable. |
Available tools
gcc
| Kind | Variables |
|---|---|
| Buildable when present | C_FILES or OBJS |
| Score hints | GCC, EXE, C_FLAGS, C_FILES, C_DEFINES, INCLUDE, LIB, OUTPUT_NAME, OBJS, LD_SCRIPT, LIBPATH, LINK_FLAGS |
| Command inputs | C_FLAGS, C_FILES, C_DEFINES, INCLUDE, OBJS, LIB, OUTPUT_NAME, LD_SCRIPT, LIBPATH, LINK_FLAGS |
| Outputs | OBJS when -c is present in C_FLAGS; otherwise BINARY and BINARY_NAME |
g++
| Kind | Variables |
|---|---|
| Buildable when present | C_FILES, CPP_FILES, or OBJS |
| Score hints | GPP, EXE, C_FLAGS, C_FILES, CPP_FILES, C_DEFINES, INCLUDE, LIB, OUTPUT_NAME, OBJS, LD_SCRIPT, LIBPATH, LINK_FLAGS |
| Command inputs | C_FLAGS, C_FILES, CPP_FILES, C_DEFINES, INCLUDE, OBJS, LIB, OUTPUT_NAME, LD_SCRIPT, LIBPATH, LINK_FLAGS |
| Outputs | OBJS when -c is present in C_FLAGS; otherwise BINARY and BINARY_NAME |
For gcc and g++, C_DEFINES values are prefixed with -D, INCLUDE values with -I, and LD_SCRIPT values with -T. When linking through the compiler driver, LIBPATH is emitted as -Wl,-L... and LINK_FLAGS as -Wl,....
Write _FLAGS variables as lists, with one compiler or linker option per item:
C_FLAGS += ["-O2", "-g"]
LINK_FLAGS += ["--gc-sections", "--print-memory-usage"]
ar
| Kind | Variables |
|---|---|
| Buildable when present | OBJS |
| Score hints | STATIC, AR, LIB, OBJS, OUTPUT_NAME |
| Command inputs | OBJS, OUTPUT_NAME |
| Outputs | LIB and LIB_NAME |
If OUTPUT_NAME ends with .a, ar receives an exact-match score hint.
ld
| Kind | Variables |
|---|---|
| Buildable when present | OBJS |
| Score hints | LD, SO, KO, SHARED, OBJS, LIB, LIBPATH, LINK_FLAGS, LD_SCRIPT |
| Command inputs | OBJS, LIB, LIBPATH, LINK_FLAGS, LD_SCRIPT, SO, SHARED, KO, OUTPUT_NAME |
| Outputs | Always BINARY and BINARY_NAME; also SO and SO_NAME for SO or SHARED |
KO changes the default output extension to .ko, but the current output context is still BINARY.
Write _FLAGS variables as lists, with one compiler or linker option per item:
LINK_FLAGS += ["--gc-sections", "--print-memory-usage"]
objcopy
| Kind | Variables |
|---|---|
| Buildable when present | INPUT |
| Score hints | OBJCOPY, INPUT, OBJCOPY_FLAGS, OUTPUT_NAME |
| Command inputs | INPUT, OBJCOPY_FLAGS, OUTPUT_NAME |
| Outputs | OUTPUT and OUTPUT_NAME |
OBJCOPY_FLAGS is a list whose items are appended to the command line in order. An item may contain an option together with its value, for example "-O binary".
firmware_bin:
{
OBJCOPY
INPUT = firmware.binary
OBJCOPY_FLAGS = ["-O binary", "--strip-all"]
OUTPUT_NAME = "firmware.bin"
}
Common C/C++ builder variables
The MSVS and GNU compiler/linker builders share these output-path variables through the common C builder layer:
[!IMPORTANT] List-like variables such as
C_DEFINES,C_FLAGS,INCLUDE,LIB, andLINK_FLAGSshould be appended as lists, even for a single item. See Append assignment.
Artifact output variables such as BINARY, LIB, DLL, EXE, SYS, SO, OUTPUT, RES, RC_FILE, and H_FILE are path values. When a builder emits one of these artifact variables, it also emits a sibling *_NAME variable containing only the artifact file name, for example BINARY_NAME = "app.exe".
| Variable | Description |
|---|---|
OUTPUT_NAME | File name for the primary artifact. It does not select a tool by itself. Some tools have extension-specific disambiguation rules for non-executable outputs. |
OUTPUT_DIR | Explicit output directory for the current target. If set, it replaces the generated default output directory. |
OUTPUT_DEF_DIR | Base directory used when OUTPUT_DIR is not set. The host context defaults it to out. |
PROJECT_ROOT | Project root used to resolve the generated output directory. |
PROFILE | Selected explicit profile name. It is added only when a profile is selected on the command line. |
If OUTPUT_DIR is not set, the generated output directory is based on PROJECT_ROOT / OUTPUT_DEF_DIR / <platform-triplet> / <PROFILE> / <namespace> / <unit> / <target>. With no explicit profile, the PROFILE segment is empty.
Prefer setting OUTPUT_DIR only in the most concrete target that needs a custom artifact location, not in a profile.
Build cache
Nobs keeps a per-project build cache in .nobs_cache under the project root. The cache is loaded before a normal build and written back after the build finishes.
The cache stores the recipe plan and output context for buildable targets. It is not a content-addressed object cache. On the next build, Nobs recreates the current recipes and compares them with the cached recipes. A recipe can be reused only when its inputs, optional intermediate assignments, outputs, and cached environment still match.
Nobs distinguishes value equality from file freshness. All recipe assignments are stored and compared as values, so changing a normal variable such as C_FLAGS, LINK_FLAGS, OUTPUT_NAME, LIB, or SUBSYSTEM invalidates the cached recipe. File timestamp checks, however, are performed only for path values written as P"(...)".
Plain strings are not treated as files for cache freshness. For example, "foo.lib" is compared as a string value, while P"(foo.lib)" is also checked as a filesystem path. Lists are walked recursively, so [P"(a.c)", P"(b.c)"] contributes both files to timestamp validation.
For a cached recipe to be fresh, all output path values must exist, and every input path value must be older than the oldest output path value. If an output is missing, an input path is missing, an input path is newer than the outputs, or the recipe no longer matches the current plan, the recipe is rebuilt. When a target contains multiple recipes, Nobs can rebuild only the invalid recipes.
Targets whose selected builder does not produce recipes are not cached as normal artifact-producing steps. Tools that modify existing files in place, such as signing, may also be unsuitable for normal recipe caching.
.nobs_cache is generated state. Do not edit it manually. If it becomes stale or corrupted, delete it and run the build again; Nobs will recreate it.
Targets without a build tool
A target that intentionally has no build tool should declare NO_TOOL directly in its target body. This is useful for aggregation or metadata-only targets that only provide output variables for dependent targets.
If a target does not select any builder and does not match NO_TOOL, Nobs reports an error instead of treating the target as a successful no-op. This helps catch projects that require a tool which is not installed or registered on the current system.
Command tools
Registry variables
| Variable | Description |
|---|---|
cmdline | Command to run in the default system shell. It can be a string or a list. When it is a list, each item is run as a separate command. |
pwsh | Windows-only command to run through powershell.exe. It can be a string or a list. When it is a list, each item is run as a separate PowerShell command. |
cwd | Optional working directory used for every cmdline or pwsh command. |
cmdline and pwsh list items do not share shell state. For example, a standalone cd temp item does not change the working directory of the next item. Use cwd for a common working directory, or put dependent shell operations in the same command, such as cd temp && tool ....
Custom registry tools can add additional required input variables with !. Output variables should be marked with ^; these are returned as the target output context.
Host context
| Variable name | Description |
|---|---|
| YEAR | Current year |
| MONTH | Current month |
| DAY | Current day |
| PROJECT_ROOT | Directory in which .nobs_project.txt was found. |
| OUTPUT_DEF_DIR | Default output directory. If unchanged, default is “out/”. |
| CWD | Current working directory. |
| OS_FAMILY | “windows” (if C:\ exists) or “unix” (if /bin exists) |
Supported compilers
Compiler discovery is registry based. Running nobs --find-compilers detects available toolchains and writes them to .nobs_registry.txt. Normal builds then use the registry entries.
Windows
MSVS
Default detection searches:
C:\Program Files\Microsoft Visual Studio\C:\Program Files (x86)\Microsoft Visual Studio\
It walks Visual Studio version directories, edition directories, and VC\Tools\MSVC\* toolset directories. Each discovered target architecture can become a registered msvc tool entry.
Windows SDK discovery searches:
C:\Program Files\Windows Kits\C:\Program Files (x86)\Windows Kits\
Use toolset_path for a direct toolset path containing bin, include, and lib. Use vs_path for a Visual Studio root to search. Use sdk_path when the Windows SDK is outside the default Windows Kits locations.
GNU
Default detection searches known Windows GNU install roots:
C:\Program Files (x86)\GNU Arm Embedded Toolchain\
The GNU crawler expects a toolchain_path and accepts either an install root, its bin directory, or a higher-level directory containing toolchain installs. It looks for bin and usr/bin directories, then recursively scans child bin directories. It accepts compiler names such as gcc, cc, g++, c++, and prefixed variants such as arm-none-eabi-gcc or x86_64-redhat-linux-gcc. It reads -dumpmachine, -dumpfullversion, and -dumpversion when available, then creates gnu-gcc tool entries.
When a GNU tool runs, its detected binary directory is prepended to PATH for that child process only. This lets compiler drivers find matching binutils and portable-installation aliases without changing the environment of Nobs or concurrently running commands from other toolchains.
C3
--find-compilers does not search default C3 install locations. Add a c3c registry tool entry manually and set toolchain_path to the C3 install root, a directory containing c3c, or the c3c binary itself.
Unix
C3
--find-compilers does not search default C3 install locations. Add a c3c registry tool entry manually and set toolchain_path to the C3 install root, a directory containing c3c, or the c3c binary itself.
GNU
Default detection searches common Unix GNU locations:
/usr/bin/usr/local/bin/opt/rh/opt
The same GNU crawler is used on Unix. It handles system compiler installs from distributions such as Ubuntu and Arch, layouts such as Software Collections under /opt/rh, where root/bin may be a symlink to root/usr/bin, and custom toolchains under /opt. It deduplicates resolved bin directories and compiler symlinks before recording prefix, arch, os, abi, toolchain_version, and toolchain_path.
Set toolchain_path in a registry tool entry. You can further restrict discovery with prefix, arch, os, abi, and toolchain_version.
Manual compiler registry entries
If a compiler is installed in a location that nobs --find-compilers does not search, add its tool entry manually to the tools block in .nobs_registry.txt. Adjust the paths and versions to match the local installation. For example:
tools {
raylib_gcc {
tool_type = "gnu-gcc"
toolchain_version = "15.2.0"
arch = "x86_64"
toolchain_path = "C:\raylib"
}
c3c {
arch = "x64"
tool_type = "c3c"
toolchain_path = "D:\c3"
}
default_gnu_arm_10_3_1 {
tool_type = "gnu-gcc"
toolchain_version = "10.3.1"
prefix = "arm-none-eabi"
arch = "arm"
os = "none"
abi = "eabi"
toolchain_path = "C:\Program Files (x86)\GNU Arm Embedded Toolchain\10 2021.10\bin"
}
default_msvs_x64_14_43_34808 {
tool_type = "msvc"
toolset_version = "14.43.34808"
visual_studio_version = "2022"
toolchain_edition = "BuildTools"
arch = "x64"
vs_path = "C:\Program Files (x86)\Microsoft Visual Studio\"
toolset_path = "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.43.34808"
}
}
Common workflows
Platform profiles
Platform-specific builds should be selected through project profiles. A profile normally sets a platform variable such as TARGET_PLATFORM, and build definitions use that variable to select platform source directories.
profiles {
win64_debug {
TARGET_PLATFORM = "win64"
C_FLAGS += ["/Od", "/Zi"]
}
rhel_debug {
TARGET_PLATFORM = "unix"
C_FLAGS += ["-O0", "-g"]
}
}
Targets can then reference platform-specific implementation files without preprocessor branching in the common build definition:
core:
{
CPP_FILES = [
P"(plat/${TARGET_PLATFORM}/util.cpp)",
P"(plat/${TARGET_PLATFORM}/builders/*.cpp)",
P"(builders/nobs_core_builders.cpp)"
]
}
For project-owned code, prefer this profile-and-directory split over adding new #ifdef blocks to common sources. If a common entry point is needed, declare a shared interface and provide one implementation per plat/<platform> directory.
Release / debug variants
Release and debug builds are also profile variants. When a project has more than one platform, name profiles by the whole output flavour instead of separating platform and optimization settings into targets:
profiles {
win64_debug {
TARGET_PLATFORM = "win64"
C_FLAGS += ["/Od", "/Zi"]
}
win64_release {
TARGET_PLATFORM = "win64"
C_FLAGS += ["/O2"]
}
unix_debug {
TARGET_PLATFORM = "unix"
C_FLAGS += ["-O0", "-g"]
}
unix_release {
TARGET_PLATFORM = "unix"
C_FLAGS += ["-O2"]
}
}
Select a profile on the command line, for example nobs win64_debug or nobs unix_release.
Building a simple binary
mybinary:
{
CPP_FILES=P"(main.cpp)"
}
Building a static library
Use STATIC to select the static-library builder explicitly. Keep compilation in a separate target that produces OBJS, then merge those objects into the library target with +core.
mylib: +core +api
{
STATIC
OBJS
OUTPUT_NAME="mylib.lib"
^INCLUDE
}
api:
{
NO_TOOL
^INCLUDE = P"(.)"
}
core:
{
C_FILES=[P"(mylib.c)", P"(util.c)"]
C_FLAGS=["/c"]
}
For GNU, use the same shape with OUTPUT_NAME="libmylib.a" and C_FLAGS=["-c"].
Resulting output context:
{
LIB=P"(/path/to/mylib.lib)"
INCLUDE=P"(/path/to/nobs_txt/.)"
}
In the above example STATIC selects lib.exe on MSVC or ar on GNU, and OUTPUT_NAME names the archive. OBJS is declared in mylib so the builder can be selected before dependencies are built. core then produces the actual OBJS, which are added to mylib because of the + modifier on core. The library builder emits LIB, and ^INCLUDE exports the public include path from api.
Building a DLL
You can build a DLL like this:
dll: +core api
{
DLL
OUTPUT_NAME="mydll.dll"
OBJS
LIB = "kernel32.lib"
DEF = P"(mylib.def)"
^API=api.INCLUDE
}
api:
{
NO_TOOL
^INCLUDE=P"(api)"
}
core:
{
C_FILES=[
P"(dllmain.c)",
P"(functions.c)"
]
C_FLAGS= ["/c"]
INCLUDE=P"(include)"
}
Note the output of core is OBJS, because that is the behaviour of the cl.exe tool when /c is present (see MSVS toolchain / Available tools / cl.exe). The dll target declares OBJS so the linker builder can be selected before dependencies are built. core is referenced with a + modifier, which means the actual OBJS produced by core are moved straight into the dll context and used in the linking process.
The api target declares NO_TOOL because it is metadata-only; it exports include paths but does not compile or link anything. When the MSVC linker produces an import library next to the DLL, the DLL target output also includes LIB pointing to that import library, so dependent targets can link against it through the usual LIB variable.
Value lookups
Something very useful for embedded are value lookups.
fw: cpu
{
CPU = "CM0PLUS"
C_FLAGS = cpu.gcc_flags[CPU]
C_FILES = P"(main.c)"
}
cpu:
{
NO_TOOL
^gcc_flags =
{
CM0PLUS=["-mthumb", "-mcpu=cortex-m0plus"]
CM3=["-mthumb", "-mcpu=cortex-m3"]
}
}
After cpu target is built, it exposes gcc_flags in its output. The cpu target declares NO_TOOL because it is only a lookup table and should not select a compiler. The output is then added to fw as new context variable under the name ‘cpu’. Contexts can be either accessed with dotted notation or it can be indexed with [], which is the same but the inside of [] is resolved beforehand. In the example above the access resolves to cpu.gcc_flags.CM0PLUS. The content of that variable is used for compiling main.c.
Subdirectories / multiple modules
In order to connect multiple nobs.txt files you need to use .nobs_project.txt. This file can be created automatically with nobs –create-project ran in root directory of your project. To avoid typing out paths, first create modules’ directories and their nobs files and then run nobs –create-project. Once project file exists you can start using qualified target names for dependencies. E.g. take a structure like this:
myapp_proj
│─── .nobs_project.txt
│
├─── myapp
│ ├── main.c
│ ├── nobs.txt
│ │
│ └── include
│ └── globals.h
│
├─── mylib
│ ├── func.c
│ ├── nobs.txt
│ │
│ └── include
│ └── mylib.h
│
└─── common
├── nobs.txt
│
└── include
└── common.h
This project builds a static library and an application that uses it. It also has a shared common unit that exposes include paths used by both modules. Targets from mylib and common are referenced using the qualified notation unit::target. Aside from that, working with these targets is no different from using local targets in their short form.
unit myapp
build: mylib::api common::include +mylib::build
{
OUTPUT_NAME="myapp.exe"
INCLUDE = [
P"(include)",
mylib.api.INCLUDE,
common.include.INCLUDE
]
C_FILES=P"(main.c)"
}
The build of the lib could look like:
unit mylib
build: common::include +core
{
STATIC
OBJS
OUTPUT_NAME="mylib.lib"
INCLUDE = common.include.INCLUDE
}
api:
{
NO_TOOL
^INCLUDE=P"(include)"
}
core:
{
C_FILES=P"(func.c)"
C_FLAGS=["/c"]
}
The shared common unit only exports include paths:
unit common
include:
{
NO_TOOL
^INCLUDE=P"(include)"
}
The project file generated with nobs –create-project should look like:
name nobs_proj
units
{
myapp {
"myapp\nobs.txt"
}
mylib {
"mylib\nobs.txt"
}
common {
"common\nobs.txt"
}
}
Running nobs at root directory will try to build the main target of the main unit, which equals to first target of first unit. That means nobs will start at myapp::build. common::include exposes a shared INCLUDE variable, available as common.include.INCLUDE in dependent targets. mylib::build uses STATIC to select the static-library builder and declares OBJS so it can be selected before core is built; core then supplies the actual object files through the +core dependency. When mylib::build is done it will have a LIB variable pointing to where the library was built in its output context (See MSVS toolchain / Available tools / lib.exe). Because there is a + on mylib::build in the app target, this variable will be moved straight into myapp::build where in turn it will be used in the linking step (See MSVS toolchain / Available tools / cl.exe).
External libraries
There isn’t much special about libraries being 3d party from using your own built libraries. To use a library in linking you need to use LIB variable in a context that is either built by cl.exe, link.exe, gcc or g++. For instance:
myapp:
{
C_FILES=P"(main.c)"
LIB=["kernel32.lib", "advapi32.lib"]
}
This will produce a build line like this:
cl.exe "E:\long path\to\myapp\main.c" "kernel32.lib" "advapi32.lib"
Command-line interface
nobs is controlled primarily through the command line. A build is defined by selecting an optional target, an optional project profile, and optional context variables as assignments.
Basic usage
nobs [options] [target] [profile] [assignment]*
Non-option arguments are evaluated one by one:
target
Optional target name to build.
If omitted, Nobs builds the main target of the main unit. The main unit is the first unit in.nobs_project.txt; the main target is the first target in that unit. Qualified target names such asunit::targetorproject/unit::targetcan be used when a project file is active.profileOptional name of a profile from.nobs_project.txt. If adefaultprofile exists, it is applied automatically. A selected profile is applied afterdefault. Only one explicit profile can be selected.assignment
Zero or more variable assignments passed on the command line.
These assignments are injected into the implicit context and behave the same way as assignments in a nobs file.
The order of target, profile, and assignments is flexible, but only one target and one explicit profile can be specified. If a token matches a target name, it is treated as the target before profile lookup is attempted.
The implicit context is then assembled as host variables, default profile, selected profile, and command-line assignments. Command-line assignments are applied last, so they can override or extend profile values.
Example:
nobs app debug USE_UNICODE DEFAULT_LANG=en
Options
--help
Displays a short help message describing available options and usage.
nobs --help
-l — list targets
Lists all targets defined in the selected unit. The unit argument is required.
nobs -l <unit>
Use 1 as an alias for the main unit, which is the first unit listed in .nobs_project.txt:
nobs -l 1
When a unit is defined by multiple files, targets from all of those files are listed. Imported project units are not included. Without a project file, 1 selects the unit from the default nobs.txt file or the standalone file selected with -f. The main target of the selected unit is highlighted in green when output is written to a terminal.
This can be useful for CI/CD integrations. You can list targets, then insert steps into pipeline based on the list.
This option does not perform any build.
-u — list units
Lists all units defined in the project file.
nobs -u
This is useful for inspecting project structure and available build modules. The output is an inventory of known units, not a contract for default build order. Use nobs -l from the project root to confirm which targets belong to the project main unit.
-p — list profiles
Lists all profiles defined in the project file.
nobs -p
This option does not perform any build.
-j <num> — thread count
Sets the maximum number of parallel jobs.
nobs -j 8
If -j is used without a number, Nobs uses the hardware concurrency reported by the host system. Values below 1 are clamped to 1.
-v <num> — verbosity level
Sets the verbosity level.
nobs -v 3
Default verbosity level is
5(Info).-vwithout a number sets verbosity to3(Verbose).Lower values produce more detailed output.
Higher values reduce console noise.
-d — dump implicit context
Prints the raw implicit context before the build starts.
nobs -d app debug
This is useful when checking which host variables, default profile variables, selected profile variables, and command-line assignments are copied toward targets. Conditional blocks in the implicit context are not evaluated by this dump; they are evaluated later in each target context. Use verbose output (-v or -v 3) when you need to inspect the evaluated target input context used by the selected builder.
-f <file> — nobs file override
Overrides the default nobs file.
nobs -f custom.nobs
The file path can be absolute or relative to the directory where nobs is launched. When -f is used, Nobs runs that file as a standalone entry file and does not load a project file discovered from the current directory.
-o <file> — main target output
Writes the main target output context to a text file. This can be useful for CI/CD integrations that need to read artifact paths produced by the build.
nobs app -o build-output.txt
This option does not set the built artifact name. Use OUTPUT_NAME for that.
-r — register current project
Registers the current project in the user registry.
nobs -r
This option does not perform a build.
--find-compilers
Detects available default toolchains and writes them to the machine-local .nobs_registry.txt.
nobs --find-compilers
Run this when setting up a machine or after installing a new compiler/toolchain that Nobs should discover automatically.
--version
Prints the current version of nobs.
nobs --version
--create-project
Initializes a new .nobs_project.txt file in the current directory.
nobs --create-project
Run this after creating module directories and their nobs.txt files. Nobs scans the current directory tree and creates the units section from nobs.txt files whose first line declares unit <name>. Files without a unit declaration are skipped.
Command-line assignments
Assignments passed on the command line behave the same as assignments defined in a nobs file.
nobs app release msvc 'VERSION="1.0"'
These assignments are merged into the implicit context so they can influence registered tool selection, builder choice, and target behavior. Each assignment must be passed as one command-line argument. Quote the whole assignment when the value contains spaces or list syntax, for example:
nobs app 'C_FLAGS+=["/Od", "/Zi"]'
Quotes must survive the shell. Nobs parses the argument exactly as it receives it from the operating system. In many shells, writing VERSION="1.0" removes the quotes before Nobs sees the argument, so Nobs receives VERSION=1.0. Quote or escape the whole assignment in a shell-specific way, for example 'VERSION="1.0"' in PowerShell or POSIX shells.
Bare identifiers are converted to strings on the command line. VERSION=release is parsed as VERSION="release". In a nobs.txt file, release would be an identifier reference, but command-line identifiers are converted to strings because command lines and CI/CD systems often remove quotes and identifier references are rarely useful there.
Numbers stay numbers unless quoted. VERSION=1 is parsed as an integer number, while VERSION="1" is parsed as a string. 1.0 is not a valid Nobs number literal because decimal numbers are not part of the assignment syntax. Use a string for version-like values: VERSION="1.0".