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 作爲參數,而你通常希望這個參數是可配置的。

幸運的是,構建系統爲此提供了兩個方便的函數:

使用這些函數,可以將編譯模式和目標作爲命令行選項:

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);
}

這將做幾件事:

  1. 創建一個新的 InstallArtifactStep,將 exe 的編譯結果複製到 $prefix/bin 中。

  2. 由於 InstallArtifactStep(隱含地)依賴於 exe,因此它也將編譯 exe

  3. 當我們調用 zig build install(或簡稱 zig build)時,它將創建 InstallArtifactStep。

  4. 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 添加值:

請注意,第一個參數必須是我們要運行的可執行文件的路徑。在本例中,我們要運行 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