Zig 構建系統解析 - 第一部分
原文鏈接: https://zig.news/xq/zig-build-explained-part-1-59lf
API 適配到 Zig 0.11.0 版本
Zig 構建系統仍然缺少文檔,對很多人來說,這是不使用它的致命理由。還有一些人經常尋找構建項目的祕訣,但也在與構建系統作鬥爭。
本系列試圖深入介紹構建系統及其使用方法。
我們將從一個剛剛初始化的 Zig 項目開始,逐步深入到更復雜的項目。在此過程中,我們將學習如何使用庫和軟件包、添加 C 代碼,甚至如何創建自己的構建步驟。
免責聲明
由於我不會解釋 Zig 語言的語法或語義,因此我希望你至少已經有了一些使用 Zig 的基本經驗。我還將鏈接到標準庫源代碼中的幾個要點,以便您瞭解所有這些內容的來源。我建議你閱讀編譯系統的源代碼,因爲如果你開始挖掘編譯腳本中的函數,大部分內容都不言自明。所有功能都是在標準庫中實現的,不存在隱藏的構建魔法。
開始
我們通過新建一個文件夾來創建一個新項目,並在該文件夾中調用 zig init-exe。
這將生成如下 build.zig 文件(我去掉了註釋)
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "test",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
}
基礎知識
構建系統的核心理念是,Zig 工具鏈將編譯一個 Zig 程序 (build.zig),該程序將導出一個特殊的入口點(pub fn build(b: *std.build.Builder) void
),當我們調用 zig build
時,該入口點將被調用。
然後,該函數將創建一個由 std.build.Step 節點組成的有向無環圖,其中每個步驟都將執行構建過程的一部分。
每個步驟都有一組依賴關係,這些依賴關係需要在步驟本身完成之前完成。作爲用戶,我們可以通過調用 zig build ${step-name}
來調用某些已命名的步驟,或者使用其中一個預定義的步驟(例如 install)。
要創建這樣一個步驟,我們需要調用 Builder.step
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
const named_step = b.step("step-name", "This is what is shown in help");
_ = named_step;
}
這將爲我們創建一個新的步驟 step-name,當我們調用 zig build --help
時將顯示該步驟:
$ zig build --help
使用方法: zig build [steps] [options]
Steps:
install (default) Copy build artifacts to prefix path
uninstall Remove build artifacts from prefix path
step-name This is what is shown in help
General Options:
...
請注意,除了在 zig build --help 中添加一個小條目並允許我們調用 zig build step-name 之外,這個步驟仍然沒有任何作用。
Step 遵循與 std.mem.Allocator 相同的接口模式,需要實現一個 make 函數。步驟創建時將調用該函數。對於我們在這裏創建的步驟,該函數什麼也不做。
現在,我們需要創建一個稍正式的 Zig 程序:
編譯 Zig 源代碼
要使用編譯系統編譯可執行文件,編譯器需要使用函數 Builder.addExecutable,它將爲我們創建一個新的 LibExeObjStep。這個步驟實現是 zig build-exe、zig build-lib、zig build-obj 或 zig test 的便捷封裝,具體取決於初始化方式。本文稍後將對此進行詳細介紹。
現在,讓我們創建一個步驟來編譯我們的 src/main.zig 文件(之前由 zig init-exe 創建)
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
const exe = b.addExecutable(.{.name = "fresh",.root_source_file = .{ .path = "src/main.zig" },});
const compile_step = b.step("compile", "Compiles src/main.zig");
compile_step.dependOn(&exe.step);
}
我們在這裏添加了幾行。首先,const exe = b.addExecutable 將創建一個新的 LibExeObjStep,將 src/main.zig 編譯成一個名爲 fresh 的文件(或 Windows 上的 fresh.exe)。
第二個添加的內容是 compile_step.dependOn(&exe.step);。這就是我們構建依賴關係圖的方法,並聲明當執行 compile_step
時,exe
步驟也需要執行。
你可以調用 zig build,然後再調用 zig build compile 來驗證這一點。第一次調用不會做任何事情,但第二次調用會輸出一些編譯信息。
這將始終在當前機器的調試模式下編譯,因此對於初學者來說,這可能就足夠了。但如果你想開始發佈你的項目,你可能需要啓用交叉編譯:
交叉編譯
交叉編譯是通過設置程序的目標和編譯模式來實現的
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
const exe = b.addExecutable(.{
.name = "fresh",
.root_source_file = .{ .path = "src/main.zig" },
.optimize = .ReleaseSafe,
});
const compile_step = b.step("compile", "Compiles src/main.zig");
compile_step.dependOn(&exe.step);
}
在這裏,.optimize = .ReleaseSafe
, 將向編譯調用傳遞 -O ReleaseSafe。但是!LibExeObjStep.setTarget 需要一個 std.zig.CrossTarget 作爲參數,而你通常希望這個參數是可配置的。
幸運的是,構建系統爲此提供了兩個方便的函數:
-
Builder.standardReleaseOptions
-
Builder.standardTargetOptions
使用這些函數,可以將編譯模式和目標作爲命令行選項:
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "fresh",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const compile_step = b.step("compile", "Compiles src/main.zig");
compile_step.dependOn(&exe.step);
}
現在,如果你調用 zig build --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
前兩個選項由 standardTargetOptions 添加,其他選項由 standardOptimizeOption 添加。現在,我們可以在調用構建腳本時使用這些選項:
zig build -Dtarget=x86_64-windows-gnu -Dcpu=athlon_fx
zig build -Doptimize=ReleaseSafe
zig build -Doptimize=ReleaseSmall
可以看到,對於布爾選項,我們可以省略 =true,直接設置選項本身。
但我們仍然必須調用 zig build 編譯,因爲默認調用仍然沒有任何作用。讓我們改變一下!
安裝工件
要安裝任何東西,我們必須讓它依賴於構建器的安裝步驟。該步驟是已創建的,可通過 Builder.getInstallStep() 訪問。我們還需要創建一個新的 InstallArtifactStep,將我們的 exe 文件複製到安裝目錄(通常是 zig-out)
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "fresh",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const install_exe = b.addInstallArtifact(exe, .{});
b.getInstallStep().dependOn(&install_exe.step);
}
這將做幾件事:
-
創建一個新的 InstallArtifactStep,將 exe 的編譯結果複製到 $prefix/bin 中。
-
由於 InstallArtifactStep(隱含地)依賴於 exe,因此它也將編譯 exe
-
當我們調用 zig build install(或簡稱 zig build)時,它將創建 InstallArtifactStep。
-
InstallArtifactStep 會將 exe 的輸出文件註冊到一個列表中,以便再次卸載它
現在,當你調用 zig build 時,你會看到一個新的目錄 zig-out 被創建了. 看起來有點像這樣:
zig-out
└── bin
└── fresh
現在運行 ./zig-out/bin/fresh,就能看到這條信息:
info: All your codebase are belong to us.
或者,你也可以通過調用 zig build uninstall 再次卸載。這將刪除 zig build install 創建的所有文件,但不會刪除目錄!
由於安裝過程是一個非常普通的操作,它有快捷方法,以縮短代碼。
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "fresh",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
}
如果你在項目中內置了多個應用程序,你可能會想創建幾個單獨的安裝步驟,並手動依賴它們,而不是直接調用 b.installArtifact(exe);,但通常這樣做是正確的。
請注意,我們還可以使用 Builder.installFile(或其他,有很多變體)和 Builder.installDirectory 安裝任何其他文件。
現在,從理解初始構建腳本到完全擴展,還缺少一個部分:
運行已構建的應用程序
爲了開發用戶體驗和一般便利性,從構建腳本中直接運行程序是非常實用的。這通常是通過運行步驟實現的,可以通過 zig build run 調用。
爲此,我們需要一個 RunStep,它將執行我們能在系統上運行的任何可執行文件
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "fresh",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
RunStep 有幾個函數可以爲執行進程的 argv 添加值:
-
addArg 將向 argv 添加一個字符串參數。
-
addArgs 將同時添加多個字符串參數
-
addArtifactArg 將向 argv 添加 LibExeObjStep 的結果文件
-
addFileSourceArg 會將其他步驟生成的任何文件添加到 argv。
請注意,第一個參數必須是我們要運行的可執行文件的路徑。在本例中,我們要運行 exe 的編譯輸出。
現在,當我們調用 zig build run 時,我們將看到與自己運行已安裝的 exe 相同的輸出:
info: All your codebase are belong to us.
請注意,這裏有一個重要的區別: 使用 RunStep 時,我們從 ./zig-cache/.../fresh 而不是 zig-out/bin/fresh 運行可執行文件!如果你加載的文件相對於可執行路徑,這一點可能很重要。
RunStep 的配置非常靈活,可以通過 stdin 向進程傳遞數據,也可以通過 stdout 和 stderr 驗證輸出。你還可以更改工作目錄或環境變量。
對了,還有一件事:
如果你想從 zig 編譯命令行向進程傳遞參數,可以通過訪問 Builder.args 來實現
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "fresh",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
這樣就可以在 cli 上的 -- 後面傳遞參數:
zig build run -- -o foo.bin foo.asm
結論
本系列的第一章應該能讓你完全理解本文開頭的構建腳本,並能創建自己的構建腳本。
大多數項目甚至只需要編譯、安裝和運行一些 Zig 可執行文件,所以你就可以開始了!
下一部分我將介紹如何構建 C 和 C++ 項目。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/W6SIdeHCODozPPvBukicXA