Chapter 3 - 构建系统 - Ziglang构建系统详解

12/02/2021

ziglang


title: "Chapter 3 - 构建系统" weight: 4 date: 2021-02-12 12:49:00 description: "Chapter 3 - Ziglang构建系统详解."

构建模式

Zig提供了四种构建模式,其中调试模式是默认的,因为它能产生最短的编译时间。.

运行时安全优化
DebugYesNo
ReleaseSafeYesYes, Speed
ReleaseSmallNoYes, Size
ReleaseFastNoYes, Speed

这些可以在zig runzig test中启用,参数为-O ReleaseSafe-O ReleaseSmall-O ReleaseFast.

建议用户在开发软件时启用运行时安全,尽管它在速度上有小的缺点.

输出一个可执行文件

命令zig build-exezig build-libzig build-obj可以分别用来输出可执行文件、库和对象。这些命令接收一个源文件和参数。

一些常见的参数:

让我们创建一个小小的hello world。保存为tiny-hello.zig,然后运行zig build-exe .\tiny-hello.zig -O ReleaseSmall -fstrip -fsingle-readed。目前对于x86_64-windows,这将产生一个2.5KiB的可执行文件。

const std = @import("std");

pub fn main() void {
    std.io.getStdOut().writeAll(
        "Hello World!",
    ) catch unreachable;
}

交叉编译

默认情况下,Zig将为你的CPU和操作系统的组合进行编译。这可以通过-target来重写。让我们把我们的小hello世界编译到64位的arm linux平台上.

zig build-exe .\tiny-hello.zig -O ReleaseSmall -fstrip -fsingle-threaded -target aarch64-linux

QEMU 或类似的东西可以用来方便地测试为国外平台制作的可执行文件.

一些可以交叉编译的CPU架构:

一些可以交叉编译的操作系统:

许多其他的目标可以用来编译,但目前还没有经过良好的测试。更多信息请参见Zig的支持表;经过良好测试的目标列表正在慢慢扩大.

由于Zig默认为您的特定CPU进行编译,这些二进制文件可能无法在其他CPU架构略有不同的计算机上运行。为了提高兼容性,指定一个特定的基线CPU型号可能是有用的。注意:选择一个较早的CPU架构会带来更大的兼容性,但也意味着你会错过较新的CPU指令;这里有一个效率/速度与兼容性的权衡.

让我们为sandybridge CPU(Intel x86_64,2011年左右)编译一个二进制文件,这样我们就可以合理地确定使用x86_64 CPU的人可以运行我们的二进制文件。这里我们可以用native来代替我们的CPU或操作系统,以使用我们系统的.

zig build-exe .\tiny-hello.zig -target x86_64-native -mcpu sandybridge

关于哪些架构、操作系统、CPU和ABI(关于ABI的细节在下一章),可以通过运行zig targets找到。注意:输出结果很长,你可能想把它放到一个文件里,例如zig targets > targets.json.

Zig Build

zig build命令允许用户根据build.zig文件进行编译。zig init-exezig init-lib可以用来给你一个基线项目.

让我们在一个新文件夹中使用zig init-exe。这就是你会发现的.

.
├── build.zig
└── src
    └── main.zig

build.zig包含我们的构建脚本。构建运行器*将使用这个 "pub fn build "函数作为其入口点--这就是你运行 "zig build "时执行的内容.

const Builder = @import("std").build.Builder;

pub fn build(b: *Builder) void {
    // 标准目标选项允许运行`zig build`的人选择
    // 为哪个目标进行构建。这里我们没有覆盖默认值,这
    // 意味着任何目标都是允许的,默认是本地的。其他选项
    // 可用于限制支持的目标集.
    const target = b.standardTargetOptions(.{});

    // 标准优化选项允许运行`zig build'的人选择
    // 在Debug、ReleaseSafe、ReleaseFast和ReleaseSmall之间选择。这里我们不
    // 设置一个首选的发布模式,允许用户决定如何优化.
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "init-exe",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });

    // 这宣布了当用户调用 "安装 "步骤时,可执行文件将被安装到标准位置的意图。
    // 当用户调用 "安装 "步骤(运行`zig build`时默认的
    // 运行`zig build`时的默认步骤).
    b.installArtifact(exe);

    const run_cmd = exe.run();
    run_cmd.step.dependOn(b.getInstallStep());

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

main.zig包含我们可执行程序的入口点。.

const std = @import("std");

pub fn main() anyerror!void {
    std.log.info("All your codebase are belong to us.", .{});
}

使用zig build命令后,可执行文件将出现在安装路径中。这里我们没有指定安装路径,所以可执行文件将被保存在./zig-out/bin.

Builder

Zig的std.Build类型包含构建运行器所使用的信息。这包括诸如以下信息:

CompileStep

std.build.CompileStep类型包含构建一个库、可执行文件、对象或测试所需的信息。

让我们利用我们的Builder',用Builder.addExecutable'创建一个`CompileStep',它接收一个名字和一个源码根的路径.

const Builder = @import("std").build.Builder;

pub fn build(b: *Builder) void {
    const exe = b.addExecutable(.{
        .name = "init-exe",
        .root_source_file = .{ .path = "src/main.zig" },
    });
    b.installArtifact(exe);
}

模块

Zig构建系统有一个模块的概念,它是用Zig编写的其他源文件.让我们来使用一个模块.

在一个新的文件夹中,运行以下命令.

zig init-exe
mkdir libs
cd libs
git clone https://github.com/Sobeston/table-helper.git

目录结构应该是这样的.

.
├── build.zig
├── libs
│   └── table-helper
│       ├── example-test.zig
│       ├── README.md
│       ├── table-helper.zig
│       └── zig.mod
└── src
    └── main.zig

在你新制作的build.zig中,添加以下几行.

    const table_helper = b.addModule("table-helper", .{
        .source_file = .{ .path = "libs/table-helper/table-helper.zig" }
    });
    exe.addModule("table-helper", table_helper);

现在,当通过zig build运行时,你的main.zig里面的@import将与 "table-helper "字符串一起工作。这意味着main拥有table-helper包。包(类型为std.build.Pkg)也有一个类型为?[]const Pkg的依赖域,默认为空。这允许你拥有依赖其他软件包的软件包.

在你的main.zig'中放置以下内容,并运行zig build run'.

const std = @import("std");
const Table = @import("table-helper").Table;

pub fn main() !void {
    try std.io.getStdOut().writer().print("{}\n", .{
        Table(&[_][]const u8{ "Version", "Date" }){
            .data = &[_][2][]const u8{
                .{ "0.7.1", "2020-12-13" },
                .{ "0.7.0", "2020-11-08" },
                .{ "0.6.0", "2020-04-13" },
                .{ "0.5.0", "2019-09-30" },
            },
        },
    });
}

这个表打印到你的控制台 .

Version Date       
------- ---------- 
0.7.1   2020-12-13 
0.7.0   2020-11-08 
0.6.0   2020-04-13 
0.5.0   2019-09-30 

Zig还没有一个官方的软件包管理器。然而一些非官方的实验性软件包管理器确实存在,即gyrozigmodtable-helper包就是为了支持这两个包而设计的。

一些寻找软件包的好地方包括: astroabe.pm, zpm, awesome-zig, 以及GitHub上的zig标签.

构建步骤

构建步骤是为构建运行器提供任务的一种方式。让我们创建一个构建步骤,并将其作为默认步骤。当你运行zig build时,它将输出Hello!.

const std = @import("std");

pub fn build(b: *std.build.Builder) void {
    const step = b.step("task", "do something");
    step.makeFn = myTask;
    b.default_step = step;
}

fn myTask(self: *std.build.Step, progress: *std.Progress.Node) !void {
    std.debug.print("Hello!\n", .{});
    _ = progress;
    _ = self;
}

我们之前调用了b.installArtifact(exe)--这增加了一个构建步骤,告诉构建器要构建可执行文件.

生成文档

Zig编译器带有自动生成文档的功能。这可以通过在zig build-{exe, lib, obj}zig run命令中添加femit-docs来调用。这些文档被保存在./docs中,作为一个小型静态网站。

Zig的文档生成利用了doc注释,它类似于注释,使用///而不是//,以及前面的globals。

在这里,我们将把它保存为x.zig',然后用zig build-lib -femit-docs x.zig -target native-windows'为它建立文档。这里有一些东西需要注意:

const std = @import("std");
const w = std.os.windows;

///**打开一个进程**, 获得一个句柄. 
///[MSDN](https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess)
pub extern "kernel32" fn OpenProcess(
    ///[进程访问权限](https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights)
    dwDesiredAccess: w.DWORD,
    ///
    bInheritHandle: w.BOOL,
    dwProcessId: w.DWORD,
) callconv(w.WINAPI) ?w.HANDLE;

///spreadsheet 位置
pub const Pos = struct{
    ///row
    x: u32,
    ///column
    y: u32,
};

pub const message = "hello!";

//用于强制分析,因为这些东西没有其他参考价值.
comptime {
    _ = OpenProcess;
    _ = Pos;
    _ = message;
}

//另一种方法是自动强制分析一切,但只在测试构建中:
test "Force analysis" {
    comptime {
        std.testing.refAllDecls(@This());
    }
}

使用build.zig时,可以通过在CompileStep上设置emit_docs字段为.emit来调用。我们可以创建一个生成文档的编译步骤,如下所示,然后用$ zig build docs来调用它.

const std = @import("std");

pub fn build(b: *std.build.Builder) void {
    const mode = b.standardReleaseOptions();

    const lib = b.addStaticLibrary("x", "src/x.zig");
    lib.setBuildMode(mode);
    lib.install();

    const tests = b.addTest("src/x.zig");
    tests.setBuildMode(mode);

    const test_step = b.step("test", "Run library tests");
    test_step.dependOn(&tests.step);

    //构建步骤来生成文档:
    const docs = b.addTest("src/x.zig");
    docs.setBuildMode(mode);
    docs.emit_docs = .emit;
    
    const docs_step = b.step("docs", "Generate docs");
    docs_step.dependOn(&docs.step);
}

这种生成方式是试验性的,在复杂的例子中经常失败。这被标准库文档所使用.

当合并错误集时,最左边的错误集的文档字符串优先于右边的。在这种情况下,C.PathNotFound的文档注释是A中提供的文档注释.

const A = error{
    NotDir,

    /// A doc comment
    PathNotFound,
};
const B = error{
    OutOfMemory,

    /// B doc comment
    PathNotFound,
};

const C = A || B;

第三章结束

本章还不完整。在未来,它将包含`zig build'的高级用法。

欢迎大家提出反馈意见和PR。