Rave Build System
General
Rave files are parsed line-by-line with each line starting with a command, followed by zero or more arguments, and then an optional block, for example:
target MyTarget {
sources main.cc
executable a.out
}
This will create ninja.build
that compiles main.cc
and links it as a.out
and then two additional rules to sign and run a.out
, these will be MyTarget
and MyTarget/run
respectively.
Variables
We can assign to variables with the following three commands:
set «name» «value»
add «name» «value»
capture «name» «command»
For example:
set CS_FLAGS "--force --options runtime"
add CS_FLAGS "--timestamp=none"
capture VERSION "grep -om1 '^## .* (v.*)$' Changes.md|sed 's/.*(v\\(.*\))/\\1/'"
First command sets the CS_FLAGS
variable, the second one appends to it.
The third one will run «command»
as a shell command and assign the output to «name»
.
Note though that this command will only run when generating build.ninja
, so it will only be re-run if build.ninja
needs to be regenerated.
If «value»
is an existing file relative to the current working directory, any build rule that uses this variable (directly or indirectly) will be set to depend on the file.
For example we may have a structure like this:
# ./default.rave
load app/*/*.rave
# ./app/MyApp/default.rave
target "${dirname}" {
set entitlements "${dir}/Entitlements.plist"
add CS_FLAGS "--entitlements '${entitlements}'"
}
This will append --entitlements app/MyApp/Entitlements.plist
to CS_FLAGS
and it will also make code signing depend on the Entitlements.plist
file (since that rule uses CS_FLAGS
).
This dependency tracking also works for capture
, but here the build rule that uses these variables, is the build rule that regenerates build.ninja
, so if we change the above version number capture to the following:
set changes "${dir}/Changes.md"
capture VERSION "grep -om1 '^## .* (v.*)$' '${changes}'|sed 's/.*(v\\(.*\))/\\1/'"
We now have build.ninja
depend on Changes.md
, i.e. if we update Changes.md
(by adding release notes for latest version), we cause a new build.ninja
to be produced that captures the most recent version number from the release notes.
Configurations
It is possible to have multiple configurations, these can be specified as follows:
config debug {
add FLAGS "-fsanitize=address -fno-omit-frame-pointer"
add LN_FLAGS "-fsanitize=address"
add CS_FLAGS "--timestamp=none"
}
config release {
add FLAGS "-Os -DNDEBUG"
add LN_FLAGS "-Wl,-dead_strip -Wl,-dead_strip_dylibs"
add CS_FLAGS "--timestamp"
}
They can either be specified at the global level, in which case all targets will get multiple configurations, or they can be scoped to a single target (but all targets that it depend on, must also have a configuration using the same name).
When there are configurations specified, they are added to all symbolic targets, so if we add the above to our first example file, we get MyTarget/debug
, MyTarget/debug/run
, MyTarget/release
, and MyTarget/release/run
.
Each configuration has its own build directory, and any command can be used inside a config
block.
Architectures
Architectures are similar to configurations, though only clang-based build rules are split when there are multiple architectures.
For both architectures and configurations, we can create multiple using the same code block, and there is a config
and arch
variable available that indicates the current configuration/architecture.
So to create a universal release build we can simply add this block:
config release {
arch "arm64" "x86_64" {
add FLAGS "-target macos-${arch}"
add LN_FLAGS "-target macos-${arch}"
}
}
Custom Actions
It is possible to define custom actions using define
followed by the name of the target to create, and then a shell command.
The output of this command is captured in a file under _Actions
in the build directory, using the current target’s name as basename, and the command itself as extension.
Commands can use variables to references the current target’s executable
or application
(for bundles). It can also reference the output (file) of other custom commands using their name.
For example to create a gzipped version of a.out
from MyTarget
, we can define it like this:
target MyTarget {
sources main.cc
executable a.out
define gz "gzip --stdout '${executable}'"
}
This will create a new target named MyTarget/gz
that creates $builddir/_Actions/MyTarget.gz
(which depends on the signed executable).
If we want to upload this version to a server, we could add a new command, e.g.:
define upload "scp '${gz}' example.org:/var/www-data/upload/"
Hint: If version
is defined as a variable, this will be included in the name of the file generated in $builddir/_Actions
.
Notarization
Notarization is handled by a command that takes 3 arguments: developer-id
, password
, and bundle-identifier
.
For example:
notarize "developer@icloud.com" "@keychain:Apple Notarization" "org.example.MyApp"
The rule (for notarization) uses the notarize_await
shell script (to poll Apple’s server).
Extending Targets
Rather than define everything in a single place, it is possible to augment a target with the extend
command. For example to notarize our application and add deployment commands, we can do something like this:
extend MyApp {
config release {
notarize "developer@icloud.com" "@keychain:Apple Notarization" "org.example.${target}"
set KEYFILE "${HOME}/private/sign.key"
define tbz "/usr/bin/tar --disable-copyfile -jcC '${application}' --strip-components=1 .."
define sign "openssl dgst -dss1 -sign '${KEYFILE}' '${tbz}'|openssl enc -base64"
define deploy "curl -sfn -Ffile='@${tbz}' -Fsignature='<${sign}' -Fversion='${version}'' 'https://example.org/releases'"
}
}
Application Bundles
When creating a bundle, one must use prefix
to set the prefix path that executable
, files
, and copy
are relative to.
There is both a files
and copy
command to include bundle resources: First one will process files as they are copied to the bundle, the last one will copy them as they are, e.g. it will not convert Markdown to HTML, convert .strings
files to UTF-16, expand variables in .strings
and Info.plist
, etc.
Here is a simple target for an application bundle:
target "${dirname}" {
prefix "${target}.app/Contents"
files Info.plist "."
files resources/* "Resources"
sources src/*.mm
executable "MacOS/${target}"
frameworks Cocoa
}
Both copy
and files
accept target names to copy by prefixing them with @
, and they both do the same thing. For example:
target "tool" {
sources main.mm
executable "${target}"
}
target "App" {
prefix "${target}.app/Contents"
files Info.plist "."
files resources/* "Resources"
files @tool "SharedSupport/bin"
sources src/*.mm
executable "MacOS/${target}"
frameworks Cocoa
}