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。
與軟件包相反,鏈接庫的方式有兩種
-
(靜態庫)在命令行中傳遞文件名
-
(動態庫)使用 -L 將庫的文件夾添加到搜索路徑中,然後使用 -l 進行實際鏈接。
在 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