クイックスタート
zenit リポジトリの外に置き、単独でビルド・実行できるネイティブ macOS アプリを作成します。
前提条件
このリリースは macOS を対象とし、Apple Silicon で継続的に検証しています。Zig 0.15.2 と Xcode Command Line Tools があることを確認してください。Bun はエンドツーエンドテストを実行する場合にのみ必要です。
zig version
xcode-select -pプロジェクトを作成
リポジトリの templates/minimal-app から始めます。これは完全な下流プロジェクトです:
minimal-app/
├── README.md
├── build.zig # zenit.attach() + run / app steps
├── build.zig.zon # declares the zenit dependency
├── e2e/
│ └── record-demo.ts # drives and records the app through the Harness
└── src/
└── main.zig # counter-button app- 1テンプレートをコピー
templates/minimal-app を zenit リポジトリ外の新しいディレクトリにコピーします。
- 2依存パスを修正
テンプレートの既定値
.path = "../../"はリポジトリ内でしか解決できません。コピー後、build.zig.zon からの相対パスで zenit の checkout を指すよう変更するか、zig fetch --save=zenit <url>で公開リビジョンに固定してください。 - 3フィンガープリントを生成
テンプレートの .fingerprint を流用しないでください。共有した 2 つのプロジェクトが Zig のパッケージキャッシュで衝突します。その行を削除して zig build を 1 回実行し、エラーに表示された値を貼り戻します。ついでに .name = .myapp も変更してください。
git clone https://github.com/version-next/zenit.git
cp -R zenit/templates/minimal-app ./myapp
cd myapp # then set .zenit = .{ .path = "../zenit" } in build.zig.zon
zig buildzenit を接続
build.zig.zon が依存を宣言し、build.zig は @import("zenit") で zenit 自身のビルド API を取得します。
.{
.name = .myapp,
.version = "0.1.0",
.fingerprint = 0x8798022a7f8e220e, // replace: delete this line, run zig build once
.minimum_zig_version = "0.15.2",
.dependencies = .{
// Relative to this file. Absolute paths are rejected by Zig 0.15.2.
.zenit = .{ .path = "../zenit" },
},
.paths = .{
"README.md",
"build.zig",
"build.zig.zon",
"e2e",
"src",
},
}以下はテンプレートの build.zig そのままです。attach 1 行だけではなく、テスト用スイッチの転送、Harness クライアントのインストール、run ステップと .app パッケージングも含みます:
const std = @import("std");
const zenit = @import("zenit");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const test_mode = b.option(bool, "test-mode", "Enable the Zenit automation harness") orelse false;
const e2e_port = b.option(u16, "e2e-port", "Zenit Harness RPC directory suffix") orelse 19816;
const zenit_dep = b.dependency("zenit", .{
.target = target,
.optimize = optimize,
// Dependency build options are isolated in Zig. Forward these
// explicitly so `zig build -Dtest-mode=true` reaches Zenit.
.@"test-mode" = test_mode,
.@"e2e-port" = e2e_port,
});
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
zenit.attach(zenit_dep, exe);
zenit.installHarnessClient(b, zenit_dep);
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);
// Optional: `zig build app` packages a double-clickable .app bundle.
if (target.result.os.tag == .macos) {
const bundled = zenit.bundleApp(b, .{
.exe = exe,
.display_name = "My App",
.bundle_id = "com.example.myapp",
.version = "0.1.0",
.signing = .ad_hoc,
});
const app_step = b.step("app", "Build the .app bundle");
app_step.dependOn(bundled.final_step);
}
}zenit.attach(zenit_dep, exe)exe に ui と zenit_app の 2 モジュールの import を追加し、macOS の ObjC ブリッジ 5 つをコンパイルして、Cocoa・Metal・CoreText などのフレームワークをリンク.@"test-mode" / .@"e2e-port"Zig の依存オプションは分離されているため、明示的に転送しないと zig build -Dtest-mode=true が zenit に届かないzenit.installHarnessClient(b, zenit_dep)型付きの Harness クライアントを zig-out/share/zenit/harness/client.ts にインストールし、e2e/record-demo.ts から使うzenit.bundleApp(b, .{ ... })ダブルクリックで起動できる zig-out/<display_name>.app を生成。ここでは ad-hoc 署名最初の UI ツリーをマウント
App.runWith はルート Scope を作成し、マウント関数を 1 回呼び出して、イベントループを引き継ぎます。マウント関数のシグネチャは fn (cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node でなければならず、アプリはルートノードを返すだけです。
const std = @import("std");
const ui = @import("ui");
const App = @import("zenit_app").App;
const Counter = struct {
n: u32 = 0,
label: ?*ui.Node = null,
buf: [32]u8 = undefined,
pub fn increment(self: *Counter) void {
self.n += 1;
const node = self.label orelse return;
const content = std.fmt.bufPrint(&self.buf, "Clicked {d} times", .{self.n}) catch return;
if (node.getText()) |old| {
var t = old;
t.content = content;
node.setText(t);
}
node.markRenderDirty();
}
};
fn mountUi(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
const allocator = cx.allocator;
const counter = try cx.bindState(Counter, .{});
const click = cx.on(Counter, counter, Counter.increment);
const root = try ui.box(cx, .{
.width = .fill(),
.height = .fill(),
.direction = .column,
.gap = 16,
.padding = ui.Padding.all(40),
.background = cx.tokens.color.bg_primary,
.align_items = .center,
.justify = .center,
}, .{});
try root.appendChild(allocator, try ui.text(cx, "Hello, zenit!", .{
.font_size = 24,
.font_weight = 600,
.color = cx.tokens.color.fg_primary,
}));
const button = try ui.widgets.Button(.{
.label = "Click me",
.variant = .primary,
.on_click = click,
}).mount(scope, cx);
button.meta.ownership.meta.test_id = "counter.increment";
try root.appendChild(allocator, button);
const label = try ui.text(cx, "Clicked 0 times", .{
.font_size = 14,
.color = cx.tokens.color.fg_secondary,
});
try root.appendChild(allocator, label);
counter.label = label;
return root;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const app = try App.init(gpa.allocator(), .{
.window = .{ .width = 640, .height = 480, .title = "Hello, zenit" },
});
defer app.deinit();
try app.runWith(mountUi);
}cx.bindState は Counter を Cx に持たせ、cx.on はそのメソッドをボタンのコールバックに変えます。test_id により E2E スクリプトが意味でボタンを見つけられます。
ビルドと実行
zig build run # build and run the unbundled executable
zig build app # package zig-out/My App.app (macOS)テンプレートには録画スクリプトも付属しています。zig build -Dtest-mode=true でビルドしてアプリを起動し、e2e/record-demo.ts を実行すると同様の動画が得られます。詳しくは E2E harness を参照してください。