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()确保所有模块测试都运行

此模式允许内部重组,而不会破坏消费者的导入。20, testing.zig

可执行文件实现

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

CLI结构和参数解析

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

2

命令处理函数

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

完整构建脚本

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

构建脚本部分说明

库创建

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

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

  • textkit_mod: 用于消费者的公共模块(通过b.addModule
  • lib: 具有独立模块配置的静态库工件

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

我们使用.linkage = .static生成一个.a存档文件;对于.so/.dylib/.dll共享库,请更改为.dynamic22

带库导入的可执行文件

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

.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,
    });
    
    // 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);
}

此标准模式:

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

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

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

  4. 连接到顶层run步骤

22, Run.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,
    });
    
    // 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

自定义演示步骤

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

自定义步骤可以在没有用户输入的情况下展示功能。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_utils, text_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.