Zig 構建系統解析 - 第三部分

  • 原文鏈接: https://zig.news/xq/zig-build-explained-part-3-1ima

  • API 適配到 Zig 0.11.0 版本

從現在起,我將只提供一個最精簡的 build.zig,用來說明解決一個問題所需的步驟。如果你想了解如何將所有這些文件粘合到一個構建文件中,請閱讀本系列第一篇文章 [1]。

複合項目

有很多簡單的項目只包含一個可執行文件。但是,一旦開始編寫庫,就必須對其進行測試,通常會編寫一個或多個示例應用程序。當人們開始使用外部軟件包、C 語言庫、生成代碼等時,複雜性也會隨之上升。

本文試圖涵蓋所有這些用例,並將解釋如何使用 build.zig 來編寫多個程序和庫。

軟件包

譯者:此處代碼和說明,需要 zig build-exe --pkg-begin,但是在 0.11 已經失效。所以刪除。

但 Zig 也知道庫這個詞。但我們不是已經討論過外部庫了嗎?

在 Zig 的世界裏,庫是一個預編譯的靜態或動態庫,就像在 C/C++ 的世界裏一樣。庫通常包含頭文件(.h 或 .zig)和二進制文件(通常爲 .a、.lib、.so 或 .dll)。

這種庫的常見例子是 zlib 或 SDL。

與軟件包相反,鏈接庫的方式有兩種

在 Zig 中,我們需要導入庫的頭文件,如果頭文件在 Zig 中,則使用包,如果是 C 語言頭文件,則使用 @cImport。

工具

如果我們的項目越來越多,那麼在構建過程中就需要使用工具。這些工具通常會完成以下任務:

生成一些代碼(如解析器生成器、序列化器或庫頭文件) 捆綁應用程序(例如生成 APK、捆綁應用程序......)。 創建資產包 ... 有了 Zig,我們不僅能在構建過程中利用現有工具,還能爲當前主機編譯我們自己(甚至外部)的工具並運行它們。

但我們如何在 build.zig 中完成這些工作呢?

添加軟件包

添加軟件包通常使用 LibExeObjStep 上的 addPackage 函數。該函數使用一個 std.build.Pkg 結構來描述軟件包的外觀:

pub const Module = struct {
    builder: *Build,
    source_file: LazyPath,
    dependencies: std.StringArrayHashMap(*Module),
};

我們可以看到,它有 2 個成員:

source_file 是定義軟件包根文件的 FileSource。這通常只是指向文件的路徑,如 vendor/zig-args/args.zig dependencies 是該軟件包所需的可選軟件包片段。如果我們使用更復雜的軟件包,這通常是必需的。

這是個人建議:我通常會在 build.zig 的頂部創建一個名爲 pkgs 的結構 / 名稱空間,看起來有點像這樣:

const args = b.createModule(.{
    .source_file = .{ .path = "libs/args/args.zig" },
    .dependencies = &.{},
});
const interface = b.createModule(.{
    .source_file = .{ .path = "libs/interface.zig/interface.zig" },
    .dependencies = &.{},
});
const lola = b.createModule(.{
    .source_file = .{ .path = "src/library/main.zig" },
    .dependencies = &.{},
});
const pkgs = .{
    .args = args,
    .interface = interface,
    .lola = lola,
};

隨後通過編譯步驟 exe,把模塊加入進來。函數 addModule 的第一個參數 name 是模塊名稱

exe.addModule("lola",pkgs.lola);
exe.addModule("args",pkgs.args);

添加庫

添加庫相對容易,但我們需要配置更多的路徑。

注:在上一篇文章中,我們已經介紹了大部分內容,但現在還是讓我們快速複習一遍:

假設我們要將 libcurl 鏈接到我們的項目,因爲我們要下載一些文件。

系統庫

對於 unixoid 系統,我們通常可以使用系統軟件包管理器來鏈接系統庫。方法是調用 linkSystemLibrary,它會使用 pkg-config 自行找出所有路徑:

//demo 3.2
const std = @import("std");
pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const exe = b.addExecutable(.{
        .name = "example",
        .root_source_file = .{ .path = "main.zig" },
        .target = target,
        .optimize = optimize,
    });
    exe.linkLibC();
    exe.linkSystemLibrary("curl");
    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);
}

對於 Linux 系統,這是鏈接外部庫的首選方式。

本地庫

不過,您也可以鏈接您作爲二進制文件提供商的庫。爲此,我們需要調用幾個函數。首先,讓我們來看看這樣一個庫是什麼樣子的:

./vendor/libcurl
include
│ └── curl
│ ├── curl.h
│ ├── curlver.h
│ ├── easy.h
│ ├── mprintf.h
│ ├─── multi.h
│ ├── options.h
│ ├── stdcheaders.h
│ ├── system.h
│ ├── typecheck-gcc.h
│ └── urlapi.h
├── lib
│ ├── libcurl.a
│ ├── libcurl.so
│ └── ...
├─── bin
│ └── ...
└──share
    └── ...

我們可以看到,vendor/libcurl/include 路徑包含我們的頭文件,vendor/libcurl/lib 文件夾包含一個靜態庫(libcurl.a)和一個共享 / 動態庫(libcurl.so)。

動態鏈接

要鏈接 libcurl,我們需要先添加 include 路徑,然後向 zig 提供庫的前綴和庫名:(todo 代碼有待驗證, 因爲 curl 可能需要自己編譯自己生成 static lib)

//demo 3.3
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 = "test",
        .root_source_file = .{ .path = "main.zig" },
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(exe);
    exe.linkLibC();
    exe.addIncludePath(.{ .path = "vendor/libcurl/include" });
    exe.addLibraryPath(.{ .path = "vendor/libcurl/lib" });
    exe.linkSystemLibraryName("curl");
}

addIncludePath 將文件夾添加到搜索路徑中,這樣 Zig 就能找到 curl/curl.h 文件。注意,我們也可以在這裏傳遞 "vendor/libcurl/include/curl",但你通常應該檢查一下你的庫到底想要什麼。

addLibraryPath 對庫文件也有同樣的作用。這意味着 Zig 現在也會搜索 "vendor/libcurl/lib" 文件夾中的庫。

最後,linkSystemLibrary 會告訴 Zig 搜索名爲 "curl" 的庫。如果你留心觀察,就會發現上面列表中的文件名是 libcurl.so,而不是 curl.so。在 unixoid 系統中,庫文件的前綴通常是 lib,這樣就不會將其傳遞給系統。在 Windows 系統中,庫文件的名字應該是 curl.lib 或類似的名字。

靜態鏈接

當我們要靜態鏈接一個庫時,我們必須採取一些不同的方法:

pub fn build(b: *std.build.Builder) 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);
    exe.linkLibC();
    exe.addIncludeDir("vendor/libcurl/include");
    exe.addObjectFile("vendor/libcurl/lib/libcurl.a");
    exe.addIncludePath(.{ .path = "vendor/libcurl/include" });
    exe.addLibraryPath(.{ .path = "vendor/libcurl/lib" });
}

對 addIncludeDir 的調用沒有改變,但我們突然不再調用帶 link 的函數了?你可能已經知道了: 靜態庫實際上就是對象文件的集合。在 Windows 上,這一點也很相似,據說 MSVC 也使用了相同的工具集。

因此,靜態庫就像對象文件一樣,通過 addObjectFile 傳遞給鏈接器,並由其解包。

注意:大多數靜態庫都有一些傳遞依賴關係。在我編譯 libcurl 的例子中,就有 nghttp2、zstd、z 和 pthread,我們需要再次手動鏈接它們:

// 示例片段
pub fn build(b: *std.build.Builder) 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);
    exe.linkLibC();
    exe.addIncludePath(.{ .path = "vendor/libcurl/include" });
    exe.addLibraryPath(.{ .path = "vendor/libcurl/lib" });
    exe.linkSystemLibrary("nghttp2");
    exe.linkSystemLibrary("zstd");
    exe.linkSystemLibrary("z");
    exe.linkSystemLibrary("pthread")}

我們可以繼續靜態鏈接越來越多的庫,並拉入完整的依賴關係樹。

通過源代碼鏈接庫

不過,我們還有一種與 Zig 工具鏈截然不同的鏈接庫方式:

我們可以自己編譯它們!

這樣做的好處是,我們可以更容易地交叉編譯我們的程序。爲此,我們需要將庫的構建文件轉換成我們的 build.zig。這通常需要對 build.zig 和你的庫所使用的構建系統都有很好的瞭解。但讓我們假設這個庫是超級簡單的,只是由一堆 C 文件組成:

// 示例片段
pub fn build(b: *std.build.Builder) void {
    const cflags = .{};
    const curl = b.addSharedLibrary("curl", null, .unversioned);
    exe.addCSourceFile(.{
            .file = std.build.LazyPath.relative("vendor/libcurl/src/tool_main.c"),
            .flags = &cflags,
            });
    exe.addCSourceFile(.{
            .file = std.build.LazyPath.relative("vendor/libcurl/src/tool_msgs.c"),
            .flags = &cflags,
            });
    exe.addCSourceFile(.{
            .file = std.build.LazyPath.relative("vendor/libcurl/src/tool_dirhie.c"),
            .flags = &cflags,
            });
    exe.addCSourceFile(.{
            .file = std.build.LazyPath.relative("vendor/libcurl/src/tool_doswin.c"),
            .flags = &cflags,
            });
    const target = b.standardTargetOptions(.{});
    exe.linkLibC();
    exe.addIncludePath(.{ .path = "vendor/libcurl/include" });
    exe.linkLibrary(curl);
    b.installArtifact(exe);
}

這樣,我們就可以使用 addSharedLibrary 和 addStaticLibrary 向 LibExeObjStep 添加庫。

這一點尤其方便,因爲我們可以使用 setTarget 和 setBuildMode 從任何地方編譯到任何地方。

使用工具

在工作流程中使用工具,通常是在需要以 bison、flex、protobuf 或其他形式進行預編譯時。工具的其他用例包括將輸出文件轉換爲不同格式(如固件映像)或捆綁最終應用程序。

系統工具 使用預裝的系統工具非常簡單,只需使用 addSystemCommand 創建一個新步驟即可:

// demo 3.5
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 = "test",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });
    const cmd = b.addSystemCommand(&.{
        "flex",
        "-outfile=lines.c",
        "lines.l",
    });
    b.installArtifact(exe);
    exe.step.dependOn(&cmd.step);
}

從這裏可以看出,我們只是向 addSystemCommand 傳遞了一個選項數組,該數組將反映我們的命令行調用。然後,我們按照習慣創建可執行文件,並使用 dependOn 在 cmd 上添加步驟依賴關係。

我們也可以反其道而行之,在編譯程序時添加有關程序的小信息:

//demo3.6
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 = "test",
        .root_source_file = .{ .path = "main.zig" },
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(exe);
    const cmd = b.addSystemCommand(&.{"size"});
    cmd.addArtifactArg(exe);
    b.getInstallStep().dependOn(&cmd.step);
}

size 是一個很好的工具,它可以輸出有關可執行文件代碼大小的信息,可能如下所示:

文本 數據 BSS Dec 十六進制 文件名 12377 620 104 13101 332d ...

如您所見,我們在這裏使用了 addArtifactArg,因爲 addSystemCommand 只會返回一個 std.build.RunStep。這樣,我們就可以增量構建完整的命令行,包括任何 LibExeObjStep 輸出、FileSource 或逐字參數。

全新工具

最酷的是 我們還可以從 LibExeObjStep 獲取 std.build.RunStep:

// 示例片段
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
     const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const game = b.addExecutable(.{
        .name = "game",
        .root_source_file = .{ .path = "src/game.zig" },
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(game);
    const pack_tool = b.addExecutable(.{
        .name = "pack",
        .root_source_file = .{ .path = "tools/pack.zig" },
        .target = target,
        .optimize = optimize,
    });
    //譯者改動:const precompilation = pack_tool.run(); // returns *RunStep
    const precompilation = b.addRunArtifact(pack_tool);
    precompilation.addArtifactArg(game);
    precompilation.addArg("assets.zip");
    const pack_step = b.step("pack", "Packs the game and assets together");
    pack_step.dependOn(&precompilation.step);
}

此構建腳本將首先編譯一個名爲 pack 的可執行文件。然後將以我們的遊戲和 assets.zig 文件作爲命令行參數調用該可執行文件。

調用 zig build pack 時,我們將運行 tools/pack.zig。這很酷,因爲我們還可以從頭開始編譯所需的工具。爲了獲得最佳的開發體驗,你甚至可以從源代碼編譯像 bison 這樣的 "外部" 工具,這樣就不會依賴系統了!

將所有內容放在一起

一開始,所有這些都會讓人望而生畏,但如果我們看一個更大的 build.zig 實例,就會發現一個好的構建文件結構會給我們帶來很大幫助。

下面的編譯腳本將編譯一個虛構的工具,它可以通過 flex 生成的詞法器解析輸入文件,然後使用 curl 連接到服務器,並在那裏傳送一些文件。當我們調用 zig build deploy 時,項目將被打包成一個 zip 文件。正常的 zig 編譯調用只會準備一個未打包的本地調試安裝。

// 示例片段
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
    const mode = b.standardOptimizeOption(.{});
    // const mode = b.standardReleaseOptions();
    const target = b.standardTargetOptions(.{});
    // Generates the lex-based parser
    const parser_gen = b.addSystemCommand(&[_][]const u8{
        "flex",
        "--outfile=review-parser.c",
        "review-parser.l",
    });
    // Our application
    const exe = b.addExecutable(.{
        .name = "upload-review",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = mode,
    });
    {
        exe.step.dependOn(&parser_gen.step);
        exe.addCSourceFile(.{ .file = std.build.LazyPath.relative("review-parser.c"), .flags = &.{} });
        // add zig-args to parse arguments
        const ap = b.createModule(.{
            .source_file = .{ .path = "vendor/zig-args/args.zig" },
            .dependencies = &.{},
        });
        exe.addModule("args-parser", ap);
        // add libcurl for uploading
        exe.addIncludePath(std.build.LazyPath.relative("vendor/libcurl/include"));
        exe.addObjectFile(std.build.LazyPath.relative("vendor/libcurl/lib/libcurl.a"));
        exe.linkLibC();
        b.installArtifact(exe);
        // exe.install();
    }
    // Our test suite
    const test_step = b.step("test", "Runs the test suite");
    const test_suite = b.addTest(.{
        .root_source_file = .{ .path = "src/tests.zig" },
    });
    test_suite.step.dependOn(&parser_gen.step);
    exe.addCSourceFile(.{ .file = std.build.LazyPath.relative("review-parser.c"), .flags = &.{} });
    // add libcurl for uploading
    exe.addIncludePath(std.build.LazyPath.relative("vendor/libcurl/include"));
    exe.addObjectFile(std.build.LazyPath.relative("vendor/libcurl/lib/libcurl.a"));
    test_suite.linkLibC();
    test_step.dependOn(&test_suite.step);
    {
        const deploy_step = b.step("deploy", "Creates an application bundle");
        // compile the app bundler
        const deploy_tool = b.addExecutable(.{
            .name = "deploy",
            .root_source_file = .{ .path = "tools/deploy.zig" },
            .target = target,
            .optimize = mode,
        });
        {
            deploy_tool.linkLibC();
            deploy_tool.linkSystemLibrary("libzip");
        }
        const bundle_app = b.addRunArtifact(deploy_tool);
        bundle_app.addArg("app-bundle.zip");
        bundle_app.addArtifactArg(exe);
        bundle_app.addArg("resources/index.htm");
        bundle_app.addArg("resources/style.css");
        deploy_step.dependOn(&bundle_app.step);
    }
}

如你所見,代碼量很大,但通過使用塊,我們可以將構建腳本結構化爲邏輯組。

如果你想知道爲什麼我們不爲 deploy_tool 和 test_suite 設置目標: 兩者都是爲了在主機平臺上運行,而不是在目標機器上。 此外,deploy_tool 還設置了固定的編譯模式,因爲我們希望快速編譯,即使我們編譯的是應用程序的調試版本。

總結

看完這一大堆文字,你現在應該可以構建任何你想要的項目了。我們已經學會了如何編譯 Zig 應用程序,如何爲其添加任何類型的外部庫,甚至如何爲發佈管理對應用程序進行後處理。

我們還可以通過少量的工作來構建 C 和 C++ 項目,並將它們部署到各個地方,而不僅僅是 Zig 項目。

即使我們混合使用項目、工具和其他一切。一個 build.zig 文件就能滿足我們的需求。但很快你就會發現... 編譯文件很快就會重複,而且有些軟件包或庫需要大量代碼才能正確設置。

在下一篇文章中,我們將學習如何將 build.zig 文件模塊化,如何爲 Zig 創建方便的 sdks,甚至如何創建自己的構建步驟!

一如既往,繼續黑客之旅!

參考資料

[1] 

第一篇文章: /post/2023/12/24/zig-build-explained-part1/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bj8YKzha8_uYRPwV1AI7uw