빠른 시작
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를 그대로 쓰지 마십시오. 이를 공유하는 두 프로젝트가 Zig 패키지 캐시에서 충돌합니다. 그 줄을 지우고 zig build를 한 번 실행한 뒤 오류에 나온 값을 다시 붙여 넣으십시오. 이참에 .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 호출 한 줄만이 아니라 테스트 스위치 전달, 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 모듈 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를 만들고 마운트 함수를 한 번 호출한 뒤 이벤트 루프를 넘겨받습니다. 마운트 함수의 시그니처는 반드시 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를 참고하십시오.