Система сборки Zig
Когда стоит использовать систему сборки Zig?
Для выполнения основных задач обычно достаточно команд zig build-exe
, zig build-lib
, zig build-obj
и zig test
. Тем не менее, в некоторых случаях проекту может понадобиться дополнительный уровень абстракции для более эффективного управления сложностью процесса сборки.
Система сборки Zig может значительно упростить процесс разработки и сборки в следующих ситуациях:
- Если команды в терминале становятся слишком громоздкими и сложными для запоминания, использование системы сборки позволит сохранить их в одном месте.
- При наличии множества компонентов или этапов сборки Zig поможет организовать процесс, разбив его на более управляемые части.
- Если вы хотите сократить время сборки, система сборки Zig поддерживает конкурентность и кэширование, что значительно ускоряет процесс.
- Zig позволяет легко задавать параметры конфигурации для вашего проекта, что делает его более гибким и адаптируемым к различным условиям.
- Если процесс сборки варьируется в зависимости от целевой системы или других параметров, Zig поможет вам настроить его под конкретные нужды.
- При наличии зависимостей от других проектов система сборки Zig упростит управление ими и обеспечит правильную интеграцию.
- Если вы стремитесь уменьшить зависимость от таких инструментов, как CMake, Make, Shell, MSVC или Python, Zig поможет сделать ваш проект более универсальным и доступным.
- Если вы хотите упростить процесс создания пакетов для использования другими разработчиками, система сборки Zig обеспечит удобные механизмы для этого.
- С помощью Zig вы сможете предложить стандартизированный подход к сборке проекта, что облегчит интеграцию с различными IDE и инструментами разработки.
Если хотя бы один из перечисленных пунктов имеет отношение к вашему проекту, применение системы сборки Zig может существенно облегчить процесс разработки.
Начало работы
Простое исполняемое приложение
Этот скрипт сборки создаёт исполняемый файл из Zig-файла, который содержит определение публичной функции main
.
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("hello.zig"),
.target = b.host,
});
b.installArtifact(exe);
}
$ zig build--summary all
Build Summary: 3/3 steps succeeded
install cached
└─ install hello cached
└─ zig build-exe hello Debug native cached 66ms MaxRSS:36M
Установка артефактов сборки
Система сборки Zig, подобно большинству других систем сборки, моделирует проект как направленный ациклический граф (DAG), где этапы выполняются независимо и параллельно.
По умолчанию основным этапом в графе является этап Установки (Install), который предназначен для копирования артефактов сборки в их конечное местоположение. Этот этап не имеет зависимостей, поэтому при выполнении команды zig build
ничего не произойдёт. Скрипт сборки проекта должен дополнительно указать, какие элементы необходимо установить, и именно это делает вызов функции installArtifact
, представленный выше.
Вывод
├── build.zig
├── hello.zig
├── .zig-cache
└── zig-out
└── bin
└── hello
В этом выводе можно увидеть две сгенерированные директории: .zig-cache
и zig-out
. Первая из них содержит файлы, которые ускоряют последующие сборки. Эти файлы не предназначены для добавления в систему контроля версий, и данную директорию можно удалить в любое время без каких-либо негативных последствий.
Вторая директория, zig-out
, является целевой директорией установки. Она соответствует стандартной структуре файловой системы. Выбор этой директории осуществляется не проектом, а пользователем команды zig build
с помощью флага --prefix
(сокращенно -p
).
Вы, как разработчик проекта, определяете, что будет помещено в эту директорию, но пользователь решает, куда установить её на своём компьютере. Скрипт сборки не должен жёстко задавать пути вывода, так как это может нарушить кэширование, конкурентное выполнение и совместимость, а также вызвать недовольство у конечного пользователя.
Этап запуска приложения
Обычно этап Запуска (Run) добавляют, чтобы упростить процесс запуска основного приложения непосредственно через команду сборки.
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("hello.zig"),
.target = b.host,
});
b.installArtifact(exe);
const run_exe = b.addRunArtifact(exe);
const run_step = b.step("run", "Run the application");
run_step.dependOn(&run_exe.step);
}
$ zig buildrun --summary all
Hello World!
Build Summary: 3/3 steps succeeded
run success
└─ run hello success 14ms MaxRSS:1M
└─ zig build-exe hello Debug native cached 44ms MaxRSS:36M
Основы
Опции, задаваемые пользователем
Можно использовать b.option
, чтобы сделать скрипт сборки настраиваемым для конечных пользователей и других проектов, которые используют данный проект в качестве пакета.
const std = @import("std");
pub fn build(b: *std.Build) void {
const windows = b.option(bool, "windows", "Target Microsoft Windows") orelse false;
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("example.zig"),
.target = b.resolveTargetQuery(.{
.os_tag = if (windows) .windows else null,
}),
});
b.installArtifact(exe);
}
$ zig build--help
Usage: /home/ci/deps/zig-linux-x86_64-0.13.0/zig build [steps] [options]
Steps:
install (default) Copy build artifacts to prefix path
uninstall Remove build artifacts from prefix path
General Options:
-p, --prefix [path] Where to install files (default: zig-out)
--prefix-lib-dir [path] Where to install libraries
--prefix-exe-dir [path] Where to install executables
--prefix-include-dir [path] Where to install C header files
--release[=mode] Request release mode, optionally specifying a
preferred optimization mode: fast, safe, small
-fdarling, -fno-darling Integration with system-installed Darling to
execute macOS programs on Linux hosts
(default: no)
-fqemu, -fno-qemu Integration with system-installed QEMU to execute
foreign-architecture programs on Linux hosts
(default: no)
--glibc-runtimes [path] Enhances QEMU integration by providing glibc built
for multiple foreign architectures, allowing
execution of non-native programs that link with glibc.
-frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on
ARM64 macOS hosts. (default: no)
-fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to
execute WASI binaries. (default: no)
-fwine, -fno-wine Integration with system-installed Wine to execute
Windows programs on Linux hosts. (default: no)
-h, --help Print this help and exit
-l, --list-steps Print available steps
--verbose Print commands before executing them
--color [auto|off|on] Enable or disable colored error messages
--prominent-compile-errors Buffer compile errors and display at end
--summary [mode] Control the printing of the build summary
all Print the build summary in its entirety
new Omit cached steps
failures (Default) Only print failed steps
none Do not print the build summary
-j<N> Limit concurrent jobs (default is to use all CPU cores)
--maxrss <bytes> Limit memory usage (default is to use available memory)
--skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
--fetch Exit after fetching dependency tree
Project-Specific Options:
-Dwindows=[bool] Target Microsoft Windows
System Integration Options:
--search-prefix [path] Add a path to look for binaries, libraries, headers
--sysroot [path] Set the system root directory (usually /)
--libc [file] Provide a file which specifies libc paths
--system [pkgdir] Disable package fetching; enable all integrations
-fsys=[name] Enable a system integration
-fno-sys=[name] Disable a system integration
Available System Integrations: Enabled:
(none) -
Advanced Options:
-freference-trace[=num] How many lines of reference trace should be shown per compile error
-fno-reference-trace Disable reference trace
--build-file [file] Override path to build.zig
--cache-dir [path] Override path to local Zig cache directory
--global-cache-dir [path] Override path to global Zig cache directory
--zig-lib-dir [arg] Override path to Zig lib directory
--build-runner [file] Override path to build runner
--seed [integer] For shuffling dependency traversal order (default: random)
--debug-log [scope] Enable debugging the compiler
--debug-pkg-config Fail if unknown pkg-config flags encountered
--verbose-link Enable compiler debug output for linking
--verbose-air Enable compiler debug output for Zig AIR
--verbose-llvm-ir[=file] Enable compiler debug output for LLVM IR
--verbose-llvm-bc=[file] Enable compiler debug output for LLVM BC
--verbose-cimport Enable compiler debug output for C imports
--verbose-cc Enable compiler debug output for C compilation
--verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}
Обратите внимание на эти строки:
Project-Specific Options:
-Dwindows=[bool] Target Microsoft Windows
Эта секция меню помощи создаётся автоматически в результате выполнения build.zig
, чтобы пользователи могли ознакомиться с параметрами конфигурации скрипта сборки.
Стандартные параметры конфигурации
Ранее мы применяли булев флаг для обозначения сборки под Windows. Однако, существует более эффективный подход.
Большинство проектов стремятся предоставить возможность изменять целевую платформу и параметры оптимизации. Для стандартизации названий этих параметров Zig предлагает вспомогательные функции standardTargetOptions
и standardOptimizeOption
.
Стандартные параметры для выбора целевой платформы позволяют пользователю, выполняющему команду zig build
, указать, для какой платформы будет осуществляться сборка проекта. По умолчанию поддерживаются все платформы, и если выбор не сделан, проект будет собран для текущей системы. Существуют дополнительные опции для ограничения списка поддерживаемых платформ.
Параметры оптимизации по умолчанию позволяют пользователю выбрать один из следующих режимов: Debug
, ReleaseSafe
, ReleaseFast
и ReleaseSmall
. Поскольку ни один из этих режимов по умолчанию не является предпочтительным, пользователю следует самостоятельно решить, какой из них использовать.
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("hello.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
}
$ zig build-Dtarget=x86_64-windows -Doptimize=ReleaseSmall --summary all
Build Summary: 3/3 steps succeeded
install cached
└─ install hello cached
└─ zig build-exe hello ReleaseSmall x86_64-windows cached 44ms MaxRSS:36M
Теперь меню --help
содержит больше пунктов:
Project-Specific Options:
-Dtarget=[string] The CPU architecture, OS, and ABI to build for
-Dcpu=[string] Target CPU features to add or subtract
-Doptimize=[enum] Prioritize performance, safety, or binary size (-O flag)
Supported Values:
Debug
ReleaseSafe
ReleaseFast
ReleaseSmall
Хотя эти параметры можно задать напрямую с помощью b.option
, данный API предоставляет стандартные наименования для часто используемых настроек.
Обратите внимание на вывод в терминале, где мы указали -Dtarget=x86_64-windows -Doptimize=ReleaseSmall
. В отличие от первого примера, теперь в целевой директории появляются дополнительные файлы:
zig-out/
└── bin
└── hello.exe
Условная компиляция
Для передачи параметров из скрипта сборки в Zig-код вашего проекта используйте Options
.
const std = @import("std");
const config = @import("config");
const semver = std.SemanticVersion.parse(config.version) catch unreachable;
extern fn foo_bar() void;
pub fn main() !void {
if (semver.major < 1) {
@compileError("too old");
}
std.debug.print("version: {s}\n", .{config.version});
if (config.have_libfoo) {
foo_bar();
}
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "app",
.root_source_file = b.path("app.zig"),
.target = b.host,
});
const version = b.option([]const u8, "version", "application version string") orelse "0.0.0";
const enable_foo = detectWhetherToEnableLibFoo();
const options = b.addOptions();
options.addOption([]const u8, "version", version);
options.addOption(bool, "have_libfoo", enable_foo);
exe.root_module.addOptions("config", options);
b.installArtifact(exe);
}
fn detectWhetherToEnableLibFoo() bool {
return false;
}
$ zig build-Dversion=1.2.3 --summary all
Build Summary: 4/4 steps succeeded
install cached
└─ install app cached
└─ zig build-exe app Debug native cached 33ms MaxRSS:36M
└─ options cached
В этом примере данные, предоставленные через @import("config")
, известны на этапе компиляции, что предотвращает срабатывание @compileError
. Если бы мы передали -Dversion=0.2.3
или пропустили эту опцию, то компиляция app.zig
завершилась бы ошибкой "too old".
Статические библиотеки
Этот скрипт сборки создаёт статическую библиотеку из кода на Zig, а затем и исполняемый файл из другого кода на Zig, который использует эту библиотеку.
export fn fizzbuzz(n: usize) ?[*:0]const u8 {
if (n % 5 == 0) {
if (n % 3 == 0) {
return "fizzbuzz";
} else {
return "fizz";
}
} else if (n % 3 == 0) {
return "buzz";
}
return null;
}
const std = @import("std");
extern fn fizzbuzz(n: usize) ?[*:0]const u8;
pub fn main() !void {
const stdout = std.io.getStdOut();
var bw = std.io.bufferedWriter(stdout.writer());
const w = bw.writer();
for (0..100) |n| {
if (fizzbuzz(n)) |s| {
try w.print("{s}\n", .{s});
} else {
try w.print("{d}\n", .{n});
}
}
try bw.flush();
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const libfizzbuzz = b.addStaticLibrary(.{
.name = "fizzbuzz",
.root_source_file = b.path("fizzbuzz.zig"),
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "demo",
.root_source_file = b.path("demo.zig"),
.target = target,
.optimize = optimize,
});
exe.linkLibrary(libfizzbuzz);
b.installArtifact(libfizzbuzz);
if (b.option(bool, "enable-demo", "install the demo too") orelse false) {
b.installArtifact(exe);
}
}
$ zig build--summary all
Build Summary: 3/3 steps succeeded
install cached
└─ install fizzbuzz cached
└─ zig build-lib fizzbuzz Debug native cached 56ms MaxRSS:36M
В этом случае только статическая библиотека будет установлена:
zig-out/
└── lib
└── libfizzbuzz.a
Однако, если присмотреться, можно заметить, что в скрипте сборки есть опция для установки демо-версии. Если дополнительно указать -Denable-demo
, то в целевой директории мы увидим следующее:
zig-out/
├── bin
│ └── demo
└── lib
└── libfizzbuzz.a
Заметьте, что хотя addExecutable
вызывается безусловно, система сборки не будет создавать исполняемый файл demo
, если только его не запросить с помощью параметра -Denable-demo
. Это возможно благодаря тому, что система сборки использует ориентированный ациклический граф (DAG), который управляет зависимостями.
Динамические библиотеки
Здесь мы оставляем все файлы такими же, как в примере со статической библиотекой, за исключением изменений в файле build.zig
.
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const libfizzbuzz = b.addSharedLibrary(.{
.name = "fizzbuzz",
.root_source_file = b.path("fizzbuzz.zig"),
.target = target,
.optimize = optimize,
.version = .{ .major = 1, .minor = 2, .patch = 3 },
});
b.installArtifact(libfizzbuzz);
}
$ zig build--summary all
Build Summary: 3/3 steps succeeded
install cached
└─ install fizzbuzz cached
└─ zig build-lib fizzbuzz Debug native cached 33ms MaxRSS:37M
Вывод
zig-out
└── lib
├── libfizzbuzz.so -> libfizzbuzz.so.1
├── libfizzbuzz.so.1 -> libfizzbuzz.so.1.2.3
└── libfizzbuzz.so.1.2.3
Как и в примере со статической библиотекой, чтобы создать исполняемый файл, который будет ссылаться на неё, можем использовать такой код:
exe.linkLibrary(libfizzbuzz);
Тестирование
Отдельные файлы можно протестировать напрямую с помощью команды zig test foo.zig
.
Для более сложных случаев тестирование можно организовать с помощью скрипта сборки. В этом случае юнит-тесты разделяются на два этапа: Компиляции (Compile) и Запуска (Run).
Важно помнить, что если не вызвать addRunArtifact
, устанавливающий зависимость между этими двумя этапами, юнит-тесты не будут выполнены.
Этап Компиляции (Compile) настраивается аналогично исполняемым файлам, библиотекам или объектным файлам. Например, его можно связывать с системными библиотеками или задавать параметры целевой платформы.
Этап Запуска (Run) настраивается аналогично другим этапам Запуска (Run). Например, можно пропустить выполнение, если текущая платформа не поддерживает запуск исполняемого файла.
При использовании системы сборки для запуска юнит-тестов сборщик и тестовый раннер взаимодействуют через stdin и stdout. Это позволяет одновременно запускать несколько наборов юнит-тестов и сообщать о сбоях в тестах понятным способом, избегая смешивания их вывода.
Одной из причин, по которой запись в стандартный вывод в юнит-тестах является проблематичной, является возможность нарушения этого канала связи. С другой стороны, эта механика откроет возможность для новой функции — возможности юнит-теста ожидать сбоя.
const std = @import("std");
test "simple test" {
var list = std.ArrayList(i32).init(std.testing.allocator);
defer list.deinit();
try list.append(42);
try std.testing.expectEqual(@as(i32, 42), list.pop());
}
const std = @import("std");
const test_targets = [_]std.Target.Query{
.{}, // native
.{
.cpu_arch = .x86_64,
.os_tag = .linux,
},
.{
.cpu_arch = .aarch64,
.os_tag = .macos,
},
};
pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run unit tests");
for (test_targets) |target| {
const unit_tests = b.addTest(.{
.root_source_file = b.path("main.zig"),
.target = b.resolveTargetQuery(target),
});
const run_unit_tests = b.addRunArtifact(unit_tests);
test_step.dependOn(&run_unit_tests.step);
}
}
$ zig buildtest --summary all
test
└─ run test failure
error: the host system (x86_64-linux.5.10...5.10-gnu.2.31) is unable to execute binaries from the target (aarch64-macos.11.7.1...14.1-none)
Build Summary: 5/7 steps succeeded; 1 failed
test transitive failure
├─ run test cached
│ └─ zig test Debug native cached 49ms MaxRSS:36M
├─ run test cached
│ └─ zig test Debug x86_64-linux cached 61ms MaxRSS:36M
└─ run test failure
└─ zig test Debug aarch64-macos cached 38ms MaxRSS:36M
error: the following build command failed with exit code 1:
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/build-system/unit-testing/.zig-cache/o/25475ace5ef269b60e98416da65a5cde/build /home/ci/deps/zig-linux-x86_64-0.13.0/zig /home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/build-system/unit-testing /home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/build-system/unit-testing/.zig-cache /home/ci/.cache/zig --seed 0x10a7accf -Zd5915bdf16628db4 --color on test --summary all
В этом случае будет полезно включить параметр skip_foreign_checks
для юнит-тестов.
@@ -23,6 +23,7 @@
});
const run_unit_tests = b.addRunArtifact(unit_tests);
+ run_unit_tests.skip_foreign_checks = true;
test_step.dependOn(&run_unit_tests.step);
}
}
const std = @import("std");
const test_targets = [_]std.Target.Query{
.{}, // native
.{
.cpu_arch = .x86_64,
.os_tag = .linux,
},
.{
.cpu_arch = .aarch64,
.os_tag = .macos,
},
};
pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run unit tests");
for (test_targets) |target| {
const unit_tests = b.addTest(.{
.root_source_file = b.path("main.zig"),
.target = b.resolveTargetQuery(target),
});
const run_unit_tests = b.addRunArtifact(unit_tests);
run_unit_tests.skip_foreign_checks = true;
test_step.dependOn(&run_unit_tests.step);
}
}
// zig-doctest: build-system --collapseable -- test --summary all
$ zig build--summary all
Build Summary: 1/1 steps succeeded
install cached
Подключение системных библиотек
Существует два основных способа управления зависимостями:
- Использование системы сборки Zig. Этот метод позволяет предоставлять библиотеки через встроенные механизмы сборки Zig. Подробности можно найти в разделах Управление пакетами и Статические библиотеки.
- Использование системных библиотек. В этом случае библиотеки предоставляются операционной системой.
Для upstream-проектов использование системы сборки Zig для управления зависимостями снижает уровень сложности и предоставляет полный контроль над конфигурацией. Этот подход обеспечивает воспроизводимость и согласованность результатов сборки на всех операционных системах, а также упрощает процесс кросс-компиляции. Кроме того, он даёт возможность точно выбирать версии всех зависимостей, необходимых для проекта. В большинстве случаев данный метод считается предпочтительным для работы с внешними библиотеками.
Тем не менее, если требуется упаковать программное обеспечение для репозиториев, таких как Debian, Homebrew или Nix, использование системных библиотек становится обязательным. Поэтому в скриптах сборки важно определить режим сборки и соответствующим образом настроить проект.
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "zip",
.root_source_file = b.path("zip.zig"),
.target = b.host,
});
exe.linkSystemLibrary("z");
exe.linkLibC();
b.installArtifact(exe);
}
$ zig build--summary all
Build Summary: 3/3 steps succeeded
install cached
└─ install zip cached
└─ zig build-exe zip Debug native cached 95ms MaxRSS:37M
Пользователи команды zig build
могут воспользоваться опцией --search-prefix
, чтобы указать дополнительные директории, которые будут рассматриваться как системные для поиска статических и динамических библиотек.
Генерация файлов
Запуск системных инструментов
Эта версия примера с "Hello, World!" ожидает, что в той же папке будет находиться файл word.txt
. Мы собираемся использовать системный инструмент для его генерации на основе JSON-файла.
Учтите, что системные зависимости могут усложнить сборку вашего проекта. Например, этот скрипт сборки зависит от программы jq
, которая по умолчанию отсутствует в большинстве дистрибутивов Linux и может быть незнакома пользователям Windows.
В следующем разделе мы заменим утилиту jq
на встроенный инструмент Zig, который уже присутствует в исходном коде. Этот подход предпочтителен, так как он устраняет необходимость в сторонних зависимостях.
words.json
{
"en": "world",
"it": "mondo",
"ja": "世界"
}
const std = @import("std");
pub fn main() !void {
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const self_exe_dir_path = try std.fs.selfExeDirPathAlloc(arena);
var self_exe_dir = try std.fs.cwd().openDir(self_exe_dir_path, .{});
defer self_exe_dir.close();
const word = try self_exe_dir.readFileAlloc(arena, "word.txt", 1000);
try std.io.getStdOut().writer().print("Hello {s}\n", .{word});
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const lang = b.option([]const u8, "language", "language of the greeting") orelse "en";
const tool_run = b.addSystemCommand(&.{"jq"});
tool_run.addArgs(&.{
b.fmt(
\\.["{s}"]
, .{lang}),
"-r", // raw output to omit quotes around the selected string
});
tool_run.addFileArg(b.path("words.json"));
const output = tool_run.captureStdOut();
b.getInstallStep().dependOn(&b.addInstallFileWithDir(output, .prefix, "word.txt").step);
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const install_artifact = b.addInstallArtifact(exe, .{
.dest_dir = .{ .override = .prefix },
});
b.getInstallStep().dependOn(&install_artifact.step);
}
$ zig build-Dlanguage=ja --summary all
Build Summary: 5/5 steps succeeded
install cached
├─ install generated to word.txt cached
│ └─ run jq cached
└─ install hello cached
└─ zig build-exe hello Debug native cached 48ms MaxRSS:36M
Вывод
zig-out
├── hello
└── word.txt
Обратите внимание, что функция captureStdOut
создаёт временный файл с выводом вызова утилиты jq
.
Запуск инструментов проекта
Эта версия программы "Hello, World!" ожидает найти файл word.txt
в той же директории. Мы планируем сгенерировать его в процессе сборки, используя программу для обработки JSON-файла на Zig.
tools/words.json
{
"en": "world",
"it": "mondo",
"ja": "世界"
}
const std = @import("std");
pub fn main() !void {
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const self_exe_dir_path = try std.fs.selfExeDirPathAlloc(arena);
var self_exe_dir = try std.fs.cwd().openDir(self_exe_dir_path, .{});
defer self_exe_dir.close();
const word = try self_exe_dir.readFileAlloc(arena, "word.txt", 1000);
try std.io.getStdOut().writer().print("Hello {s}\n", .{word});
}
const std = @import("std");
const usage =
\\Usage: ./word_select [options]
\\
\\Options:
\\ --input-file INPUT_JSON_FILE
\\ --output-file OUTPUT_TXT_FILE
\\ --lang LANG
\\
;
pub fn main() !void {
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const args = try std.process.argsAlloc(arena);
var opt_input_file_path: ?[]const u8 = null;
var opt_output_file_path: ?[]const u8 = null;
var opt_lang: ?[]const u8 = null;
{
var i: usize = 1;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
try std.io.getStdOut().writeAll(usage);
return std.process.cleanExit();
} else if (std.mem.eql(u8, "--input-file", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_input_file_path != null) fatal("duplicated {s} argument", .{arg});
opt_input_file_path = args[i];
} else if (std.mem.eql(u8, "--output-file", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_output_file_path != null) fatal("duplicated {s} argument", .{arg});
opt_output_file_path = args[i];
} else if (std.mem.eql(u8, "--lang", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_lang != null) fatal("duplicated {s} argument", .{arg});
opt_lang = args[i];
} else {
fatal("unrecognized arg: '{s}'", .{arg});
}
}
}
const input_file_path = opt_input_file_path orelse fatal("missing --input-file", .{});
const output_file_path = opt_output_file_path orelse fatal("missing --output-file", .{});
const lang = opt_lang orelse fatal("missing --lang", .{});
var input_file = std.fs.cwd().openFile(input_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ input_file_path, @errorName(err) });
};
defer input_file.close();
var output_file = std.fs.cwd().createFile(output_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
};
defer output_file.close();
var json_reader = std.json.reader(arena, input_file.reader());
var words = try std.json.ArrayHashMap([]const u8).jsonParse(arena, &json_reader, .{
.allocate = .alloc_if_needed,
.max_value_len = 1000,
});
const w = words.map.get(lang) orelse fatal("Lang not found in JSON file", .{});
try output_file.writeAll(w);
return std.process.cleanExit();
}
fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.debug.print(format, args);
std.process.exit(1);
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const lang = b.option([]const u8, "language", "language of the greeting") orelse "en";
const tool = b.addExecutable(.{
.name = "word_select",
.root_source_file = b.path("tools/word_select.zig"),
.target = b.host,
});
const tool_step = b.addRunArtifact(tool);
tool_step.addArg("--input-file");
tool_step.addFileArg(b.path("tools/words.json"));
tool_step.addArg("--output-file");
const output = tool_step.addOutputFileArg("word.txt");
tool_step.addArgs(&.{ "--lang", lang });
b.getInstallStep().dependOn(&b.addInstallFileWithDir(output, .prefix, "word.txt").step);
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const install_artifact = b.addInstallArtifact(exe, .{
.dest_dir = .{ .override = .prefix },
});
b.getInstallStep().dependOn(&install_artifact.step);
}
$ zig build--summary all
Build Summary: 6/6 steps succeeded
install cached
├─ install generated to word.txt cached
│ └─ run word_select (word.txt) cached
│ └─ zig build-exe word_select Debug native cached 43ms MaxRSS:36M
└─ install hello cached
└─ zig build-exe hello Debug native cached 83ms MaxRSS:36M
Вывод
zig-out
├── hello
└── word.txt
Создание ресурсов для @embedFile
В этой версии программы "Hello, World!" планируется использовать @embedFile
для встраивания ресурса, который будет создан в процессе сборки. Мы будем генерировать этот ресурс с помощью инструмента, написанного на Zig.
tools/words.json
{
"en": "world",
"it": "mondo",
"ja": "世界"
}
const std = @import("std");
const word = @embedFile("word");
pub fn main() !void {
try std.io.getStdOut().writer().print("Hello {s}\n", .{word});
}
const std = @import("std");
const usage =
\\Usage: ./word_select [options]
\\
\\Options:
\\ --input-file INPUT_JSON_FILE
\\ --output-file OUTPUT_TXT_FILE
\\ --lang LANG
\\
;
pub fn main() !void {
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const args = try std.process.argsAlloc(arena);
var opt_input_file_path: ?[]const u8 = null;
var opt_output_file_path: ?[]const u8 = null;
var opt_lang: ?[]const u8 = null;
{
var i: usize = 1;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
try std.io.getStdOut().writeAll(usage);
return std.process.cleanExit();
} else if (std.mem.eql(u8, "--input-file", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_input_file_path != null) fatal("duplicated {s} argument", .{arg});
opt_input_file_path = args[i];
} else if (std.mem.eql(u8, "--output-file", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_output_file_path != null) fatal("duplicated {s} argument", .{arg});
opt_output_file_path = args[i];
} else if (std.mem.eql(u8, "--lang", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_lang != null) fatal("duplicated {s} argument", .{arg});
opt_lang = args[i];
} else {
fatal("unrecognized arg: '{s}'", .{arg});
}
}
}
const input_file_path = opt_input_file_path orelse fatal("missing --input-file", .{});
const output_file_path = opt_output_file_path orelse fatal("missing --output-file", .{});
const lang = opt_lang orelse fatal("missing --lang", .{});
var input_file = std.fs.cwd().openFile(input_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ input_file_path, @errorName(err) });
};
defer input_file.close();
var output_file = std.fs.cwd().createFile(output_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
};
defer output_file.close();
var json_reader = std.json.reader(arena, input_file.reader());
var words = try std.json.ArrayHashMap([]const u8).jsonParse(arena, &json_reader, .{
.allocate = .alloc_if_needed,
.max_value_len = 1000,
});
const w = words.map.get(lang) orelse fatal("Lang not found in JSON file", .{});
try output_file.writeAll(w);
return std.process.cleanExit();
}
fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.debug.print(format, args);
std.process.exit(1);
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const lang = b.option([]const u8, "language", "language of the greeting") orelse "en";
const tool = b.addExecutable(.{
.name = "word_select",
.root_source_file = b.path("tools/word_select.zig"),
.target = b.host,
});
const tool_step = b.addRunArtifact(tool);
tool_step.addArg("--input-file");
tool_step.addFileArg(b.path("tools/words.json"));
tool_step.addArg("--output-file");
const output = tool_step.addOutputFileArg("word.txt");
tool_step.addArgs(&.{ "--lang", lang });
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe.root_module.addAnonymousImport("word", .{
.root_source_file = output,
});
b.installArtifact(exe);
}
$ zig build--summary all
Build Summary: 5/5 steps succeeded
install cached
└─ install hello cached
└─ zig build-exe hello Debug native cached 51ms MaxRSS:36M
└─ run word_select (word.txt) cached
└─ zig build-exe word_select Debug native cached 88ms MaxRSS:36M
Вывод
zig-out/
└── bin
└── hello
Генерация исходного кода Zig
В этом файле сборки используется программа на Zig, которая создаёт новый Zig-файл. Сгенерированный файл затем подключается к основной программе в качестве зависимости.
const std = @import("std");
const Person = @import("person").Person;
pub fn main() !void {
const p: Person = .{};
try std.io.getStdOut().writer().print("Hello {any}\n", .{p});
}
const std = @import("std");
pub fn main() !void {
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const args = try std.process.argsAlloc(arena);
if (args.len != 2) fatal("wrong number of arguments", .{});
const output_file_path = args[1];
var output_file = std.fs.cwd().createFile(output_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
};
defer output_file.close();
try output_file.writeAll(
\\pub const Person = struct {
\\ age: usize = 18,
\\ name: []const u8 = "foo"
\\};
);
return std.process.cleanExit();
}
fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.debug.print(format, args);
std.process.exit(1);
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const tool = b.addExecutable(.{
.name = "generate_struct",
.root_source_file = b.path("tools/generate_struct.zig"),
.target = b.host,
});
const tool_step = b.addRunArtifact(tool);
const output = tool_step.addOutputFileArg("person.zig");
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe.root_module.addAnonymousImport("person", .{
.root_source_file = output,
});
b.installArtifact(exe);
}
$ zig build--summary all
Build Summary: 5/5 steps succeeded
install cached
└─ install hello cached
└─ zig build-exe hello Debug native cached 39ms MaxRSS:36M
└─ run generate_struct (person.zig) cached
└─ zig build-exe generate_struct Debug native cached 53ms MaxRSS:36M
Вывод
zig-out/
└── bin
└── hello
Работа с одним или несколькими сгенерированными файлами
Этап WriteFiles предоставляет возможность генерировать один или несколько файлов, которые имеют общую родительскую директорию. Сгенерированная директория располагается внутри локального .zig-cache
, и каждый созданный файл доступен как std.Build.LazyPath
. Кроме того, сама родительская директория также представлена в виде LazyPath
.
Этот API позволяет записывать произвольные строки в сгенерированную директорию, а также копировать в неё файлы.
const std = @import("std");
pub fn main() !void {
std.debug.print("hello world\n", .{});
}
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "app",
.root_source_file = b.path("src/main.zig"),
.target = b.host,
});
const version = b.option([]const u8, "version", "application version string") orelse "0.0.0";
const wf = b.addWriteFiles();
const app_exe_name = b.fmt("project/{s}", .{exe.out_filename});
_ = wf.addCopyFile(exe.getEmittedBin(), app_exe_name);
_ = wf.add("project/version.txt", version);
const tar = b.addSystemCommand(&.{ "tar", "czf" });
tar.setCwd(wf.getDirectory());
const out_file = tar.addOutputFileArg("project.tar.gz");
tar.addArgs(&.{"project/"});
const install_tar = b.addInstallFileWithDir(out_file, .prefix, "project.tar.gz");
b.getInstallStep().dependOn(&install_tar.step);
}
$ zig build--summary all
Build Summary: 5/5 steps succeeded
install cached
└─ install generated to project.tar.gz cached
└─ run tar (project.tar.gz) cached
└─ WriteFile project/app cached
└─ zig build-exe app Debug native cached 68ms MaxRSS:36M
Вывод
zig-out/
└── project.tar.gz
Мутирование исходных файлов
Хотя это и не является распространённой практикой, иногда проекты сохраняют сгенерированные файлы в системе контроля версий. Это может быть полезно, когда такие файлы обновляются редко и имеют сложные системные зависимости, которые затрудняют процесс обновления, но только в рамках самого процесса обновления.
Для этой цели этап WriteFiles предоставляет необходимый функционал. В будущих версиях Zig эта возможность будет выделена из WriteFiles в отдельный этап сборки.
С этим следует быть осторожным: данный функционал не предназначен для использования в процессе обычной сборки. Он должен применяться как утилита, запускаемая разработчиком с целью обновления исходных файлов, которые затем будут зафиксированы в системе контроля версий. Если использовать эту функцию во время стандартного процесса сборки, это может привести к ошибкам кэширования и конкурентного выполнения.
const std = @import("std");
pub fn main() !void {
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const args = try std.process.argsAlloc(arena);
if (args.len != 2) fatal("wrong number of arguments", .{});
const output_file_path = args[1];
var output_file = std.fs.cwd().createFile(output_file_path, .{}) catch |err| {
fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
};
defer output_file.close();
try output_file.writeAll(
\\pub const Header = extern struct {
\\ magic: u64,
\\ width: u32,
\\ height: u32,
\\};
);
return std.process.cleanExit();
}
fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.debug.print(format, args);
std.process.exit(1);
}
const std = @import("std");
const Protocol = @import("protocol.zig");
pub fn main() !void {
const header = try std.io.getStdIn().reader().readStruct(Protocol.Header);
std.debug.print("header: {any}\n", .{header});
}
pub const Header = extern struct {
magic: u64,
width: u32,
height: u32,
};
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "demo",
.root_source_file = b.path("src/main.zig"),
});
b.installArtifact(exe);
const proto_gen = b.addExecutable(.{
.name = "proto_gen",
.root_source_file = b.path("tools/proto_gen.zig"),
});
const run = b.addRunArtifact(proto_gen);
const generated_protocol_file = run.addOutputFileArg("protocol.zig");
const wf = b.addWriteFiles();
wf.addCopyFileToSource(generated_protocol_file, "src/protocol.zig");
const update_protocol_step = b.step("update-protocol", "update src/protocol.zig to latest");
update_protocol_step.dependOn(&wf.step);
}
fn detectWhetherToEnableLibFoo() bool {
return false;
}
$ zig build update-protocol --summary all
Build Summary: 4/4 steps succeeded
update-protocol success
└─ WriteFile success
└─ run proto_gen (protocol.zig) success 401us MaxRSS:1M
└─ zig build-exe proto_gen Debug native success 1s MaxRSS:183M
После выполнения этой команды файл src/protocol.zig
будет обновлён.
Полезные примеры
Сборка для нескольких целевых платформ с целью создания релиза
В этом примере мы изменим некоторые стандартные параметры на этапе InstallArtifact
, чтобы размещать сборку для каждой целевой платформы в отдельной подпапке. Такой подход позволит более эффективно организовать артефакты, созданные для различных платформ, а также упростит их управление и развёртывание в дальнейшем.
const std = @import("std");
const targets: []const std.Target.Query = &.{
.{ .cpu_arch = .aarch64, .os_tag = .macos },
.{ .cpu_arch = .aarch64, .os_tag = .linux },
.{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu },
.{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl },
.{ .cpu_arch = .x86_64, .os_tag = .windows },
};
pub fn build(b: *std.Build) !void {
for (targets) |t| {
const exe = b.addExecutable(.{
.name = "hello",
.root_source_file = b.path("hello.zig"),
.target = b.resolveTargetQuery(t),
.optimize = .ReleaseSafe,
});
const target_output = b.addInstallArtifact(exe, .{
.dest_dir = .{
.override = .{
.custom = try t.zigTriple(b.allocator),
},
},
});
b.getInstallStep().dependOn(&target_output.step);
}
}
$ zig build--summary all
Build Summary: 11/11 steps succeeded
install cached
├─ install hello cached
│ └─ zig build-exe hello ReleaseSafe aarch64-macos cached 33ms MaxRSS:36M
├─ install hello cached
│ └─ zig build-exe hello ReleaseSafe aarch64-linux cached 98ms MaxRSS:36M
├─ install hello cached
│ └─ zig build-exe hello ReleaseSafe x86_64-linux-gnu cached 37ms MaxRSS:33M
├─ install hello cached
│ └─ zig build-exe hello ReleaseSafe x86_64-linux-musl cached 43ms MaxRSS:36M
└─ install hello cached
└─ zig build-exe hello ReleaseSafe x86_64-windows cached 45ms MaxRSS:36M
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}
Вывод
zig-out
├── aarch64-linux
│ └── hello
├── aarch64-macos
│ └── hello
├── x86_64-linux-gnu
│ └── hello
├── x86_64-linux-musl
│ └── hello
└── x86_64-windows
├── hello.exe
└── hello.pdb