docker multi-platform build support and ci
Some checks failed
Generic zig build / build (push) Failing after 8s

This commit is contained in:
Emil Lerch 2025-04-12 14:47:27 -07:00
parent 45eae9800f
commit ba251b4b80
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 139 additions and 1 deletions

View file

@ -26,6 +26,41 @@ jobs:
curl --user ${{ github.actor }}:${{ secrets.PACKAGE_PUSH }} \ curl --user ${{ github.actor }}:${{ secrets.PACKAGE_PUSH }} \
--upload-file zig-out/bin/syncthing_events \ --upload-file zig-out/bin/syncthing_events \
https://git.lerch.org/api/packages/lobo/generic/aws-sdk-with-models/${{ github.sha }}/syncthing_events-x86_64-linux-${{ github.sha }} https://git.lerch.org/api/packages/lobo/generic/aws-sdk-with-models/${{ github.sha }}/syncthing_events-x86_64-linux-${{ github.sha }}
- name: Prepare docker image
run: zig build docker
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
# list of Docker images to use as base name for tags
images: |
git.lerch.org/&{{ github.repository }}
# generate Docker tags based on the following events/attributes
tags: |
type=schedule
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: git.lerch.org
username: ${{ github.actor }}
password: ${{ secrets.PACKAGE_PUSH }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: zig-out
platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6
# load: true # will not work for multiplatform
push: true
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
- name: Notify - name: Notify
uses: elerch/action-notify-ntfy@v2.github uses: elerch/action-notify-ntfy@v2.github
if: always() && env.GITEA_ACTIONS == 'true' if: always() && env.GITEA_ACTIONS == 'true'

105
build.zig
View file

@ -3,7 +3,7 @@ const std = @import("std");
// Although this function looks imperative, note that its job is to // Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external // declaratively construct a build graph that will be executed by an external
// runner. // runner.
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) !void {
// Standard target options allows the person running `zig build` to choose // Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which // what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options // means any target is allowed, and the default is native. Other options
@ -125,4 +125,107 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run unit tests"); const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step); test_step.dependOn(&run_lib_unit_tests.step);
test_step.dependOn(&run_exe_unit_tests.step); test_step.dependOn(&run_exe_unit_tests.step);
try docker(b, exe);
}
fn docker(b: *std.Build, compile: *std.Build.Step.Compile) !void {
const DockerTarget = struct {
platform: []const u8,
target: std.Target.Query,
};
// From docker source:
// https://github.com/containerd/containerd/blob/52f02c3aa1e7ccd448060375c821cae4e3300cdb/test/init-buildx.sh#L45
// Platforms: linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
const docker_targets = [_]DockerTarget{
.{ .platform = "linux/amd64", .target = .{ .cpu_arch = .x86_64, .os_tag = .linux } },
.{ .platform = "linux/arm64", .target = .{ .cpu_arch = .aarch64, .os_tag = .linux } },
.{ .platform = "linux/riscv64", .target = .{ .cpu_arch = .riscv64, .os_tag = .linux } },
.{ .platform = "linux/ppc64le", .target = .{ .cpu_arch = .powerpc64le, .os_tag = .linux } },
.{ .platform = "linux/390x", .target = .{ .cpu_arch = .s390x, .os_tag = .linux } },
.{ .platform = "linux/386", .target = .{ .cpu_arch = .x86, .os_tag = .linux } },
.{ .platform = "linux/arm/v7", .target = .{ .cpu_arch = .arm, .os_tag = .linux, .abi = .musleabihf } }, // linux/arm/v7
.{ .platform = "linux/arm/v6", .target = .{
.cpu_arch = .arm,
.os_tag = .linux,
.abi = .musleabihf,
.cpu_model = .{ .explicit = &std.Target.arm.cpu.arm1176jzf_s },
} },
};
const SubPath = struct {
path: [3][]const u8,
len: usize,
};
// We are going to put all the binaries in paths that will be happy with
// the dockerfile at the end, which means we need to get all the platforms
// into slices. We can do this at comptime, but need to use arrays, so we
// will hard code 3 element arrays which will hold our linux/arm/v7. If
// deeper platforms are invented by docker later, we'll need to tweak the
// hardcoded "3" values above and below, but at least we'll throw a compile
// error to let the maintainer of the code know they screwed up by adding
// a hardcoded platform above without changing the hardcoded length values.
// By having the components chopped up this way, we should be able to build
// all this from a Windows host
comptime var dest_sub_paths: [docker_targets.len]SubPath = undefined;
comptime {
for (docker_targets, 0..) |dt, inx| {
var si = std.mem.splitScalar(u8, dt.platform, '/');
var sub_path: SubPath = undefined;
sub_path.len = 1 + std.mem.count(u8, dt.platform, "/");
if (sub_path.len > 3) @compileError("Docker platform cannot have more than 2 forward slashes");
var jnx: usize = 0;
while (si.next()) |s| : (jnx += 1)
sub_path.path[jnx] = s;
dest_sub_paths[inx] = sub_path;
}
}
const docker_step = b.step("docker", "Prepares the app for bundling as multi-platform docker image");
for (docker_targets, 0..) |dt, i| {
const target_module = b.createModule(.{
.root_source_file = compile.root_module.root_source_file,
.target = b.resolveTargetQuery(dt.target),
.optimize = .ReleaseSafe,
});
for (compile.root_module.import_table.keys()) |k|
target_module.addImport(k, compile.root_module.import_table.get(k).?);
const target_exe = b.addExecutable(.{
.name = compile.name,
.root_module = target_module,
});
// We can't use our dest_sub_paths directly here, because adding
// a value for "dest_sub_path" in the installArtifact options will also
// override the use of the basename. So wee need to construct our own
// slice. We know the number of path components though, so we will
// alloc what we need (no free, since zig build uses an arena) and
// copy our components in place
var final_sub_path = try b.allocator.alloc([]const u8, dest_sub_paths[i].len + 1);
for (dest_sub_paths[i].path, 0..) |p, j| final_sub_path[j] = p;
final_sub_path[final_sub_path.len - 1] = target_exe.name; // add basename at end
docker_step.dependOn(&b.addInstallArtifact(target_exe, .{
.dest_sub_path = try std.fs.path.join(b.allocator, final_sub_path),
}).step);
}
// The above will get us all the binaries, but we also need a dockerfile
try dockerInstallDockerfile(b, docker_step, compile.name);
}
fn dockerInstallDockerfile(b: *std.Build, docker_step: *std.Build.Step, exe_name: []const u8) !void {
const dockerfile_fmt =
\\FROM alpine:latest as build
\\RUN apk --update add ca-certificates
\\
\\FROM scratch
\\ARG TARGETPLATFORM
\\ENV PATH=/bin
\\COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
\\COPY bin/$TARGETPLATFORM/{s} /bin
;
const dockerfile_data = try std.fmt.allocPrint(b.allocator, dockerfile_fmt, .{exe_name});
const writefiles = b.addWriteFiles();
const dockerfile = writefiles.add("Dockerfile", dockerfile_data);
docker_step.dependOn(&b.addInstallFile(dockerfile, "Dockerfile").step);
} }