Define
#target and #build answer questions the compiler already knows: which machine, which kind of build. A define is a question you answer yourself — a named value handed to the build, such as a feature switch, a customer's name or the address of a test server. The program reads it while compiling with #config, so a when can branch on it and keep only the code that applies.
Handing a value to the build
On the command line, --define takes a name and, optionally, a value. Repeat it for each define:
rux run --define Name=Ada --define Verbose
A define without a value, like Verbose here, holds the text "true". Defines that belong to the project rather than to one build go in a [Build.Defines] table in Rux.toml:
[Build.Defines]
Name = "Grace"
The two combine: the manifest gives the defaults, and --define overrides them for one build. With the table above, rux run greets Grace and rux run --define Name=Ada greets Ada. A manifest value may be a string, a boolean or an integer, but #config always hands it to the program as text — Retries = 3 reads as "3".
flowchart LR
m["Rux.toml<br/>[Build.Defines]"] --> merge{"the build's defines"}
c["--define Name=Value<br/>on the command line"] -- "overrides" --> merge
merge --> has["Has: is it<br/>defined at all?"]
merge --> get["Get: its text,<br/>or empty"]
has --> w["when picks the code<br/>while compiling"]
get --> wHas and Get
#config comes from Core and has two questions to ask:
#config.Has("Name")says whether the build definesNameat all.#config.Get("Name")gives its text, or an empty string when it is not defined.
Get alone cannot tell "not defined" from "defined as empty", so the program asks Has first:
when #config.Has("Name") {
let name = #config.Get("Name");
} else {
let name = "World";
}
PrintLine("Hello, {}!", name);
| How the build was started | Has("Name") | Get("Name") | Greeting |
|---|---|---|---|
rux run | false | "" | Hello, World! |
rux run --define Name=Ada | true | "Ada" | Hello, Ada! |
rux run --define Name= | true | "" | Hello, ! |
As in the When lesson, the when opens no scope, so name is still there for the PrintLine after it. The last line of the program shows Get on a name nobody defined: its length is 0.
A feature switch
The most common use of a define is to switch a feature on or off:
when #config.Has("Verbose") {
PrintLine("Verbose is on (its value is \"{}\")", #config.Get("Verbose"));
} else {
PrintLine("Verbose is off; rebuild with --define Verbose to turn it on");
}
The untaken branch is not part of the program at all — a build without Verbose contains no trace of the verbose code. The flip side is that a define is fixed once the program is built. Changing one means rebuilding: rux run does that for you, but an .exe you built earlier keeps the defines it was built with.
Because both questions are answered while compiling, the name must be written as a string literal in the source. Even a const holding the name is refused.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// `#target` and `#build` answer questions the compiler already knows. A define is a question you
// answer yourself: a named value handed to the build, such as a feature switch or a customer's
// name. Pass one on the command line with `--define Name=Value`, or list it in a `[Build.Defines]`
// table in `Rux.toml`. The program reads it with `#config`:
//
// - `#config.Has("Name")` says whether the build defines `Name` at all.
// - `#config.Get("Name")` gives its text, or an empty string when it is not defined.
//
// A define without a value, such as `--define Verbose`, holds the text "true". Both questions are
// answered while compiling, so `when` can branch on them, and changing a define means rebuilding.
// For the same reason the name must be written as a string literal: passing a variable is refused
// with "the argument to 'Has' must be a string literal written in the source".
import Core::{ #config };
import Io::PrintLine;
func Main() -> int {
// Has tells "not defined" apart from "defined as empty", which Get alone cannot.
when #config.Has("Name") {
let name = #config.Get("Name");
} else {
let name = "World";
}
PrintLine("Hello, {}!", name);
// A feature switch: the untaken branch is not part of the program at all.
when #config.Has("Verbose") {
PrintLine("Verbose is on (its value is \"{}\")", #config.Get("Verbose"));
} else {
PrintLine("Verbose is off; rebuild with --define Verbose to turn it on");
}
PrintLine("An undefined name reads as {} characters", #config.Get("Missing").length);
return 0;
}
Besides Io, its Rux.toml lists Core under [Dependencies].
Run it
cd Examples/CompileTime/Define
rux run
Hello, World!
Verbose is off; rebuild with --define Verbose to turn it on
An undefined name reads as 0 characters
rux run --define Name=Ada --define Verbose
Hello, Ada!
Verbose is on (its value is "true")
An undefined name reads as 0 characters
Common mistakes
#config.Has(key) fails with error: the argument to 'Has' must be a string literal written in the source, and so does a constant. Inside a when condition the same mistake reads error: 'key' is not a compile-time constant. Write the name in quotes.Get.An empty answer from
Get means either "not defined" or "defined as empty". Ask Has when the difference matters.A define is folded in while compiling. Running an old
.exe with a new --define in mind changes nothing; build again.Try it yourself
- Run the program with
--define Name=Ada --define Verbose, then with--define Name=alone. Explain the second greeting. - Add a
[Build.Defines]table toRux.tomlwithName = "Grace". Run with and without--define Name=Ada. - Add a
when #config.Get("Name") == "Ada"that prints an extra line just for Ada.
Learn more
- Build context in the Rux Reference —
#configand the rest of the build context #configin the Core API reference- Package manifest — the
[Build.Defines]table rux runandrux build— the--defineoption- Build mode — the other switch every build has