解答14: ビルドシステムの実践

概要

この解答では、Zigのビルドシステムを使って、実行ファイル・ライブラリのビルド、カスタムビルドオプション、マルチターゲットビルド、カスタムビルドステップについて学びます。

解答例

Part 1: 基本的なbuild.zigの作成

part1/src/lib.zig:

pub fn add(a: i32, b: i32) i32 {
    return a + b;
}

pub fn subtract(a: i32, b: i32) i32 {
    return a - b;
}

pub fn multiply(a: i32, b: i32) i32 {
    return a * b;
}

pub fn divide(a: i32, b: i32) !i32 {
    if (b == 0) return error.DivisionByZero;
    return @divTrunc(a, b);
}

part1/src/main.zig:

const std = @import("std");
const lib = @import("lib.zig");

pub fn main() !void {
    std.debug.print("Calculator Demo\n", .{});
    std.debug.print("2 + 3 = {}\n", .{lib.add(2, 3)});
    std.debug.print("5 - 3 = {}\n", .{lib.subtract(5, 3)});
    std.debug.print("4 * 5 = {}\n", .{lib.multiply(4, 5)});
    std.debug.print("10 / 2 = {}\n", .{try lib.divide(10, 2)});
}

part1/build.zig:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // 静的ライブラリをビルド
    const lib = b.addStaticLibrary(.{
        .name = "mathlib",
        .root_source_file = b.path("src/lib.zig"),
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(lib);

    // 実行ファイルをビルド
    const exe = b.addExecutable(.{
        .name = "calculator",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // ライブラリモジュールを追加
    exe.root_module.addAnonymousImport("lib.zig", .{
        .root_source_file = b.path("src/lib.zig"),
    });

    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 calculator");
    run_step.dependOn(&run_cmd.step);

    // テストステップを追加
    const lib_tests = b.addTest(.{
        .root_source_file = b.path("src/lib.zig"),
        .target = target,
        .optimize = optimize,
    });

    const run_tests = b.addRunArtifact(lib_tests);

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

Part 2: カスタムビルドオプション

part2/src/main.zig:

const std = @import("std");
const config = @import("config");

pub fn main() !void {
    std.debug.print("=== {} v{s} ===\n\n", .{config.app_name, config.version});

    if (config.enable_logging) {
        std.debug.print("Logging enabled\n", .{});
        std.debug.print("Log level: {}\n\n", .{config.log_level});
    }

    std.debug.print("Application running...\n", .{});
}

part2/build.zig:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // カスタムビルドオプションを追加
    const enable_logging = b.option(bool, "enable-logging", "Enable logging") orelse false;
    const log_level = b.option(enum { Debug, Info, Warning, Error }, "log-level", "Log level") orelse .Info;
    const app_name = b.option([]const u8, "app-name", "Application name") orelse "MyApp";
    const version = b.option([]const u8, "version", "Application version") orelse "1.0.0";

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

    // ビルドオプションをコードに渡す
    const options = b.addOptions();
    options.addOption(bool, "enable_logging", enable_logging);
    options.addOption(@TypeOf(log_level), "log_level", log_level);
    options.addOption([]const u8, "app_name", app_name);
    options.addOption([]const u8, "version", version);

    exe.root_module.addOptions("config", options);

    b.installArtifact(exe);

    // 実行ステップ
    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());

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

Part 3: マルチターゲットビルド

const std = @import("std");

pub fn build(b: *std.Build) void {
    const optimize = b.standardOptimizeOption(.{});

    // 複数のターゲット向けにビルド
    const targets = [_]std.Target.Query{
        .{ .cpu_arch = .x86_64, .os_tag = .linux },
        .{ .cpu_arch = .x86_64, .os_tag = .windows },
        .{ .cpu_arch = .aarch64, .os_tag = .macos },
        .{ .cpu_arch = .wasm32, .os_tag = .freestanding },
    };

    const build_all_step = b.step("build-all", "Build for all targets");

    for (targets) |target_query| {
        const resolved_target = b.resolveTargetQuery(target_query);

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

        // ターゲット別のディレクトリにインストール
        const target_triple = resolved_target.result.zigTriple(b.allocator) catch @panic("OOM");

        const install = b.addInstallArtifact(exe, .{
            .dest_dir = .{
                .override = .{
                    .custom = target_triple,
                },
            },
        });

        build_all_step.dependOn(&install.step);
    }

    // デフォルトターゲット
    const target = b.standardTargetOptions(.{});
    const exe = b.addExecutable(.{
        .name = "multiplatform",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    const run_step = b.step("run", "Run for native target");
    run_step.dependOn(&run_cmd.step);
}

Part 4: カスタムビルドステップ

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // バージョンファイルを生成
    const version_step = b.addWriteFiles();
    const timestamp = std.time.timestamp();
    const version_content = std.fmt.allocPrint(
        b.allocator,
        "1.0.0-{}",
        .{timestamp}
    ) catch @panic("OOM");
    const version_file = version_step.add("version.txt", version_content);

    // 設定ファイルを生成
    const config_step = b.addWriteFiles();
    const config_content =
        \\{
        \\  "name": "MyApp",
        \\  "debug": true,
        \\  "max_connections": 100
        \\}
    ;
    const config_file = config_step.add("config.json", config_content);

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

    // ビルド前にファイル生成
    exe.step.dependOn(&version_step.step);
    exe.step.dependOn(&config_step.step);

    b.installArtifact(exe);
    b.installFile(version_file, "version.txt");
    b.installFile(config_file, "config.json");

    // フォーマットステップ
    const fmt_step = b.addFmt(.{
        .paths = &[_][]const u8{ "src", "build.zig" },
        .check = false,
    });

    const fmt_cmd = b.step("fmt", "Format source code");
    fmt_cmd.dependOn(&fmt_step.step);

    // クリーンステップ
    const clean_step = b.step("clean", "Remove build artifacts");
    const rm_cmd = b.addRemoveDirTree(b.install_path);
    clean_step.dependOn(&rm_cmd.step);

    // 実行ステップ
    const run_cmd = b.addRunArtifact(exe);
    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

ポイント解説

1. ビルドステップの依存関係

step.dependOn()を使って、ビルドステップの実行順序を制御します。

2. カスタムターゲット

resolveTargetQuery()を使って、任意のプラットフォーム向けにビルドできます。

3. ビルドオプションの活用

コンパイル時定数として、ビルドオプションをコードに渡せます。

4. カスタムステップ

addWriteFiles(), addFmt(), addRemoveDirTree()などで独自のビルド処理を追加できます。

よくある間違い

1. パスの指定ミス

// 間違い
.root_source_file = .{ .path = "src/main.zig" },

// 正しい
.root_source_file = b.path("src/main.zig"),

2. ステップ依存の順序

// 間違い
const run_step = b.step("run", "Run");
run_step.dependOn(&run_cmd.step);  // run_cmdがまだ作られていない

// 正しい
const run_cmd = b.addRunArtifact(exe);
const run_step = b.step("run", "Run");
run_step.dependOn(&run_cmd.step);

発展課題

Challenge 1: パッケージマネージャー

複数の外部パッケージを管理するビルドシステムを作成してください。

Challenge 2: インクリメンタルビルド

変更されたファイルのみを再コンパイルするシステムを実装してください。

Challenge 3: プラグインシステム

動的にプラグインをロードできるビルドシステムを作成してください。

Challenge 4: ドキュメント生成

ソースコードからAPIドキュメントを自動生成してください。

Challenge 5: CI/CD統合

GitHub ActionsやGitLab CIと統合できるビルド設定を作成してください。

まとめ

この課題を通じて、以下を学びました:

  • 基本的なbuild.zig: ライブラリと実行ファイルのビルド
  • カスタムオプション: ビルド時のカスタマイズ
  • クロスコンパイル: 複数プラットフォーム向けビルド
  • カスタムステップ: 独自のビルド処理

Zigのビルドシステムは、シンプルでありながら強力な機能を提供します。