Skip to content

Commit 3940e03

Browse files
committed
Split main.zig into focussed modules
1 parent 12fa4bd commit 3940e03

7 files changed

Lines changed: 1188 additions & 1105 deletions

File tree

src/cache.zig

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
const std = @import("std");
2+
const log = @import("util/log.zig");
3+
const config = @import("config.zig");
4+
const oci_lib = @import("oci");
5+
const rootfs_builder = @import("rootfs/builder.zig");
6+
const oci_layout_writer = oci_lib.layout_writer;
7+
8+
const scoped_log = log.scoped("cache");
9+
10+
/// Compute a cache key from the effective layer list.
11+
/// The key is a sha256 of the normalized layer descriptions.
12+
pub fn computeBuildCacheKey(allocator: std.mem.Allocator, layers: []const config.Layer) ![64]u8 {
13+
var hasher = std.crypto.hash.sha2.Sha256.init(.{});
14+
15+
for (layers) |layer| {
16+
switch (layer) {
17+
.image => |ref| {
18+
hasher.update("image:");
19+
const normalized = config.normalizeImageRef(allocator, ref) catch ref;
20+
defer if (normalized.ptr != ref.ptr) allocator.free(normalized);
21+
hasher.update(normalized);
22+
},
23+
.rootfs => |path| {
24+
hasher.update("rootfs:");
25+
hasher.update(path);
26+
},
27+
}
28+
hasher.update("\n");
29+
}
30+
31+
var digest: [32]u8 = undefined;
32+
hasher.final(&digest);
33+
return std.fmt.bytesToHex(digest, .lower);
34+
}
35+
36+
/// Check if a build with the given cache key exists.
37+
/// Returns the path to the cached OCI layout directory, or null.
38+
pub fn checkBuildCache(allocator: std.mem.Allocator, cache_dir: []const u8, key: []const u8) ?[]const u8 {
39+
const cache_path = std.fmt.allocPrint(allocator, "{s}/builds/{s}", .{ cache_dir, key }) catch return null;
40+
41+
// Check if index.json exists in the cached build
42+
var buf: [std.fs.max_path_bytes]u8 = undefined;
43+
const index_path = std.fmt.bufPrint(&buf, "{s}/index.json", .{cache_path}) catch {
44+
allocator.free(cache_path);
45+
return null;
46+
};
47+
48+
std.fs.accessAbsolute(index_path, .{}) catch {
49+
allocator.free(cache_path);
50+
return null;
51+
};
52+
53+
return cache_path;
54+
}
55+
56+
/// Save a built rootfs as a cached OCI layout.
57+
pub fn saveBuildCache(allocator: std.mem.Allocator, cache_dir: []const u8, key: []const u8, rootfs_dir: []const u8, image_config: ?rootfs_builder.BuildResult.ImageConfig) void {
58+
const cache_path = std.fmt.allocPrint(allocator, "{s}/builds/{s}", .{ cache_dir, key }) catch return;
59+
defer allocator.free(cache_path);
60+
61+
// Ensure cache directory structure exists
62+
{
63+
std.fs.makeDirAbsolute(cache_dir) catch |err| {
64+
if (err != error.PathAlreadyExists) {
65+
// Try creating parent dirs
66+
if (std.fs.path.dirname(cache_dir)) |parent| {
67+
var root = std.fs.openDirAbsolute("/", .{}) catch return;
68+
defer root.close();
69+
if (parent.len > 1) root.makePath(parent[1..]) catch return;
70+
}
71+
std.fs.makeDirAbsolute(cache_dir) catch return;
72+
}
73+
};
74+
var dir = std.fs.openDirAbsolute(cache_dir, .{}) catch return;
75+
defer dir.close();
76+
dir.makePath("builds") catch return;
77+
}
78+
79+
// Write OCI layout to cache
80+
_ = oci_layout_writer.writeOciLayout(allocator, rootfs_dir, cache_path, image_config) catch |err| {
81+
scoped_log.warn("Failed to save build cache: {}", .{err});
82+
};
83+
}

src/cmd/build.zig

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
const std = @import("std");
2+
const log = @import("../util/log.zig");
3+
const config = @import("../config.zig");
4+
const oci_lib = @import("oci");
5+
const rootfs_builder = @import("../rootfs/builder.zig");
6+
const initscript = @import("../initscript.zig");
7+
const oci_layout_writer = oci_lib.layout_writer;
8+
const containerfile_exec = @import("containerfile_exec.zig");
9+
const cache = @import("../cache.zig");
10+
const helpers = @import("../helpers.zig");
11+
12+
const ContainerfileResult = containerfile_exec.ContainerfileResult;
13+
const mergeImageConfig = containerfile_exec.mergeImageConfig;
14+
const computeBuildCacheKey = cache.computeBuildCacheKey;
15+
const saveBuildCache = cache.saveBuildCache;
16+
const buildInitScriptConfig = helpers.buildInitScriptConfig;
17+
const resolveTailscaleArgs = helpers.resolveTailscaleArgs;
18+
19+
const scoped_log = log.scoped("cmd/build");
20+
21+
pub fn runBuild(allocator: std.mem.Allocator, cfg: *const config.Config) !void {
22+
scoped_log.info("Building OCI image", .{});
23+
24+
// Handle containerfile if specified
25+
var cf_result_build: ?ContainerfileResult = null;
26+
defer if (cf_result_build) |*cfr| cfr.deinit(allocator);
27+
28+
if (cfg.containerfile) |cf_path| {
29+
const context_dir = cfg.context orelse blk: {
30+
break :blk std.fs.path.dirname(cf_path) orelse ".";
31+
};
32+
scoped_log.info("Building from containerfile: {s}", .{cf_path});
33+
cf_result_build = containerfile_exec.executeContainerfile(allocator, cf_path, context_dir, cfg.work_dir) catch |err| {
34+
scoped_log.err("Failed to parse containerfile: {}", .{err});
35+
return err;
36+
};
37+
}
38+
39+
// Build effective layer list
40+
var effective_layers: std.ArrayListUnmanaged(config.Layer) = .{};
41+
defer effective_layers.deinit(allocator);
42+
43+
if (cf_result_build) |cfr| {
44+
if (cfr.base_image) |bi| {
45+
try effective_layers.append(allocator, .{ .image = bi });
46+
}
47+
}
48+
if (effective_layers.items.len == 0) {
49+
try effective_layers.appendSlice(allocator, cfg.layers);
50+
}
51+
52+
for (effective_layers.items, 0..) |layer, i| {
53+
switch (layer) {
54+
.image => |ref| scoped_log.info("Layer {}/{}: image {s}", .{ i + 1, effective_layers.items.len, ref }),
55+
.rootfs => |path| scoped_log.info("Layer {}/{}: rootfs {s}", .{ i + 1, effective_layers.items.len, path }),
56+
}
57+
}
58+
59+
// Build rootfs from first layer
60+
var builder = rootfs_builder.RootfsBuilder.init(allocator, cfg.cache_dir);
61+
var build_result = builder.buildFromLayer(effective_layers.items[0], .{
62+
.target_dir = cfg.work_dir,
63+
.skip_verify = true,
64+
.tmpfs_headroom = 1.5 + 0.5 * @as(f64, @floatFromInt(effective_layers.items.len - 1)),
65+
}) catch |err| {
66+
scoped_log.err("Failed to build rootfs: {}", .{err});
67+
return err;
68+
};
69+
defer build_result.deinit(allocator);
70+
defer build_result.unmountTmpfs();
71+
72+
// Thread ImageConfig through the merge loop: last OCI image wins
73+
var effective_config: ?rootfs_builder.BuildResult.ImageConfig = build_result.config;
74+
build_result.config = null;
75+
defer {
76+
if (effective_config) |*ec| {
77+
if (ec.entrypoint) |ep| {
78+
for (ep) |e| allocator.free(e);
79+
allocator.free(ep);
80+
}
81+
if (ec.cmd) |cmd| {
82+
for (cmd) |c| allocator.free(c);
83+
allocator.free(cmd);
84+
}
85+
if (ec.env) |env| {
86+
for (env) |e| allocator.free(e);
87+
allocator.free(env);
88+
}
89+
if (ec.working_dir) |wd| allocator.free(wd);
90+
}
91+
}
92+
93+
// Merge additional layers (later overwrites earlier on conflict)
94+
for (effective_layers.items[1..]) |layer| {
95+
switch (layer) {
96+
.image => |ref| scoped_log.info("Merging image {s}", .{ref}),
97+
.rootfs => |path| scoped_log.info("Merging rootfs {s}", .{path}),
98+
}
99+
const merge_config = builder.mergeLayer(layer, cfg.work_dir) catch |err| {
100+
scoped_log.err("Failed to merge layer: {}", .{err});
101+
return err;
102+
};
103+
if (merge_config) |mc| {
104+
mergeImageConfig(allocator, &effective_config, mc);
105+
}
106+
}
107+
108+
// Execute RUN commands from containerfile
109+
if (cf_result_build) |cfr| {
110+
for (cfr.run_commands) |argv| {
111+
oci_lib.run.executeInRootfs(allocator, cfg.work_dir, argv, null) catch |err| {
112+
scoped_log.err("RUN command failed: {}", .{err});
113+
return err;
114+
};
115+
}
116+
if (cfr.img_config) |ic| {
117+
mergeImageConfig(allocator, &effective_config, ic);
118+
}
119+
}
120+
121+
// Auto-install packages for --ssh-port
122+
if (cfg.ssh_port != null) {
123+
scoped_log.info("Installing dropbear SSH server", .{});
124+
oci_lib.run.executeInRootfs(allocator, cfg.work_dir, &.{ "/bin/sh", "-c", "apk add --no-cache dropbear" }, null) catch |err| {
125+
scoped_log.warn("Failed to install dropbear: {} (image may not be alpine-based)", .{err});
126+
};
127+
}
128+
129+
// Create init script if any services are configured
130+
// Create init script only if services are configured (not for bare builds)
131+
{
132+
const effective_ts_args = resolveTailscaleArgs(allocator, cfg);
133+
const init_cfg = buildInitScriptConfig(allocator, cfg, effective_ts_args);
134+
if (init_cfg.hasServices()) {
135+
initscript.createInitScript(allocator, cfg.work_dir, &init_cfg) catch |err| {
136+
scoped_log.err("Failed to create init script: {}", .{err});
137+
return err;
138+
};
139+
}
140+
}
141+
142+
// Save to cache
143+
const cache_key = try computeBuildCacheKey(allocator, effective_layers.items);
144+
saveBuildCache(allocator, cfg.cache_dir, &cache_key, cfg.work_dir, effective_config);
145+
scoped_log.info("Cached build: {s}", .{&cache_key});
146+
147+
// Write OCI layout to output if requested
148+
if (cfg.output) |output_path| {
149+
scoped_log.info("Writing OCI layout to {s}", .{output_path});
150+
const oci_digest = try oci_layout_writer.writeOciLayout(allocator, cfg.work_dir, output_path, effective_config);
151+
scoped_log.info("OCI image: sha256:{s}", .{&oci_digest.manifest_digest});
152+
}
153+
154+
// Optionally write rootfs tarball
155+
if (cfg.rootfs_output) |rootfs_path| {
156+
scoped_log.info("Writing rootfs tarball to {s}", .{rootfs_path});
157+
oci_layout_writer.createTarFromDir(cfg.work_dir, rootfs_path, allocator) catch |err| {
158+
scoped_log.err("Failed to create rootfs tarball: {}", .{err});
159+
return err;
160+
};
161+
}
162+
163+
// Entrypoint validation (warning only for generate)
164+
if (!cfg.entrypoint_explicit) {
165+
const has_entrypoint = if (effective_config) |ec|
166+
(ec.entrypoint != null and ec.entrypoint.?.len > 0) or
167+
(ec.cmd != null and ec.cmd.?.len > 0)
168+
else
169+
false;
170+
171+
if (!has_entrypoint) {
172+
var rootfs_dir = std.fs.openDirAbsolute(cfg.work_dir, .{}) catch {
173+
scoped_log.warn("Cannot open rootfs to validate entrypoint", .{});
174+
return;
175+
};
176+
defer rootfs_dir.close();
177+
rootfs_dir.access("sbin/init", .{}) catch {
178+
scoped_log.warn("No entrypoint from image config and /sbin/init not found in rootfs", .{});
179+
};
180+
}
181+
}
182+
183+
if (cfg.output) |output_path| {
184+
scoped_log.info("Generated {s}", .{output_path});
185+
} else {
186+
scoped_log.info("Build cached (no output requested)", .{});
187+
}
188+
}

0 commit comments

Comments
 (0)