tsoding/nob.h
| cli.odin | ||
| cmd.odin | ||
| cmd_posix.odin | ||
| errors.odin | ||
| fs.odin | ||
| mjolnob.odin | ||
| README.md | ||
Mjolnob
Mjobnob is a small Odin library for creating build "scripts" in Odin itself.
It's kind of like tsoding/nob.h, but for Odin and probably worse in many ways.
If you would like to see some "real world" examples, I have one for a hobby kernel and one for an early-dev scratch-built Linux distro.
If you're wondering why I would build this when the Odin compiler is already a fantastic build tool, I encourage you to give the example links above a quick skim to see my use case. For 99% of what I do, I'll never need this. That said, it's nice to be able to define my build steps in the language I enjoy writing the most on the occasion that I do need it.
Usage
- running external commands
- reading and writing files
- collecting files from a directory tree
- deciding when outputs need rebuilding
- reading environment variables and simple CLI flags
Error Handling Style
Most helpers come in two forms:
foo(...)exits the process on failurefoo_or_err(...)returns(value, mjolnob.Error)ormjolnob.Error
This makes the package convenient for builds where failing fast is desirable, while still allowing explicit error propagation when needed.
Running Commands
Build a command with cmd_make, add arguments with cmd_append, then run it with either cmd_run or cmd_run_and_capture.
package build
import "mjolnob"
main :: proc() {
env := mjolnob.env_get()
defer mjolnob.env_destroy(&env)
cmd := mjolnob.cmd_make("odin", "build", "src/main.odin", "-file")
defer mjolnob.cmd_destroy(&cmd)
mjolnob.cmd_append(&cmd, "-out:bin/app")
mjolnob.cmd_run(cmd, env, echo = true)
}
- the
echo: boolargument tocmd_run*enables or disables printing the command line before invocation (default: false) cmd_runstreams stdout/stderr directly to the terminalcmd_run_and_capturereturns aCapture_Resultcontainingstdoutandstderr- a
Cmdcan be reused by callingcmd_clearto discard previous arguments before usingcmd_append
Example with captured output:
main :: proc() {
env := mjolnob.env_get()
defer mjolnob.env_destroy(&env)
cmd := mjolnob.cmd_make("git", "rev-parse", "HEAD")
defer mjolnob.cmd_destroy(&cmd)
result := mjolnob.cmd_run_and_capture(cmd, env)
defer mjolnob.capture_result_destroy(&result)
fmt.println(string(result.stdout))
}
On POSIX targets, the cmd_from_shell_str helper can be used for creating commands that invoke a shell.
Example:
cmd := mjolnob.cmd_make("bash", "-c", "echo foo && ls") // this can be replaced
cmd := mjolnob.cmd_from_shell_str("echo foo && ls -l", shell = "bash") // with this
This is a simple convenience wrapper that creates a command with the args {shell, "-c", cmd_line}.
The shell argument can be omitted and will default to sh.
Filesystem Helpers
Check and Create Paths
if !mjolnob.exists("config.ini") {
// handle config parsing, etc.
}
mjolnob.mkdir_if_missing("build")
mkdir_if_missing is safe to call even if the directory already exists.
Read and Write Files
data := mjolnob.read_entire_file("input.txt")
defer delete(data)
mjolnob.write_entire_file("output.txt", data)
mjolnob.write_entire_file("output2.txt", "hello")
write_entire_file is overloaded for both []u8 and string.
Copy and Stat Files
import "core:os"
import "mjolnob"
// destination, source - same as core:os.copy_file()
mjolnob.copy_file("build/config.json", "config.json")
info := mjolnob.stat_file("build/config.json")
defer os.file_info_delete(info, context.allocator)
stat_file returns os.File_Info from core:os, so cleanup is done with os.file_info_delete.
Collecting Files
Use collect_files to recursively walk a directory and keep files whose base name matches a shell-like pattern.
sources := mjolnob.collect_files("src", "*.odin")
defer mjolnob.file_list_destroy(&sources)
for path in sources.paths {
fmt.println(path)
}
Notes:
- only regular files are included
- matching is done against
info.name, not the full path - destroy the returned
File_Listwithfile_list_destroy
Rebuild Checks
Use needs_rebuild to compare an output file against one or more input files.
if mjolnob.needs_rebuild("bin/app", "src/main.odin", "src/lib.odin") {
// rebuild bin/app
}
There are two entrypoints:
needs_rebuild(output_path, ..input_paths)needs_rebuild(output_path, file_list)
Behavior:
- returns
trueif the output file does not exist - returns
trueif any input is newer than the output - otherwise returns
false
Example using a collected file list:
sources := mjolnob.collect_files("src", "*.odin")
defer mjolnob.file_list_destroy(&sources)
if mjolnob.needs_rebuild("bin/app", sources) {
// run build steps
}
Environment Access
Read the current process environment with env_get.
env := mjolnob.env_get()
defer mjolnob.env_destroy(&env)
This is primarily useful for passing environment variables into cmd_run and cmd_run_and_capture.
CLI Flags
cli_has_flag performs a simple scan of os.args[1:].
if mjolnob.cli_has_flag("--verbose") {
// enable extra logging
}
It only checks for exact string equality and does not parse flag values.
Self-Rebuilding Build Scripts
rebuild_self_if_needed(source_path, echo := false) is intended for Odin build scripts that compile themselves before running.
main :: proc() {
mjolnob.rebuild_self_if_needed("build.odin", true)
// rest of build script
}
Behavior:
- compares the current executable timestamp with the source file timestamp
- rebuilds the executable with
odin build <source> -file - replaces the current executable
- reruns the new executable with the original arguments and environment
Minimal Example
package build
import "mjolnob"
main :: proc() {
mjolnob.rebuild_self_if_needed("build.odin", true)
env := mjolnob.env_get()
defer mjolnob.env_destroy(&env)
mjolnob.mkdir_if_missing("build")
sources := mjolnob.collect_files("src", "*.odin")
defer mjolnob.file_list_destroy(&sources)
if mjolnob.needs_rebuild("build/app", sources) {
cmd := mjolnob.cmd_make("odin", "build", "src/main.odin", "-file", "-out:build/app")
defer mjolnob.cmd_destroy(&cmd)
mjolnob.cmd_run(cmd, env, true)
}
}
This example is somewhat contrived, but it shows the general flow of a mjolnob build program.
Ownership Summary
Callers should explicitly destroy these values when finished:
Envwithenv_destroyCmdwithcmd_destroyCapture_Resultwithcapture_result_destroyFile_Listwithfile_list_destroy- buffers from
read_entire_filewithdelete os.File_Infofromstat_filewithos.file_info_delete