概述
第22章讲解了std.Build API的机制;本章通过一个完整的项目来巩固这些知识:TextKit,一个文本处理库,配有一个CLI工具,展示了构建工作区、组织模块、链接构件、集成测试和创建自定义构建步骤的实际模式。参见22和Build.zig。
通过逐步了解TextKit的实现——从模块组织到构建脚本编排——你将理解专业的Zig项目如何在可重用库和特定应用的可执行文件之间分离关注点,同时维护处理编译、测试和分发的单一统一构建图。参见21和Compile.zig。
学习目标
项目结构:TextKit
TextKit是一个文本处理工具,包含:
- 库():作为模块公开的可重用文本处理函数
- 可执行文件():使用该库的命令行界面
- 测试:对库功能的全面覆盖
- 自定义步骤:超出标准构建/测试/运行的演示命令
库实现
TextKit库公开了两个主要模块:用于字符级操作的StringUtils和用于文档分析的TextStats。参见Module.zig。
字符串工具模块
// Import the standard library for testing utilities
const std = @import("std");
// / String utilities for text processing
pub const StringUtils = struct {
// / Count occurrences of a character in a string
// / Returns the total number of times the specified character appears
pub fn countChar(text: []const u8, char: u8) usize {
var count: usize = 0;
// Iterate through each character in the text
for (text) |c| {
// Increment counter when matching character is found
if (c == char) count += 1;
}
return count;
}
// / Check if string contains only ASCII characters
// / ASCII characters have values from 0-127
pub fn isAscii(text: []const u8) bool {
for (text) |c| {
// Any character with value > 127 is non-ASCII
if (c > 127) return false;
}
return true;
}
// / Reverse a string in place
// / Modifies the input buffer directly using two-pointer technique
pub fn reverse(text: []u8) void {
// Early return for empty strings
if (text.len == 0) return;
var left: usize = 0;
var right: usize = text.len - 1;
// Swap characters from both ends moving towards the center
while (left < right) {
const temp = text[left];
text[left] = text[right];
text[right] = temp;
left += 1;
right -= 1;
}
}
};
// Test suite verifying countChar functionality with various inputs
test "countChar counts occurrences" {
const text = "hello world";
// Verify counting of 'l' character (appears 3 times)
try std.testing.expectEqual(@as(usize, 3), StringUtils.countChar(text, 'l'));
// Verify counting of 'o' character (appears 2 times)
try std.testing.expectEqual(@as(usize, 2), StringUtils.countChar(text, 'o'));
// Verify counting returns 0 for non-existent character
try std.testing.expectEqual(@as(usize, 0), StringUtils.countChar(text, 'x'));
}
// Test suite verifying ASCII detection for different character sets
test "isAscii detects ASCII strings" {
// Standard ASCII letters should return true
try std.testing.expect(StringUtils.isAscii("hello"));
// ASCII digits should return true
try std.testing.expect(StringUtils.isAscii("123"));
// String with non-ASCII character (é = 233) should return false
try std.testing.expect(!StringUtils.isAscii("héllo"));
}
// Test suite verifying in-place string reversal
test "reverse reverses string" {
// Create a mutable buffer to test in-place reversal
var buffer = [_]u8{ 'h', 'e', 'l', 'l', 'o' };
StringUtils.reverse(&buffer);
// Verify the buffer contents are reversed
try std.testing.expectEqualSlices(u8, "olleh", &buffer);
}
此模块展示了:
- 基于结构的组织:静态方法分组在
StringUtils下 - 内联测试:每个函数与其测试用例配对以提高局部性
- 简单算法:字符计数、ASCII验证、原地反转
文本统计模块
const std = @import("std");
// Text statistics and analysis structure
// Provides functionality to analyze text content and compute various metrics
// such as word count, line count, and character count.
pub const TextStats = struct {
// Total number of words found in the analyzed text
word_count: usize,
// Total number of lines in the analyzed text
line_count: usize,
// Total number of characters in the analyzed text
char_count: usize,
// Analyze text and compute statistics
// Iterates through the input text to count words, lines, and characters.
// Words are defined as sequences of non-whitespace characters separated by whitespace.
// Lines are counted based on newline characters, with special handling for text
// that doesn't end with a newline.
pub fn analyze(text: []const u8) TextStats {
var stats = TextStats{
.word_count = 0,
.line_count = 0,
.char_count = text.len,
};
// Track whether we're currently inside a word to avoid counting multiple
// consecutive whitespace characters as separate word boundaries
var in_word = false;
for (text) |c| {
if (c == '\n') {
stats.line_count += 1;
in_word = false;
} else if (std.ascii.isWhitespace(c)) {
// Whitespace marks the end of a word
in_word = false;
} else if (!in_word) {
// Transition from whitespace to non-whitespace marks a new word
stats.word_count += 1;
in_word = true;
}
}
// Count last line if text doesn't end with newline
if (text.len > 0 and text[text.len - 1] != '\n') {
stats.line_count += 1;
}
return stats;
}
// Format and write statistics to the provided writer
// Outputs the statistics in a human-readable format: "Lines: X, Words: Y, Chars: Z"
pub fn format(self: TextStats, writer: *std.Io.Writer) std.Io.Writer.Error!void {
try writer.print("Lines: {d}, Words: {d}, Chars: {d}", .{
self.line_count,
self.word_count,
self.char_count,
});
}
};
// Verify that TextStats correctly analyzes multi-line text with multiple words
test "TextStats analyzes simple text" {
const text = "hello world\nfoo bar";
const stats = TextStats.analyze(text);
try std.testing.expectEqual(@as(usize, 2), stats.line_count);
try std.testing.expectEqual(@as(usize, 4), stats.word_count);
try std.testing.expectEqual(@as(usize, 19), stats.char_count);
}
// Verify that TextStats correctly handles edge case of empty input
test "TextStats handles empty text" {
const text = "";
const stats = TextStats.analyze(text);
try std.testing.expectEqual(@as(usize, 0), stats.line_count);
try std.testing.expectEqual(@as(usize, 0), stats.word_count);
try std.testing.expectEqual(@as(usize, 0), stats.char_count);
}
关键模式:
- 状态聚合:
TextStats结构体保存计算的统计信息 - 分析函数:纯函数,接受文本并返回统计信息
- 格式化方法:用于打印的Zig 0.15.2格式化接口
- 全面测试:边界情况(空文本、无尾随换行符)
库根文件:公共API
// ! TextKit - A text processing library
//!
// ! This library provides utilities for text manipulation and analysis,
// ! including string utilities and text statistics.
pub const StringUtils = @import("string_utils.zig").StringUtils;
pub const TextStats = @import("text_stats.zig").TextStats;
const std = @import("std");
// Library version information
pub const version = std.SemanticVersion{
.major = 1,
.minor = 0,
.patch = 0,
};
test {
// Ensure all module tests are run
std.testing.refAllDecls(@This());
}
根文件(textkit.zig)作为库的公共接口:
- 重新导出:使子模块可作为
textkit.StringUtils和textkit.TextStats访问 - 版本元数据:供外部使用者使用的语义版本
- 测试聚合:
std.testing.refAllDecls()确保所有模块测试运行
这种模式允许内部重组而不破坏使用者导入。20,testing.zig
可执行文件实现
CLI工具将库功能包装在用户友好的命令行界面中,为不同操作提供子命令。process.zig
CLI结构和参数解析
const std = @import("std");
const textkit = @import("textkit");
pub fn main() !void {
// 设置通用分配器用于动态内存分配
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// 检索传递给程序的命令行参数
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
// 确保至少提供一个命令参数(args[0]是程序名)
if (args.len < 2) {
try printUsage();
return;
}
// 从第一个参数中提取命令动词
const command = args[1];
// 根据命令分派到适当的处理程序
if (std.mem.eql(u8, command, "analyze")) {
// 'analyze'需要一个文件名参数
if (args.len < 3) {
std.debug.print("Error: analyze requires a filename\n", .{});
return;
}
try analyzeFile(allocator, args[2]);
} else if (std.mem.eql(u8, command, "reverse")) {
// 'reverse'需要反转的文本
if (args.len < 3) {
std.debug.print("Error: reverse requires text\n", .{});
return;
}
try reverseText(args[2]);
} else if (std.mem.eql(u8, command, "count")) {
// 'count'需要文本和要计数的单个字符
if (args.len < 4) {
std.debug.print("Error: count requires text and character\n", .{});
return;
}
// 验证字符参数恰好是一个字节
if (args[3].len != 1) {
std.debug.print("Error: character must be single byte\n", .{});
return;
}
try countCharacter(args[2], args[3][0]);
} else {
// 处理无法识别的命令
std.debug.print("Unknown command: {s}\n", .{command});
try printUsage();
}
}
/// 打印使用说明以指导用户了解可用命令
fn printUsage() !void {
const usage =
\\TextKit CLI - Text processing utility
\\
\\Usage:
\\ textkit-cli analyze <file> Analyze text file statistics
\\ textkit-cli reverse <text> Reverse the given text
\\ textkit-cli count <text> <char> Count character occurrences
\\
;
std.debug.print("{s}", .{usage});
}
/// 读取文件并显示其文本内容的统计分析
fn analyzeFile(allocator: std.mem.Allocator, filename: []const u8) !void {
// 从当前工作目录以只读模式打开文件
const file = try std.fs.cwd().openFile(filename, .{});
defer file.close();
// 将整个文件内容读取到内存(限制为1MB)
const content = try file.readToEndAlloc(allocator, 1024 * 1024);
defer allocator.free(content);
// 使用textkit库计算文本统计信息
const stats = textkit.TextStats.analyze(content);
// 向用户显示计算出的统计信息
std.debug.print("File: {s}\n", .{filename});
std.debug.print(" Lines: {d}\n", .{stats.line_count});
std.debug.print(" Words: {d}\n", .{stats.word_count});
std.debug.print(" Characters: {d}\n", .{stats.char_count});
std.debug.print(" ASCII only: {}\n", .{textkit.StringUtils.isAscii(content)});
}
/// 反转提供的文本并显示原始和反转版本
fn reverseText(text: []const u8) !void {
// 为就地反转分配栈缓冲区
var buffer: [1024]u8 = undefined;
// 确保输入文本适合缓冲区
if (text.len > buffer.len) {
std.debug.print("Error: text too long (max {d} chars)\n", .{buffer.len});
return;
}
// 将输入文本复制到可变缓冲区中进行反转
@memcpy(buffer[0..text.len], text);
// 使用textkit实用工具执行就地反转
textkit.StringUtils.reverse(buffer[0..text.len]);
// 显示原始和反转文本
std.debug.print("Original: {s}\n", .{text});
std.debug.print("Reversed: {s}\n", .{buffer[0..text.len]});
}
/// 计算提供文本中特定字符的出现次数
fn countCharacter(text: []const u8, char: u8) !void {
// 使用textkit计算字符出现次数
const count = textkit.StringUtils.countChar(text, char);
// 显示计数结果
std.debug.print("Character '{c}' appears {d} time(s) in: {s}\n", .{
char,
count,
text,
});
}
// 测试此模块中的所有声明是否可访问并正确编译
test "main program compiles" {
std.testing.refAllDecls(@This());
}
可执行文件展示了:
- 命令分发:将子命令路由到处理函数
- 参数验证:检查参数数量和格式
- 错误处理:带有信息性消息的优雅失败
- 库使用:通过
@import("textkit")进行干净的导入
命令处理函数
const std = @import("std");
const textkit = @import("textkit");
pub fn main() !void {
// 设置通用分配器用于动态内存分配
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// 检索传递给程序的命令行参数
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
// 确保至少提供一个命令参数(args[0]是程序名)
if (args.len < 2) {
try printUsage();
return;
}
// 从第一个参数中提取命令动词
const command = args[1];
// 根据命令分派到适当的处理程序
if (std.mem.eql(u8, command, "analyze")) {
// 'analyze'需要一个文件名参数
if (args.len < 3) {
std.debug.print("Error: analyze requires a filename\n", .{});
return;
}
try analyzeFile(allocator, args[2]);
} else if (std.mem.eql(u8, command, "reverse")) {
// 'reverse'需要反转的文本
if (args.len < 3) {
std.debug.print("Error: reverse requires text\n", .{});
return;
}
try reverseText(args[2]);
} else if (std.mem.eql(u8, command, "count")) {
// 'count'需要文本和要计数的单个字符
if (args.len < 4) {
std.debug.print("Error: count requires text and character\n", .{});
return;
}
// 验证字符参数恰好是一个字节
if (args[3].len != 1) {
std.debug.print("Error: character must be single byte\n", .{});
return;
}
try countCharacter(args[2], args[3][0]);
} else {
// 处理无法识别的命令
std.debug.print("Unknown command: {s}\n", .{command});
try printUsage();
}
}
/// 打印使用说明以指导用户了解可用命令
fn printUsage() !void {
const usage =
\\TextKit CLI - Text processing utility
\\
\\Usage:
\\ textkit-cli analyze <file> Analyze text file statistics
\\ textkit-cli reverse <text> Reverse the given text
\\ textkit-cli count <text> <char> Count character occurrences
\\
;
std.debug.print("{s}", .{usage});
}
/// 读取文件并显示其文本内容的统计分析
fn analyzeFile(allocator: std.mem.Allocator, filename: []const u8) !void {
// 从当前工作目录以只读模式打开文件
const file = try std.fs.cwd().openFile(filename, .{});
defer file.close();
// 将整个文件内容读取到内存(限制为1MB)
const content = try file.readToEndAlloc(allocator, 1024 * 1024);
defer allocator.free(content);
// 使用textkit库计算文本统计信息
const stats = textkit.TextStats.analyze(content);
// 向用户显示计算出的统计信息
std.debug.print("File: {s}\n", .{filename});
std.debug.print(" Lines: {d}\n", .{stats.line_count});
std.debug.print(" Words: {d}\n", .{stats.word_count});
std.debug.print(" Characters: {d}\n", .{stats.char_count});
std.debug.print(" ASCII only: {}\n", .{textkit.StringUtils.isAscii(content)});
}
/// 反转提供的文本并显示原始和反转版本
fn reverseText(text: []const u8) !void {
// 为就地反转分配栈缓冲区
var buffer: [1024]u8 = undefined;
// 确保输入文本适合缓冲区
if (text.len > buffer.len) {
std.debug.print("Error: text too long (max {d} chars)\n", .{buffer.len});
return;
}
// 将输入文本复制到可变缓冲区中进行反转
@memcpy(buffer[0..text.len], text);
// 使用textkit实用工具执行就地反转
textkit.StringUtils.reverse(buffer[0..text.len]);
// 显示原始和反转文本
std.debug.print("Original: {s}\n", .{text});
std.debug.print("Reversed: {s}\n", .{buffer[0..text.len]});
}
/// 计算提供文本中特定字符的出现次数
fn countCharacter(text: []const u8, char: u8) !void {
// 使用textkit计算字符出现次数
const count = textkit.StringUtils.countChar(text, char);
// 显示计数结果
std.debug.print("Character '{c}' appears {d} time(s) in: {s}\n", .{
char,
count,
text,
});
}
// 测试此模块中的所有声明是否可访问并正确编译
test "main program compiles" {
std.testing.refAllDecls(@This());
}
每个处理程序展示了不同的库功能:
analyzeFile:文件I/O、内存分配、文本统计reverseText:栈缓冲区使用、字符串操作countCharacter:简单的库委托
构建脚本:编排工作区
build.zig文件将所有内容联系在一起,定义库和可执行文件之间的关系以及用户如何与项目交互。
完整的构建脚本
const std = @import("std");
pub fn build(b: *std.Build) void {
// 标准目标和优化选项
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// ===== 库 =====
// 创建 TextKit 库模块
const textkit_mod = b.addModule("textkit", .{
.root_source_file = b.path("src/textkit.zig"),
.target = target,
});
// 构建静态库产物
const lib = b.addLibrary(.{
.name = "textkit",
.root_module = b.createModule(.{
.root_source_file = b.path("src/textkit.zig"),
.target = target,
.optimize = optimize,
}),
.version = .{ .major = 1, .minor = 0, .patch = 0 },
.linkage = .static,
});
// 安装库 (到 zig-out/lib/)
b.installArtifact(lib);
// ===== 可执行文件 =====
// 创建使用库的可执行文件
const exe = b.addExecutable(.{
.name = "textkit-cli",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "textkit", .module = textkit_mod },
},
}),
});
// 安装可执行文件 (到 zig-out/bin/)
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 TextKit CLI");
run_step.dependOn(&run_cmd.step);
// ===== 测试 =====
// 库测试
const lib_tests = b.addTest(.{
.root_module = textkit_mod,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
// 可执行文件测试 (main.zig 的最小测试)
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
// 运行所有测试的测试步骤
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step);
// ===== 自定义步骤 =====
// 展示用法的演示步骤
const demo_step = b.step("demo", "Run demo commands");
const demo_reverse = b.addRunArtifact(exe);
demo_reverse.addArgs(&.{ "reverse", "Hello Zig!" });
demo_step.dependOn(&demo_reverse.step);
const demo_count = b.addRunArtifact(exe);
demo_count.addArgs(&.{ "count", "mississippi", "s" });
demo_step.dependOn(&demo_count.step);
}
构建脚本部分解释
库创建
const std = @import("std");
pub fn build(b: *std.Build) void {
// 标准目标和优化选项
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// ===== 库 =====
// 创建 TextKit 库模块
const textkit_mod = b.addModule("textkit", .{
.root_source_file = b.path("src/textkit.zig"),
.target = target,
});
// 构建静态库产物
const lib = b.addLibrary(.{
.name = "textkit",
.root_module = b.createModule(.{
.root_source_file = b.path("src/textkit.zig"),
.target = target,
.optimize = optimize,
}),
.version = .{ .major = 1, .minor = 0, .patch = 0 },
.linkage = .static,
});
// 安装库 (到 zig-out/lib/)
b.installArtifact(lib);
// ===== 可执行文件 =====
// 创建使用库的可执行文件
const exe = b.addExecutable(.{
.name = "textkit-cli",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "textkit", .module = textkit_mod },
},
}),
});
// 安装可执行文件 (到 zig-out/bin/)
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 TextKit CLI");
run_step.dependOn(&run_cmd.step);
// ===== 测试 =====
// 库测试
const lib_tests = b.addTest(.{
.root_module = textkit_mod,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
// 可执行文件测试 (main.zig 的最小测试)
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
// 运行所有测试的测试步骤
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step);
// ===== 自定义步骤 =====
// 展示用法的演示步骤
const demo_step = b.step("demo", "Run demo commands");
const demo_reverse = b.addRunArtifact(exe);
demo_reverse.addArgs(&.{ "reverse", "Hello Zig!" });
demo_step.dependOn(&demo_reverse.step);
const demo_count = b.addRunArtifact(exe);
demo_count.addArgs(&.{ "count", "mississippi", "s" });
demo_step.dependOn(&demo_count.step);
}
两个模块创建服务于不同目的:
textkit_mod:供使用者使用的公共模块(通过b.addModule)lib:具有单独模块配置的静态库构件
库模块仅指定.target,因为优化是面向用户的,而库构件需要.target和.optimize用于编译。
我们使用.linkage = .static来生成.a归档文件;更改为.dynamic用于.so/.dylib/.dll共享库。22
带库导入的可执行文件
const std = @import("std");
pub fn build(b: *std.Build) void {
// 标准目标和优化选项
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// ===== 库 =====
// 创建 TextKit 库模块
const textkit_mod = b.addModule("textkit", .{
.root_source_file = b.path("src/textkit.zig"),
.target = target,
});
// 构建静态库产物
const lib = b.addLibrary(.{
.name = "textkit",
.root_module = b.createModule(.{
.root_source_file = b.path("src/textkit.zig"),
.target = target,
.optimize = optimize,
}),
.version = .{ .major = 1, .minor = 0, .patch = 0 },
.linkage = .static,
});
// 安装库 (到 zig-out/lib/)
b.installArtifact(lib);
// ===== 可执行文件 =====
// 创建使用库的可执行文件
const exe = b.addExecutable(.{
.name = "textkit-cli",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "textkit", .module = textkit_mod },
},
}),
});
// 安装可执行文件 (到 zig-out/bin/)
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 TextKit CLI");
run_step.dependOn(&run_cmd.step);
// ===== 测试 =====
// 库测试
const lib_tests = b.addTest(.{
.root_module = textkit_mod,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
// 可执行文件测试 (main.zig 的最小测试)
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
// 运行所有测试的测试步骤
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step);
// ===== 自定义步骤 =====
// 展示用法的演示步骤
const demo_step = b.step("demo", "Run demo commands");
const demo_reverse = b.addRunArtifact(exe);
demo_reverse.addArgs(&.{ "reverse", "Hello Zig!" });
demo_step.dependOn(&demo_reverse.step);
const demo_count = b.addRunArtifact(exe);
demo_count.addArgs(&.{ "count", "mississippi", "s" });
demo_step.dependOn(&demo_count.step);
}
.imports表将main.zig连接到库模块,启用@import("textkit")。名称"textkit"是任意的——你可以将其重命名为"lib"并使用@import("lib")。
带参数转发的运行步骤
const std = @import("std");
pub fn build(b: *std.Build) void {
// 标准目标和优化选项
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// ===== 库 =====
// 创建 TextKit 库模块
const textkit_mod = b.addModule("textkit", .{
.root_source_file = b.path("src/textkit.zig"),
.target = target,
});
// 构建静态库产物
const lib = b.addLibrary(.{
.name = "textkit",
.root_module = b.createModule(.{
.root_source_file = b.path("src/textkit.zig"),
.target = target,
.optimize = optimize,
}),
.version = .{ .major = 1, .minor = 0, .patch = 0 },
.linkage = .static,
});
// 安装库 (到 zig-out/lib/)
b.installArtifact(lib);
// ===== 可执行文件 =====
// 创建使用库的可执行文件
const exe = b.addExecutable(.{
.name = "textkit-cli",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "textkit", .module = textkit_mod },
},
}),
});
// 安装可执行文件 (到 zig-out/bin/)
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 TextKit CLI");
run_step.dependOn(&run_cmd.step);
// ===== 测试 =====
// 库测试
const lib_tests = b.addTest(.{
.root_module = textkit_mod,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
// 可执行文件测试 (main.zig 的最小测试)
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
// 运行所有测试的测试步骤
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step);
// ===== 自定义步骤 =====
// 展示用法的演示步骤
const demo_step = b.step("demo", "Run demo commands");
const demo_reverse = b.addRunArtifact(exe);
demo_reverse.addArgs(&.{ "reverse", "Hello Zig!" });
demo_step.dependOn(&demo_reverse.step);
const demo_count = b.addRunArtifact(exe);
demo_count.addArgs(&.{ "count", "mississippi", "s" });
demo_step.dependOn(&demo_count.step);
}
此标准模式:
创建一个运行构件步骤
依赖于安装(确保二进制文件在
zig-out/bin/中)转发
--之后的CLI参数连接到顶级
run步骤
测试集成
const std = @import("std");
pub fn build(b: *std.Build) void {
// 标准目标和优化选项
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// ===== 库 =====
// 创建 TextKit 库模块
const textkit_mod = b.addModule("textkit", .{
.root_source_file = b.path("src/textkit.zig"),
.target = target,
});
// 构建静态库产物
const lib = b.addLibrary(.{
.name = "textkit",
.root_module = b.createModule(.{
.root_source_file = b.path("src/textkit.zig"),
.target = target,
.optimize = optimize,
}),
.version = .{ .major = 1, .minor = 0, .patch = 0 },
.linkage = .static,
});
// 安装库 (到 zig-out/lib/)
b.installArtifact(lib);
// ===== 可执行文件 =====
// 创建使用库的可执行文件
const exe = b.addExecutable(.{
.name = "textkit-cli",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "textkit", .module = textkit_mod },
},
}),
});
// 安装可执行文件 (到 zig-out/bin/)
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 TextKit CLI");
run_step.dependOn(&run_cmd.step);
// ===== 测试 =====
// 库测试
const lib_tests = b.addTest(.{
.root_module = textkit_mod,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
// 可执行文件测试 (main.zig 的最小测试)
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
// 运行所有测试的测试步骤
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step);
// ===== 自定义步骤 =====
// 展示用法的演示步骤
const demo_step = b.step("demo", "Run demo commands");
const demo_reverse = b.addRunArtifact(exe);
demo_reverse.addArgs(&.{ "reverse", "Hello Zig!" });
demo_step.dependOn(&demo_reverse.step);
const demo_count = b.addRunArtifact(exe);
demo_count.addArgs(&.{ "count", "mississippi", "s" });
demo_step.dependOn(&demo_count.step);
}
分离库和可执行文件测试可以隔离故障并启用并行执行。两者都依赖于相同的test步骤,因此zig build test会运行所有测试。13
自定义演示步骤
const std = @import("std");
pub fn build(b: *std.Build) void {
// 标准目标和优化选项
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// ===== 库 =====
// 创建 TextKit 库模块
const textkit_mod = b.addModule("textkit", .{
.root_source_file = b.path("src/textkit.zig"),
.target = target,
});
// 构建静态库产物
const lib = b.addLibrary(.{
.name = "textkit",
.root_module = b.createModule(.{
.root_source_file = b.path("src/textkit.zig"),
.target = target,
.optimize = optimize,
}),
.version = .{ .major = 1, .minor = 0, .patch = 0 },
.linkage = .static,
});
// 安装库 (到 zig-out/lib/)
b.installArtifact(lib);
// ===== 可执行文件 =====
// 创建使用库的可执行文件
const exe = b.addExecutable(.{
.name = "textkit-cli",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "textkit", .module = textkit_mod },
},
}),
});
// 安装可执行文件 (到 zig-out/bin/)
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 TextKit CLI");
run_step.dependOn(&run_cmd.step);
// ===== 测试 =====
// 库测试
const lib_tests = b.addTest(.{
.root_module = textkit_mod,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
// 可执行文件测试 (main.zig 的最小测试)
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
// 运行所有测试的测试步骤
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step);
// ===== 自定义步骤 =====
// 展示用法的演示步骤
const demo_step = b.step("demo", "Run demo commands");
const demo_reverse = b.addRunArtifact(exe);
demo_reverse.addArgs(&.{ "reverse", "Hello Zig!" });
demo_step.dependOn(&demo_reverse.step);
const demo_count = b.addRunArtifact(exe);
demo_count.addArgs(&.{ "count", "mississippi", "s" });
demo_step.dependOn(&demo_count.step);
}
自定义步骤无需用户输入即可展示功能。zig build demo按顺序运行预定义命令,展示CLI的功能。
使用项目
TextKit支持构建、测试和运行的多种工作流。22
构建库和可执行文件
$ zig build- 库:
zig-out/lib/libtextkit.a - 可执行文件:
zig-out/bin/textkit-cli
两个构件默认都安装到标准位置。
运行测试
$ zig build testAll 5 tests passed.
string_utils.zig、text_stats.zig和main.zig中的测试一起运行,报告聚合结果。13
运行CLI
查看用法
$ zig build runTextKit CLI - Text processing utility
Usage:
textkit-cli analyze <file> Analyze text file statistics
textkit-cli reverse <text> Reverse the given text
textkit-cli count <text> <char> Count character occurrences反转文本
$ zig build run -- reverse "Hello World"Original: Hello World
Reversed: dlroW olleH统计字符
$ zig build run -- count "mississippi" "s"Character 's' appears 4 time(s) in: mississippi分析文件
$ zig build run -- analyze sample.txtFile: sample.txt
Lines: 7
Words: 51
Characters: 336
ASCII only: true运行演示步骤
$ zig build demoOriginal: Hello Zig!
Reversed: !giZ olleH
Character 's' appears 4 time(s) in: mississippi无需用户交互即可按顺序执行多个命令——对于CI/CD流水线或快速验证很有用。
对比构建工作流
理解何时使用zig build与zig build-exe可以阐明构建系统的目的。
使用直接编译
$ zig build-exe src/main.zig --name textkit-cli --pkg-begin textkit src/textkit.zig --pkg-end这个命令式命令:
- 无需构建图即可立即编译
- 需要手动指定所有模块和标志
- 不产生缓存或增量编译优势
- 适用于快速一次性构建或调试
使用进行基于图的构建
$ zig build这个声明式命令:
- 执行
build.zig来构建依赖图 - 缓存构件并跳过未更改的步骤
- 并行化独立编译
- 通过
-D标志支持用户自定义 - 集成测试、安装和自定义步骤
基于图的方法随着项目增长而扩展得更好,使zig build成为非平凡代码库的标准。22
设计模式和最佳实践
TextKit展示了几个值得采用的专业模式。
构建脚本模式
- 标准选项优先:始终以
standardTargetOptions()和standardOptimizeOption()开始 - 逻辑分组:注释部分(===== LIBRARY =====)提高可读性
- 构件安装:为用户应该访问的所有内容调用
installArtifact() - 测试分离:独立的库和可执行文件测试步骤隔离故障
CLI设计模式
- 子命令分发:中央路由器委托给处理函数
- 优雅降级:无效输入的用法消息
- 资源清理:
defer确保分配器和文件句柄清理 - 库分离:所有逻辑在库中,CLI是薄包装器
练习
注意事项
- 静态库(
.a文件)并非严格必需,因为Zig可以直接链接模块,但生成库构件展示了传统的库分发模式。 - 当同时创建公共模块(
b.addModule)和库构件(b.addLibrary)时,确保两者指向相同的根源文件以避免混淆。 installArtifact()步骤默认安装到zig-out/;使用.prefix选项覆盖以使用自定义安装路径。main.zig中的测试通常仅验证可执行文件是否编译;全面的功能测试属于库模块。13
注意事项、替代方案和边界情况
- 如果库是仅头文件的(没有运行时代码),你不需要
addLibrary()——仅模块定义就足够了。20 - Zig 0.14.0弃用了
ExecutableOptions中的直接root_source_file;始终使用root_module包装器,如这里所示。 - 对于C互操作场景,你需要添加
lib.linkLibC(),并可能使用lib.addCSourceFile()加上installHeader()生成头文件。 - 大型项目可能将
build.zig拆分为辅助函数或通过@import("build_helpers.zig")包含的单独文件——构建脚本是常规的Zig代码。