Variant builds¶
Many projects need to maintain one set of requirements that describes several product variants — different hardware platforms, customer editions, build configurations, or deployment targets — and then produce a tailored output for each one.
ubCode supports this through what is often called a “150% model”: a single source of truth that contains the union of all variants (the 150%), from which each build selects the relevant 100%. Instead of copying documents per variant (and keeping the copies painfully in sync), you keep one model and let the build context decide which values, links, and content apply.
See also
variants-demo is a small, runnable project that demonstrates the workflow described here.
The demo also uses the if directive for conditional compilation (including or excluding whole content blocks per variant), documented below.
The build context¶
A “build” is described by two pieces of configuration:
build_tagsA list of tags describing the current target (for example the builder or environment). These mirror Sphinx’s tags and are available as the
build_tagsvariable in filter expressions.- Variant data
A nested, read-only key-value store exposed under the
var.*namespace, holding the parameters of the current variant (platform, architecture, enabled features, …).
Both can be set in ubproject.toml and overridden per build
(see Producing a build per variant below),
so the same model resolves differently depending on the target.
Four building blocks¶
Variant builds combine four mechanisms.
They share the same filter expression language
(see Writing a filter) and the same var.* / build_tags context.
1. Variant data and var.* filtering¶
The variant’s parameters live in variant data, declared inline or loaded from a JSON file:
build_tags = ["html"]
[needs.variant_data]
platform = "arm"
archs = ["arm", "x86"]
[needs.variant_data.build]
compiler = "clang"
features = ["networking", "logging"]
The var.* namespace can then be used in any filter —
needextend and needimport directives, external_needs filters,
conditional defaults (predicates), and the filters inside variant functions:
.. needextend:: var.platform == "windows"
:status: supported
.. needimport:: shared.json
:filter: "arm" in var.archs
Because var.* is global to the build (it does not depend on the current
need), it is ideal for build-wide switches.
2. Injecting values with <{ ... }>¶
A variant data reference
substitutes a var.* value directly into a field or link value.
The field (or link) must opt in with parse_variants = true:
[needs.fields.arch]
schema = {type = "string"}
parse_variants = true
.. req:: Bootloader
:id: REQ_001
:arch: <{ var.platform }>
.. req:: Build banner
:id: REQ_002
:arch: built for <{ var.platform }>
This is a plain lookup — there is no condition, just a value taken from the current variant. See Referencing variant data in field values (<{ ... }>) for the embedding rules, type-checking, and the diagnostics emitted for unknown keys or type mismatches.
3. Choosing values with <<...>>¶
A variant function selects between candidate values using conditional logic. The first matching expression wins; the final comma-separated value is the fallback:
.. req:: Power management
:id: REQ_003
:status: <<[var.platform == "windows"]: active, inactive>>
:priority: <<['html' in build_tags]: web_critical, medium>>
Unlike <{ ... }> (a direct substitution),
<<...>> evaluates one or more filter expressions
and picks the corresponding value.
4. Conditional content with the if directive¶
Where <{ ... }> and <<...>> choose a value, the if directive
(matching Sphinx-Needs 8.2’s if directive)
includes or excludes a whole block of content — paragraphs, needs, tables,
anything — based on a var.* expression.
Its argument is a filter expression evaluated against the current variant;
a truthy result keeps the body, a falsy result skips it entirely:
.. if:: var.platform == "windows"
.. req:: Windows power management
:id: REQ_WIN_PM
This requirement only exists in the Windows variant.
.. if:: "arm" in var.archs
This paragraph, and any needs it contains, appear only when the
``arm`` architecture is enabled.
When the condition is truthy, the body is treated exactly as if the if
wrapper were not there — a need inside it is collected and indexed normally.
When the condition is falsy, the body is skipped completely:
its needs are not collected, and any targets or references inside it do
not resolve — matching Sphinx-Needs.
Supported expression forms¶
The condition uses the same filter language as everything else (see Writing a filter). The supported forms — all matching Python’s semantics, which is what Sphinx-Needs evaluates with — are:
comparisons against literals or other
var.*fields:==,!=,<,<=,>,>=(e.g.var.opt_level >= 2,var.platform == "windows")truthiness of a bare field (
var.debug—0,""and[]are falsy, as in Python)nested attribute access (
var.build.compiler == "clang")membership:
"x" in var.features,var.platform in ["arm", "x86"], andnot inis None/is not Noneboolean combinators
and/or/not, with parenthesesthe string methods
.upper(),.lower(),.startswith("…"),.endswith("…")
Anything else is an error, and the body is excluded, reported as an
if.invalid_expression diagnostic. That covers:
an unknown
var.*key, a syntax error, or a type-mismatched operation (Sphinx-Needs also warns and excludes for these);the filter-language extensions that only make sense for need filters —
len(...),search(...)— which Sphinx-Needs would also reject in anifcondition (it evaluates with all Python builtins removed, solenraisesNameError);Python forms outside the list above (bare literals like
True, chained comparisons like1 < x < 5, arithmetic, indexing, …). Note this last group is where ubCode is deliberately stricter than Sphinx-Needs: its full-Pythonevalwould evaluate them, ubCode reports them loudly rather than risk a silent misreading — keep conditions to the supported forms for results that are identical under both tools.
Tip
In the editor and preview, a falsy if block is not hidden: the
RST preview renders it as a collapsed,
greyed block labelled with its condition, and the editor fades the
inactive source region (like #if-disabled code in a C editor).
This is a deliberate difference from a Sphinx-Needs build, which omits the
content entirely — the previewer is a development tool, so seeing that
inactive content exists (and why) is useful.
The built output (needs.json, the index) still excludes it exactly as
Sphinx-Needs does.
Producing a build per variant¶
The point of a 150% model is to build it more than once, once per variant, by swapping the build context.
Keep the shared model in your sources
and the per-variant parameters in separate files,
for example variants1.json and variants2.json:
{"platform": "arm", "build": {"compiler": "clang"}}
{"platform": "x86", "build": {"compiler": "gcc"}}
With ubc, select the file with a -c/--config override
(repeatable, accepts any ubproject.toml snippet):
# Default build (uses variant_data / variant_data_file from ubproject.toml)
ubc build needs --pretty --output needs.arm.json
# Build the second variant by overriding the data file
ubc build needs --pretty --output needs.x86.json \
-c "needs.variant_data_file = 'variants2.json'"
The same override works for ubc build index and other commands.
Individual values can be overridden directly too,
using TOML dotted-key syntax —
-c "needs.variant_data.platform = 'x86'" —
and -c may be repeated to apply several overrides.
When building with Sphinx-Needs itself,
pass the equivalent Sphinx config value with -D:
sphinx-build -E . _build -D needs_variant_data_file=variants2.json
Comparing variants¶
Because each variant is just a different build context, you can use ubc diff to see exactly how the resolved needs differ between two variants.
The --config mode of ubc diff compares the current project
against the same project with a configuration override applied —
perfect for diffing one variant against another:
# Compare the default variant against variants2.json
ubc diff -c "needs.variant_data_file = 'variants2.json'"
The output reports which needs changed — added, removed, or modified fields and links — making it easy to review the impact of a variant or to catch unintended differences between targets.
See also¶
Variant data — full reference for
variant_data,variant_data_file, thevar.*namespace, and<{ ... }>references.Variants — named variants and
<<...>>functions.Variants — what the preview renders for the
ifdirective, thevariantrole, and variant field values.Writing a filter — the filter expression language used throughout.
Diff & Impact Analysis — diff and impact analysis.