# Nobs manual

## Installation

Nobs doesn't require installation setup other than being available in your PATH. Before the first normal build, run:

```sh
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](https://nobs.build/help/getting-started).

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:

```make
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 `default` project profile, if the project defines one

- Variables 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.*

```make
target: dependency
{
    cmdline = "echo Hello ${dependency.VAR1}"
}

dependency:
{
    NO_TOOL
    ^VAR1 = "World"
}
```



Targets can have parameters, which are passed to dependencies as variables.

```make
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:

```make
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:

```make
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:

```make
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"(.)"
}
```

<details>
<summary>Where are the object files?</summary>
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.
</details>

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.

```make
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

```c
// this is a line comment
```

#### Block comments

```c
/* 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:
  
  ```make
  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 `_FLAGS` are lists of tool arguments. For example, write `C_FLAGS += ["/Od", "/Zi"]` and `LINK_FLAGS += ["/DEBUG", "/OPT:REF"]`, not `C_FLAGS += "/Od /Zi"` or `LINK_FLAGS += "/DEBUG /OPT:REF"`.
  
  

##### String expansion

String expansion is performed in multiple phases and follows a deterministic order.

1. **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.

2. **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.

3. **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.

4. **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.  

```make
{} // 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**.

```make
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:

```make
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

```make
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:

```make
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:

```make
^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:

```make
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**

```make
cpu_gcc_flags =
{
  CM0PLUS=["-mthumb", "-mcpu=cortex-m0plus"]
  CM3=["-mthumb", "-mcpu=cortex-m3"]
}
CPU = "CM0PLUS"
C_FLAGS = cpu_gcc_flags[CPU]

```

* `CPU` resolves to `"CM0PLUS"`.

* `cpu_gcc_flags["CM0PLUS"]` is fetched.

* `C_FLAGS` is 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.

```make
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.

```make
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.

```make
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:

```make
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 `=>`:

```make
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:

```make
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:

```make
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**.

```make
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 `VARIABLE` exists 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:

```make
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.

```make
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:

```make
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 `=>`:

```make
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:

```make
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:

```make
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.

1. **Target context copy**  
   A mutable copy of the target’s context is created.  
   This ensures the original target definition remains unchanged.

2. **Implicit context injection**  
   An implicit context is merged into the target context.  
   This implicit context is composed of:
   
   * the host context,
   
   * the `default` profile, 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, `PROFILE` is also added to the implicit context.

3. **Parameter binding**  
   Target parameters are added to the context and become available as regular variables.

4. **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 target `if` block. Target conditions are evaluated after builder selection, so variables added only by such conditions cannot make a tool applicable for the target.

5. **Conditional evaluation**  
   All `if` statements are evaluated and applied to the context.

6. **Dependency resolution**  
   The dependency list is created:
   
   * dependency macros are expanded,
   
   * all dependencies are resolved into concrete target invocations.

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

8. **Context variable resolution**  
   All variables in the context are resolved and expanded.

9. **Build execution**  
   The selected builder is finally given the resolved context and performs the actual build.

10. **Output collection**  
    The builder output is merged with all variables marked as output (`^`).  
    The resulting context is returned as the output of `buildTarget()`.
    


### Nobs registry

Nobs keeps machine-local configuration in a registry file. On Windows the file is stored in:

```text
%APPDATA%\nobs\.nobs_registry.txt
```

On Unix-like systems it is stored in:

```text
~/.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:

```make
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:

```make
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 `!`:

```make
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`:

```make
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 as `msvc`, `gnu-gcc`, or `command`.

* 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:

* `gcc` is viable with `C_FILES` or `OBJS`

* `g++` is viable with `C_FILES`, `CPP_FILES`, or `OBJS`

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:

- `*.lib` can disambiguate MSVC static libraries toward `lib.exe`
- `*.res` can disambiguate Windows resources toward `rc.exe`
- `*.cat` can 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:

```make
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:

```make
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:

```make
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>`.

```make
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"]
}
```

```make
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:

```make
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:

```make
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"`.

```make
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`, and `LINK_FLAGS` should be appended as lists, even for a single item. See [Append assignment](#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:

```make
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.

```make
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:

```make
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:

```make
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

```make
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`.

```make
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:**

```make
{
    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:

```make
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.

```make
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:



<pre class="tree">
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
</pre>

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.

```make
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:

```make
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:

```make
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 as `unit::target` or `project/unit::target` can be used when a project file is active.

* `profile`
  Optional name of a profile from `.nobs_project.txt`.
  If a `default` profile exists, it is applied automatically. A selected profile is applied after `default`.
  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`).

* `-v` without a number sets verbosity to `3` (`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"`.

---
