概述
第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 {
// Set up a general-purpose allocator for dynamic memory allocation
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Retrieve command line arguments passed to the program
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
// Ensure at least one command argument is provided (args[0] is the program name)
if (args.len < 2) {
try printUsage();
return;
}
// Extract the command verb from the first argument
const command = args[1];
// Dispatch to the appropriate handler based on the command
if (std.mem.eql(u8, command, "analyze")) {
// 'analyze' requires a filename argument
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' requires text to 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' requires both text and a single character to count
if (args.len < 4) {
std.debug.print("Error: count requires text and character\n", .{});
return;
}
// Validate that the character argument is exactly one byte
if (args[3].len != 1) {
std.debug.print("Error: character must be single byte\n", .{});
return;
}
try countCharacter(args[2], args[3][0]);
} else {
// Handle unrecognized commands
std.debug.print("Unknown command: {s}\n", .{command});
try printUsage();
}
}
/// Print usage information to guide users on available commands
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});
}
/// Read a file and display statistical analysis of its text content
fn analyzeFile(allocator: std.mem.Allocator, filename: []const u8) !void {
// Open the file in read-only mode from the current working directory
const file = try std.fs.cwd().openFile(filename, .{});
defer file.close();
// Read the entire file content into memory (limited to 1MB)
const content = try file.readToEndAlloc(allocator, 1024 * 1024);
defer allocator.free(content);
// Use textkit library to compute text statistics
const stats = textkit.TextStats.analyze(content);
// Display the computed statistics to the user
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)});
}
/// Reverse the provided text and display both original and reversed versions
fn reverseText(text: []const u8) !void {
// Allocate a stack buffer for in-place reversal
var buffer: [1024]u8 = undefined;
// Ensure the input text fits within the buffer
if (text.len > buffer.len) {
std.debug.print("Error: text too long (max {d} chars)\n", .{buffer.len});
return;
}
// Copy input text into the mutable buffer for reversal
@memcpy(buffer[0..text.len], text);
// Perform in-place reversal using textkit utility
textkit.StringUtils.reverse(buffer[0..text.len]);
// Display both the original and reversed text
std.debug.print("Original: {s}\n", .{text});
std.debug.print("Reversed: {s}\n", .{buffer[0..text.len]});
}
/// Count occurrences of a specific character in the provided text
fn countCharacter(text: []const u8, char: u8) !void {
// Use textkit to count character occurrences
const count = textkit.StringUtils.countChar(text, char);
// Display the count result
std.debug.print("Character '{c}' appears {d} time(s) in: {s}\n", .{
char,
count,
text,
});
}
// Test that all declarations in this module are reachable and compile correctly
test "main program compiles" {
std.testing.refAllDecls(@This());
}
可执行文件演示了:
- 命令分派: 将子命令路由到处理函数
- 参数验证: 检查参数数量和格式
- 错误处理: 带有信息性消息的优雅失败
- 库使用: 通过
@import("textkit")进行干净的导入
命令处理函数
const std = @import("std");
const textkit = @import("textkit");
pub fn main() !void {
// Set up a general-purpose allocator for dynamic memory allocation
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Retrieve command line arguments passed to the program
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
// Ensure at least one command argument is provided (args[0] is the program name)
if (args.len < 2) {
try printUsage();
return;
}
// Extract the command verb from the first argument
const command = args[1];
// Dispatch to the appropriate handler based on the command
if (std.mem.eql(u8, command, "analyze")) {
// 'analyze' requires a filename argument
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' requires text to 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' requires both text and a single character to count
if (args.len < 4) {
std.debug.print("Error: count requires text and character\n", .{});
return;
}
// Validate that the character argument is exactly one byte
if (args[3].len != 1) {
std.debug.print("Error: character must be single byte\n", .{});
return;
}
try countCharacter(args[2], args[3][0]);
} else {
// Handle unrecognized commands
std.debug.print("Unknown command: {s}\n", .{command});
try printUsage();
}
}
/// Print usage information to guide users on available commands
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});
}
/// Read a file and display statistical analysis of its text content
fn analyzeFile(allocator: std.mem.Allocator, filename: []const u8) !void {
// Open the file in read-only mode from the current working directory
const file = try std.fs.cwd().openFile(filename, .{});
defer file.close();
// Read the entire file content into memory (limited to 1MB)
const content = try file.readToEndAlloc(allocator, 1024 * 1024);
defer allocator.free(content);
// Use textkit library to compute text statistics
const stats = textkit.TextStats.analyze(content);
// Display the computed statistics to the user
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)});
}
/// Reverse the provided text and display both original and reversed versions
fn reverseText(text: []const u8) !void {
// Allocate a stack buffer for in-place reversal
var buffer: [1024]u8 = undefined;
// Ensure the input text fits within the buffer
if (text.len > buffer.len) {
std.debug.print("Error: text too long (max {d} chars)\n", .{buffer.len});
return;
}
// Copy input text into the mutable buffer for reversal
@memcpy(buffer[0..text.len], text);
// Perform in-place reversal using textkit utility
textkit.StringUtils.reverse(buffer[0..text.len]);
// Display both the original and reversed text
std.debug.print("Original: {s}\n", .{text});
std.debug.print("Reversed: {s}\n", .{buffer[0..text.len]});
}
/// Count occurrences of a specific character in the provided text
fn countCharacter(text: []const u8, char: u8) !void {
// Use textkit to count character occurrences
const count = textkit.StringUtils.countChar(text, char);
// Display the count result
std.debug.print("Character '{c}' appears {d} time(s) in: {s}\n", .{
char,
count,
text,
});
}
// Test that all declarations in this module are reachable and compile correctly
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,
});
// Install the library (to zig-out/lib/)
b.installArtifact(lib);
// ===== EXECUTABLE =====
// Create executable that uses the library
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 },
},
}),
});
// Install the executable (to zig-out/bin/)
b.installArtifact(exe);
// ===== RUN STEP =====
// Create a run step for the executable
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
// Forward command-line arguments to the application
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the TextKit CLI");
run_step.dependOn(&run_cmd.step);
// ===== TESTS =====
// Library tests
const lib_tests = b.addTest(.{
.root_module = textkit_mod,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
// Executable tests (minimal for main.zig)
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
// Test step that runs all tests
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step);
// ===== CUSTOM STEPS =====
// Demo step that shows usage
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,
});
// Install the library (to zig-out/lib/)
b.installArtifact(lib);
// ===== EXECUTABLE =====
// Create executable that uses the library
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 },
},
}),
});
// Install the executable (to zig-out/bin/)
b.installArtifact(exe);
// ===== RUN STEP =====
// Create a run step for the executable
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
// Forward command-line arguments to the application
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the TextKit CLI");
run_step.dependOn(&run_cmd.step);
// ===== TESTS =====
// Library tests
const lib_tests = b.addTest(.{
.root_module = textkit_mod,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
// Executable tests (minimal for main.zig)
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
// Test step that runs all tests
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step);
// ===== CUSTOM STEPS =====
// Demo step that shows usage
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存档文件;对于.so/.dylib/.dll共享库,请更改为.dynamic。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,
});
// Install the library (to zig-out/lib/)
b.installArtifact(lib);
// ===== EXECUTABLE =====
// Create executable that uses the library
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 },
},
}),
});
// Install the executable (to zig-out/bin/)
b.installArtifact(exe);
// ===== RUN STEP =====
// Create a run step for the executable
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
// Forward command-line arguments to the application
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the TextKit CLI");
run_step.dependOn(&run_cmd.step);
// ===== TESTS =====
// Library tests
const lib_tests = b.addTest(.{
.root_module = textkit_mod,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
// Executable tests (minimal for main.zig)
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
// Test step that runs all tests
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step);
// ===== CUSTOM STEPS =====
// Demo step that shows usage
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,
});
// Install the library (to zig-out/lib/)
b.installArtifact(lib);
// ===== EXECUTABLE =====
// Create executable that uses the library
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 },
},
}),
});
// Install the executable (to zig-out/bin/)
b.installArtifact(exe);
// ===== RUN STEP =====
// Create a run step for the executable
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
// Forward command-line arguments to the application
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the TextKit CLI");
run_step.dependOn(&run_cmd.step);
// ===== TESTS =====
// Library tests
const lib_tests = b.addTest(.{
.root_module = textkit_mod,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
// Executable tests (minimal for main.zig)
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
// Test step that runs all tests
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step);
// ===== CUSTOM STEPS =====
// Demo step that shows usage
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,
});
// Install the library (to zig-out/lib/)
b.installArtifact(lib);
// ===== EXECUTABLE =====
// Create executable that uses the library
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 },
},
}),
});
// Install the executable (to zig-out/bin/)
b.installArtifact(exe);
// ===== RUN STEP =====
// Create a run step for the executable
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
// Forward command-line arguments to the application
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the TextKit CLI");
run_step.dependOn(&run_cmd.step);
// ===== TESTS =====
// Library tests
const lib_tests = b.addTest(.{
.root_module = textkit_mod,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
// Executable tests (minimal for main.zig)
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
// Test step that runs all tests
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step);
// ===== CUSTOM STEPS =====
// Demo step that shows usage
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,
});
// Install the library (to zig-out/lib/)
b.installArtifact(lib);
// ===== EXECUTABLE =====
// Create executable that uses the library
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 },
},
}),
});
// Install the executable (to zig-out/bin/)
b.installArtifact(exe);
// ===== RUN STEP =====
// Create a run step for the executable
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
// Forward command-line arguments to the application
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the TextKit CLI");
run_step.dependOn(&run_cmd.step);
// ===== TESTS =====
// Library tests
const lib_tests = b.addTest(.{
.root_module = textkit_mod,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
// Executable tests (minimal for main.zig)
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
// Test step that runs all tests
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step);
// ===== CUSTOM STEPS =====
// Demo step that shows usage
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代码。