Build Settings

Settings in the source, values per build

A program’s build-time configuration (which server a release talks to, a development build’s extra panels, a feature switch) is declared in its source with @setting, and each build picks the values with --set:

tape
import io from "io";

pub enum Build : u8 { Dev = 1; Beta = 2; Release = 3; }

@setting pub const BUILD: Build = Build.Dev;
@setting const SERVER: []const u8 = "local";
@setting const VERBOSE_SYNC: bool = false;

fn show_debug_panel() {
    io.println("debug panel");
}

fn main() -> i32 {
    if (BUILD == Build.Dev) { show_debug_panel(); }   // not in a Release build
    io.println("connecting to {SERVER}");
    return 0;
}
bash
tape run app.tape                                              # Dev, "local"
tape build app.tape -o sulle.exe --set BUILD=Release --set SERVER=chat.example.com

The source says which settings exist, their types and their defaults; the build only picks values. Without --set a setting is its default, so tape run, tape test, tape check and the editor see the first variant of an enum like Build: what a developer runs.

Declaring a setting

@setting goes on a module-level const, pub or not, in any module of the program (libraries and the standard library included). Its type is written and is one of:

Type--set value
booltrue or false
an integer type (i8 … u64)a decimal integer in the type’s range: PORT=8080, DELTA=-1
[]const u8any bytes: SERVER=chat.example.com
an enum (this module’s, or an imported one’s m.Kind)a variant’s name exactly as declared: BUILD=Release

The initializer is the default and is a compile-time constant. A @setting anywhere else (a local, a var, a struct field, a function) or of another type is a compile error.

Otherwise a setting is an ordinary constant. It is folded at compile time, the same in the VM and natively, and a branch its value decides is left out of the program: in a Release build show_debug_panel above is not reached, so it is not in the executable. It works in @tape code:

tape
@tape {
    if (BUILD != Build.Dev) {
        @emit fn edition() -> []const u8 { return "release"; }
    } else {
        @emit fn edition() -> []const u8 { return "development"; }
    }
}

and in the #resource region’s conditions, such as a release with no console window (@windowed):

tape
#resource
@windowed(BUILD != Build.Dev)

--set

--set NAME=VALUE is an option of tape run, tape build, tape test (passed on to each file’s VM and native runs) and tape check, as many times as there are settings to set.

  • NAME is a setting of the root file’s module by its name as declared: --set BUILD=Release.
  • Another module’s setting is module.NAME, the module named as its file is, the way import names it: --set brand.SERVER=prod, --set netstack.TRACE=true.

Every --set is checked before anything is compiled, so a typo is never a silent default:

plaintext
error: --set BUILD=Relase: Build has no variant 'Relase'; its variants are Dev, Beta, Release
error: --set VERBOSE_SYNC=yes: VERBOSE_SYNC is a bool: true or false
error: --set SRVER=x: the root file's module (app) declares no @setting SRVER; its settings are BUILD, SERVER, VERBOSE_SYNC
error: --set brnd.SERVER=prod: no module brnd in this build (a module is named as its file is, the way `import` names it)
error: --set BUILD is given twice (Beta and Release): one value per setting

What a build can see

Only @target and @program describe the build to its code, and settings are the program’s own. How a build is produced (-O0, --map, --no-dce, …) is not readable, so it never changes what the program does. Settings are inputs of the build like its sources and @embeded files: the same sources, files, flags and --set values give the same program. A value from outside, such as a CI build number or a commit hash, comes in through --set, or through a file the build writes and the program @embeds; compile-time code reads no environment variables.

A build script

A build script run with tape run build.tape starts the same compiler through the TAPE environment variable and passes the settings on:

tape
import os from "os";
import process from "process";

fn main() -> i32 {
    var c: process.Command = process.command(os.env("TAPE")!);
    c.args(["build", "app.tape", "-o", "out/sulle.exe", "--set", "BUILD=Release"]);
    let st: process.Status = c.run() or { return 1; };
    if (!st.success()) { return 1; }
    return 0;
}

Last modified: