Chapter 23Project Library And Executable Workspace

项目

概述

第22章讲解了std.Build API的机制;本章通过一个完整的项目来巩固这些知识:TextKit,一个文本处理库,配有一个CLI工具,展示了构建工作区、组织模块、链接构件、集成测试和创建自定义构建步骤的实际模式。参见22Build.zig

通过逐步了解TextKit的实现——从模块组织到构建脚本编排——你将理解专业的Zig项目如何在可重用库和特定应用的可执行文件之间分离关注点,同时维护处理编译、测试和分发的单一统一构建图。参见21Compile.zig

学习目标

  • 构建一个包含库和可执行构件的工作区,共享一个共同的build.zig
  • 将库代码组织成多个模块以提高可维护性和可测试性。20
  • 使用b.addLibrary()构建静态库并安装以供外部使用。
  • 创建一个导入并使用库模块的可执行文件。22
  • 为库和可执行组件集成全面的测试。13
  • 定义超出默认安装、运行和测试目标的自定义构建步骤。
  • 理解zig build(基于图)和zig build-exe(命令式)之间的对比。

项目结构:TextKit

TextKit是一个文本处理工具,包含:

  • 库():作为模块公开的可重用文本处理函数
  • 可执行文件():使用该库的命令行界面
  • 测试:对库功能的全面覆盖
  • 自定义步骤:超出标准构建/测试/运行的演示命令

目录布局

Text
textkit/
├── build.zig              # 构建图定义
├── build.zig.zon          # 包元数据
├── sample.txt             # 演示输入文件
└── src/
    ├── textkit.zig        # 库根文件(公共API)
    ├── string_utils.zig   # 字符串操作工具
    ├── text_stats.zig     # 文本分析函数
    └── main.zig           # CLI可执行文件入口点

此布局遵循Zig约定:src/包含所有源文件,build.zig编排编译过程,build.zig.zon声明包标识。参见21init模板

库实现

TextKit库公开了两个主要模块:用于字符级操作的StringUtils和用于文档分析的TextStats。参见Module.zig

字符串工具模块

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验证、原地反转

文本统计模块

Zig
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格式化接口
  • 全面测试:边界情况(空文本、无尾随换行符)

参见v0.15.2Io.zig

库根文件:公共API

Zig
// ! 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.StringUtilstextkit.TextStats访问
  • 版本元数据:供外部使用者使用的语义版本
  • 测试聚合std.testing.refAllDecls()确保所有模块测试运行

这种模式允许内部重组而不破坏使用者导入。20testing.zig

可执行文件实现

CLI工具将库功能包装在用户友好的命令行界面中,为不同操作提供子命令。process.zig

CLI结构和参数解析

Zig
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")进行干净的导入

2

命令处理函数

Zig
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文件将所有内容联系在一起,定义库和可执行文件之间的关系以及用户如何与项目交互。

完整的构建脚本

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

构建脚本部分解释

库创建

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

两个模块创建服务于不同目的:

  • textkit_mod:供使用者使用的公共模块(通过b.addModule
  • lib:具有单独模块配置的静态库构件

库模块仅指定.target,因为优化是面向用户的,而库构件需要.target.optimize用于编译。

我们使用.linkage = .static来生成.a归档文件;更改为.dynamic用于.so/.dylib/.dll共享库。22

带库导入的可执行文件

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

.imports表将main.zig连接到库模块,启用@import("textkit")。名称"textkit"是任意的——你可以将其重命名为"lib"并使用@import("lib")

带参数转发的运行步骤

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

此标准模式:

  1. 创建一个运行构件步骤

  2. 依赖于安装(确保二进制文件在zig-out/bin/中)

  3. 转发--之后的CLI参数

  4. 连接到顶级run步骤

22Run.zig

测试集成

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

分离库和可执行文件测试可以隔离故障并启用并行执行。两者都依赖于相同的test步骤,因此zig build test会运行所有测试。13

自定义演示步骤

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

自定义步骤无需用户输入即可展示功能。zig build demo按顺序运行预定义命令,展示CLI的功能。

使用项目

TextKit支持构建、测试和运行的多种工作流。22

构建库和可执行文件

Shell
$ zig build
  • 库:zig-out/lib/libtextkit.a
  • 可执行文件:zig-out/bin/textkit-cli

两个构件默认都安装到标准位置。

运行测试

Shell
$ zig build test
输出(成功)
All 5 tests passed.

string_utils.zigtext_stats.zigmain.zig中的测试一起运行,报告聚合结果。13

运行CLI

查看用法

Shell
$ zig build run
输出
Shell
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

反转文本

Shell
$ zig build run -- reverse "Hello World"
输出
Shell
Original: Hello World
Reversed: dlroW olleH

统计字符

Shell
$ zig build run -- count "mississippi" "s"
输出
Shell
Character 's' appears 4 time(s) in: mississippi

分析文件

Shell
$ zig build run -- analyze sample.txt
输出
Shell
File: sample.txt
  Lines: 7
  Words: 51
  Characters: 336
  ASCII only: true

运行演示步骤

Shell
$ zig build demo
输出
Shell
Original: Hello Zig!
Reversed: !giZ olleH
Character 's' appears 4 time(s) in: mississippi

无需用户交互即可按顺序执行多个命令——对于CI/CD流水线或快速验证很有用。

对比构建工作流

理解何时使用zig buildzig build-exe可以阐明构建系统的目的。

使用直接编译

Shell
$ zig build-exe src/main.zig --name textkit-cli --pkg-begin textkit src/textkit.zig --pkg-end

这个命令式命令:

  • 无需构建图即可立即编译
  • 需要手动指定所有模块和标志
  • 不产生缓存或增量编译优势
  • 适用于快速一次性构建或调试

使用进行基于图的构建

Shell
$ zig build

这个声明式命令:

  • 执行build.zig来构建依赖图
  • 缓存构件并跳过未更改的步骤
  • 并行化独立编译
  • 通过-D标志支持用户自定义
  • 集成测试、安装和自定义步骤

基于图的方法随着项目增长而扩展得更好,使zig build成为非平凡代码库的标准。22

设计模式和最佳实践

TextKit展示了几个值得采用的专业模式。

模块组织

  • 单一职责:每个模块(string_utilstext_stats)专注于一个关注点
  • 根重新导出textkit.zig提供统一的公共API
  • 测试共位:测试与实现相邻以提高可维护性

20

构建脚本模式

  • 标准选项优先:始终以standardTargetOptions()standardOptimizeOption()开始
  • 逻辑分组:注释部分(===== LIBRARY =====)提高可读性
  • 构件安装:为用户应该访问的所有内容调用installArtifact()
  • 测试分离:独立的库和可执行文件测试步骤隔离故障

22

CLI设计模式

  • 子命令分发:中央路由器委托给处理函数
  • 优雅降级:无效输入的用法消息
  • 资源清理defer确保分配器和文件句柄清理
  • 库分离:所有逻辑在库中,CLI是薄包装器

练习

  • 添加一个新的子命令trim,用于从文本中删除前导/尾随空格,在string_utils.zig中实现该函数并添加测试。ascii.zig
  • 将库从静态(.linkage = .static)转换为动态(.linkage = .dynamic)并观察输出文件的差异。
  • 创建第二个可执行文件textkit-batch,使用线程并行处理多个文件,共享相同的库模块。37
  • 添加一个自定义构建步骤bench,在各种输入大小下对StringUtils.reverse运行性能基准测试。

注意事项

  • 静态库(.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代码。

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.