mirror of
https://github.com/NilsIrl/dockerc.git
synced 2025-09-26 19:11:13 +08:00
Initial commit
This commit is contained in:
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
src/tools/skopeo filter=lfs diff=lfs merge=lfs -text
|
||||
src/tools/squashfuse filter=lfs diff=lfs merge=lfs -text
|
||||
src/tools/umoci.amd64 filter=lfs diff=lfs merge=lfs -text
|
||||
src/tools/crun filter=lfs diff=lfs merge=lfs -text
|
||||
src/tools/fuse-overlayfs filter=lfs diff=lfs merge=lfs -text
|
||||
src/tools/mksquashfs filter=lfs diff=lfs merge=lfs -text
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
zig-cache/
|
||||
zig-out/
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "lib/zig-clap"]
|
||||
path = src/zig-clap
|
||||
url = https://github.com/Hejsil/zig-clap.git
|
15
BUILD.md
Normal file
15
BUILD.md
Normal file
@@ -0,0 +1,15 @@
|
||||
```
|
||||
sudo apt install libzstd-dev
|
||||
./configure --without-xz --without-zlib LDFLAGS="-static"
|
||||
make LDFLAGS="-all-static" -j
|
||||
```
|
||||
|
||||
```
|
||||
zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-linux-musl
|
||||
```
|
||||
|
||||
Examples images to try on:
|
||||
|
||||
* https://github.com/oven-sh/bun?tab=readme-ov-file#install
|
||||
* https://github.com/containers/skopeo/blob/main/install.md#container-images
|
||||
* https://github.com/shepherdjerred/macos-cross-compiler
|
18
README.md
Normal file
18
README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# dockerc - compile docker images to standalone portable binaries
|
||||
|
||||
## Features
|
||||
|
||||
- [X] Compiler docker images into portable binaries
|
||||
- [ ] MacOS support (using qemu)
|
||||
- [X] x86_64 support
|
||||
- [ ] arm64 support
|
||||
- [X] Supports arguments
|
||||
- [ ] Support `-p`
|
||||
- [ ] Support `-v`
|
||||
- [ ] Support other arguments...
|
||||
|
||||
|
||||
### Why zig?
|
||||
|
||||
* Small binary size
|
||||
* Full static linking
|
72
build.zig
Normal file
72
build.zig
Normal file
@@ -0,0 +1,72 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Although this function looks imperative, note that its job is to
|
||||
// declaratively construct a build graph that will be executed by an external
|
||||
// runner.
|
||||
pub fn build(b: *std.Build) void {
|
||||
b.reference_trace = 64;
|
||||
|
||||
// Standard target options allows the person running `zig build` to choose
|
||||
// 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
|
||||
// for restricting supported target set are available.
|
||||
const target = b.standardTargetOptions(.{});
|
||||
// target.result.abi = .musl;
|
||||
|
||||
// Standard optimization options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// const clap = b.addModule("clap", .{ .root_source_file = .{ .path = "lib/zig-clap/clap.zig" } });
|
||||
|
||||
const runtime = b.addExecutable(.{
|
||||
.name = "runtime",
|
||||
.root_source_file = .{ .path = "src/main.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
// necessary to link in bigger file
|
||||
// .code_model = .medium,
|
||||
});
|
||||
|
||||
const dockerc = b.addExecutable(.{
|
||||
.name = "dockerc",
|
||||
.root_source_file = .{ .path = "src/dockerc.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
});
|
||||
|
||||
dockerc.root_module.addAnonymousImport("runtime", .{ .root_source_file = runtime.getEmittedBin() });
|
||||
|
||||
b.installArtifact(dockerc);
|
||||
|
||||
// This declares intent for the executable to be installed into the
|
||||
// standard location when the user invokes the "install" step (the default
|
||||
// step when running `zig build`).
|
||||
// b.installArtifact(exe);
|
||||
|
||||
// This *creates* a Run step in the build graph, to be executed when another
|
||||
// step is evaluated that depends on it. The next line below will establish
|
||||
// such a dependency.
|
||||
// const run_cmd = b.addRunArtifact(exe);
|
||||
|
||||
// By making the run step depend on the install step, it will be run from the
|
||||
// installation directory rather than directly from within the cache directory.
|
||||
// This is not necessary, however, if the application depends on other installed
|
||||
// files, this ensures they will be present and in the expected location.
|
||||
// run_cmd.step.dependOn(b.getInstallStep());
|
||||
|
||||
// This allows the user to pass arguments to the application in the build
|
||||
// command itself, like this: `zig build run -- arg1 arg2 etc`
|
||||
// if (b.args) |args| {
|
||||
// run_cmd.addArgs(args);
|
||||
// }
|
||||
|
||||
// This creates a build step. It will be visible in the `zig build --help` menu,
|
||||
// and can be selected like this: `zig build run`
|
||||
// This will evaluate the `run` step rather than the default, which is "install".
|
||||
// const run_step = b.step("run", "Run the app");
|
||||
// run_step.dependOn(&run_cmd.step);
|
||||
}
|
62
build.zig.zon
Normal file
62
build.zig.zon
Normal file
@@ -0,0 +1,62 @@
|
||||
.{
|
||||
.name = "dockerc",
|
||||
// This is a [Semantic Version](https://semver.org/).
|
||||
// In a future version of Zig it will be used for package deduplication.
|
||||
.version = "0.0.0",
|
||||
|
||||
// This field is optional.
|
||||
// This is currently advisory only; Zig does not yet do anything
|
||||
// with this value.
|
||||
//.minimum_zig_version = "0.11.0",
|
||||
|
||||
// This field is optional.
|
||||
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
|
||||
// Once all dependencies are fetched, `zig build` no longer requires
|
||||
// internet connectivity.
|
||||
.dependencies = .{
|
||||
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
|
||||
//.example = .{
|
||||
// // When updating this field to a new URL, be sure to delete the corresponding
|
||||
// // `hash`, otherwise you are communicating that you expect to find the old hash at
|
||||
// // the new URL.
|
||||
// .url = "https://example.com/foo.tar.gz",
|
||||
//
|
||||
// // This is computed from the file contents of the directory of files that is
|
||||
// // obtained after fetching `url` and applying the inclusion rules given by
|
||||
// // `paths`.
|
||||
// //
|
||||
// // This field is the source of truth; packages do not come from a `url`; they
|
||||
// // come from a `hash`. `url` is just one of many possible mirrors for how to
|
||||
// // obtain a package matching this `hash`.
|
||||
// //
|
||||
// // Uses the [multihash](https://multiformats.io/multihash/) format.
|
||||
// .hash = "...",
|
||||
//
|
||||
// // When this is provided, the package is found in a directory relative to the
|
||||
// // build root. In this case the package's hash is irrelevant and therefore not
|
||||
// // computed. This field and `url` are mutually exclusive.
|
||||
// .path = "foo",
|
||||
//},
|
||||
},
|
||||
|
||||
// Specifies the set of files and directories that are included in this package.
|
||||
// Only files and directories listed here are included in the `hash` that
|
||||
// is computed for this package.
|
||||
// Paths are relative to the build root. Use the empty string (`""`) to refer to
|
||||
// the build root itself.
|
||||
// A directory listed here means that all files within, recursively, are included.
|
||||
.paths = .{
|
||||
// This makes *all* files, recursively, included in this package. It is generally
|
||||
// better to explicitly list the files and directories instead, to insure that
|
||||
// fetching from tarballs, file system paths, and version control all result
|
||||
// in the same contents hash.
|
||||
"",
|
||||
// For example...
|
||||
//"build.zig",
|
||||
//"build.zig.zon",
|
||||
//"src",
|
||||
//"LICENSE",
|
||||
//"README.md",
|
||||
},
|
||||
}
|
16
src/common.zig
Normal file
16
src/common.zig
Normal file
@@ -0,0 +1,16 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub extern fn mkdtemp(in: [*:0]const u8) ?[*:0]const u8;
|
||||
|
||||
// TODO: ideally we can use memfd_create
|
||||
// The problem is that zig doesn't have fexecve support by default so it would
|
||||
// be a pain to find the location of the file.
|
||||
pub fn extract_file(tmpDir: []const u8, name: []const u8, data: []const u8, allocator: std.mem.Allocator) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ tmpDir, name });
|
||||
|
||||
const file = try std.fs.createFileAbsolute(path, .{ .mode = 0o700 });
|
||||
defer file.close();
|
||||
try file.writeAll(data);
|
||||
|
||||
return path;
|
||||
}
|
130
src/dockerc.zig
Normal file
130
src/dockerc.zig
Normal file
@@ -0,0 +1,130 @@
|
||||
const std = @import("std");
|
||||
// const clap = @import("../lib/zip-clap/clap.zig");
|
||||
const clap = @import("zig-clap/clap.zig");
|
||||
const common = @import("common.zig");
|
||||
|
||||
const mkdtemp = common.mkdtemp;
|
||||
const extract_file = common.extract_file;
|
||||
|
||||
const debug = std.debug;
|
||||
|
||||
const io = std.io;
|
||||
|
||||
const skopeo_content = @embedFile("tools/skopeo");
|
||||
const mksquashfs_content = @embedFile("tools/mksquashfs");
|
||||
const umoci_content = @embedFile("tools/umoci.amd64");
|
||||
const runtime_content = @embedFile("runtime");
|
||||
|
||||
const runtime_content_len_u64 = data: {
|
||||
var buf: [8]u8 = undefined;
|
||||
std.mem.writeInt(u64, &buf, runtime_content.len, .big);
|
||||
break :data buf;
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
|
||||
const temp_dir_path = std.mem.span(mkdtemp("/tmp/dockerc-XXXXXX") orelse @panic("failed to create temp dir"));
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
const skopeo_path = try extract_file(temp_dir_path, "skopeo", skopeo_content, allocator);
|
||||
defer allocator.free(skopeo_path);
|
||||
|
||||
const umoci_path = try extract_file(temp_dir_path, "umoci", umoci_content, allocator);
|
||||
defer allocator.free(umoci_path);
|
||||
|
||||
const mksquashfs_path = try extract_file(temp_dir_path, "mksquashfs", mksquashfs_content, allocator);
|
||||
defer allocator.free(mksquashfs_path);
|
||||
|
||||
const params = comptime clap.parseParamsComptime(
|
||||
\\-h, --help Display this help and exit.
|
||||
\\-i, --image <str> Image to pull.
|
||||
\\-o, --output <str> Output file.
|
||||
\\--rootfull Do not use rootless container.
|
||||
\\
|
||||
);
|
||||
|
||||
var diag = clap.Diagnostic{};
|
||||
var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{
|
||||
.diagnostic = &diag,
|
||||
.allocator = allocator,
|
||||
}) catch |err| {
|
||||
// Report useful error and exit
|
||||
diag.report(io.getStdErr().writer(), err) catch {};
|
||||
return err;
|
||||
};
|
||||
defer res.deinit();
|
||||
|
||||
if (res.args.help != 0) {
|
||||
debug.print("help message\n", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
var missing_args = false;
|
||||
if (res.args.image == null) {
|
||||
debug.print("no --image specified\n", .{});
|
||||
missing_args = true;
|
||||
}
|
||||
|
||||
if (res.args.output == null) {
|
||||
debug.print("no --output specified\n", .{});
|
||||
missing_args = true;
|
||||
}
|
||||
|
||||
if (missing_args) {
|
||||
return;
|
||||
}
|
||||
|
||||
// safe to assert because checked above
|
||||
const image = res.args.image.?;
|
||||
const output_path = res.args.output.?;
|
||||
|
||||
const destination_arg = try std.fmt.allocPrint(allocator, "oci:{s}/image:latest", .{temp_dir_path});
|
||||
defer allocator.free(destination_arg);
|
||||
|
||||
var skopeoProcess = std.ChildProcess.init(&[_][]const u8{ skopeo_path, "copy", image, destination_arg }, gpa.allocator());
|
||||
_ = try skopeoProcess.spawnAndWait();
|
||||
|
||||
const umoci_image_layout_path = try std.fmt.allocPrint(allocator, "{s}/image:latest", .{temp_dir_path});
|
||||
defer allocator.free(umoci_image_layout_path);
|
||||
|
||||
const bundle_destination = try std.fmt.allocPrint(allocator, "{s}/bundle", .{temp_dir_path});
|
||||
defer allocator.free(bundle_destination);
|
||||
|
||||
const umoci_args = [_][]const u8{
|
||||
umoci_path,
|
||||
"unpack",
|
||||
"--image",
|
||||
umoci_image_layout_path,
|
||||
bundle_destination,
|
||||
"--rootless",
|
||||
};
|
||||
var umociProcess = std.ChildProcess.init(if (res.args.rootfull == 0) &umoci_args else umoci_args[0 .. umoci_args.len - 1], gpa.allocator());
|
||||
_ = try umociProcess.spawnAndWait();
|
||||
|
||||
const offset_arg = try std.fmt.allocPrint(allocator, "{}", .{runtime_content.len});
|
||||
defer allocator.free(offset_arg);
|
||||
|
||||
var mksquashfsProcess = std.ChildProcess.init(&[_][]const u8{
|
||||
mksquashfs_path,
|
||||
bundle_destination,
|
||||
output_path,
|
||||
"-comp",
|
||||
"zstd",
|
||||
"-offset",
|
||||
offset_arg,
|
||||
"-noappend",
|
||||
}, gpa.allocator());
|
||||
_ = try mksquashfsProcess.spawnAndWait();
|
||||
|
||||
const file = try std.fs.cwd().openFile(output_path, .{
|
||||
.mode = .write_only,
|
||||
});
|
||||
defer file.close();
|
||||
|
||||
try file.writeAll(runtime_content);
|
||||
try file.seekFromEnd(0);
|
||||
try file.writeAll(&runtime_content_len_u64);
|
||||
try file.chmod(0o755);
|
||||
}
|
142
src/main.zig
Normal file
142
src/main.zig
Normal file
@@ -0,0 +1,142 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const common = @import("common.zig");
|
||||
|
||||
const mkdtemp = common.mkdtemp;
|
||||
const extract_file = common.extract_file;
|
||||
|
||||
const squashfuse_content = @embedFile("tools/squashfuse");
|
||||
const crun_content = @embedFile("tools/crun");
|
||||
const overlayfs_content = @embedFile("tools/fuse-overlayfs");
|
||||
|
||||
fn getOffset(path: []const u8) !u64 {
|
||||
var file = try std.fs.cwd().openFile(path, .{});
|
||||
try file.seekFromEnd(-8);
|
||||
|
||||
var buffer: [8]u8 = undefined;
|
||||
assert(try file.readAll(&buffer) == 8);
|
||||
|
||||
return std.mem.readInt(u64, buffer[0..8], std.builtin.Endian.big);
|
||||
}
|
||||
|
||||
const eql = std.mem.eql;
|
||||
|
||||
fn processArgs(file: std.fs.File, allocator: std.mem.Allocator) !void {
|
||||
// const file = try std.fs.openFileAbsolute(path, .{ .mode = .read_write });
|
||||
|
||||
var jsonReader = std.json.reader(allocator, file.reader());
|
||||
|
||||
// TODO: having to specify max_value_len seems like a bug
|
||||
var root_value = try std.json.Value.jsonParse(allocator, &jsonReader, .{ .max_value_len = 99999999 });
|
||||
|
||||
const argVal = args: {
|
||||
switch (root_value) {
|
||||
.object => |*object| {
|
||||
const processVal = object.getPtr("process") orelse @panic("no process key");
|
||||
switch (processVal.*) {
|
||||
.object => |*process| {
|
||||
const argsVal = process.getPtr("args") orelse @panic("no args key");
|
||||
switch (argsVal.*) {
|
||||
.array => |*argsArr| {
|
||||
break :args argsArr;
|
||||
},
|
||||
else => return error.InvalidJSON,
|
||||
}
|
||||
},
|
||||
else => return error.InvalidJSON,
|
||||
}
|
||||
},
|
||||
else => return error.InvalidJSON,
|
||||
}
|
||||
};
|
||||
|
||||
var args = std.process.args();
|
||||
_ = args.next() orelse @panic("there should be an executable");
|
||||
|
||||
while (args.next()) |arg| {
|
||||
if (eql(u8, arg, "-p")) {
|
||||
_ = args.next();
|
||||
@panic("not implemented");
|
||||
} else if (eql(u8, arg, "-v")) {
|
||||
_ = args.next();
|
||||
@panic("not implemented");
|
||||
} else if (eql(u8, arg, "--")) {
|
||||
while (args.next()) |arg_inner| {
|
||||
try argVal.append(std.json.Value{ .string = arg_inner });
|
||||
}
|
||||
} else {
|
||||
try argVal.append(std.json.Value{ .string = arg });
|
||||
}
|
||||
}
|
||||
|
||||
try file.setEndPos(0);
|
||||
try file.seekTo(0);
|
||||
var jsonWriter = std.json.writeStream(file.writer(), .{ .whitespace = .indent_tab });
|
||||
|
||||
try std.json.Value.jsonStringify(root_value, &jsonWriter);
|
||||
}
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const allocator = gpa.allocator();
|
||||
// defer _ = gpa.deinit();
|
||||
var args = std.process.args();
|
||||
const executable_path = args.next() orelse unreachable;
|
||||
|
||||
const temp_dir_path = std.mem.span(mkdtemp("/tmp/dockerc-XXXXXX") orelse @panic("failed to create temp dir"));
|
||||
|
||||
const squashfuse_path = try extract_file(temp_dir_path, "squashfuse", squashfuse_content, allocator);
|
||||
defer allocator.free(squashfuse_path);
|
||||
|
||||
const crun_path = try extract_file(temp_dir_path, "crun", crun_content, allocator);
|
||||
defer allocator.free(crun_path);
|
||||
|
||||
const overlayfs_path = try extract_file(temp_dir_path, "fuse-overlayfs", overlayfs_content, allocator);
|
||||
defer allocator.free(overlayfs_path);
|
||||
|
||||
const filesystem_bundle_dir_null = try std.fmt.allocPrintZ(allocator, "{s}/{s}", .{ temp_dir_path, "bundle.squashfs" });
|
||||
defer allocator.free(filesystem_bundle_dir_null);
|
||||
|
||||
try std.fs.makeDirAbsolute(filesystem_bundle_dir_null);
|
||||
|
||||
const mount_dir_path = try std.fmt.allocPrint(allocator, "{s}/mount", .{temp_dir_path});
|
||||
defer allocator.free(mount_dir_path);
|
||||
|
||||
const offsetArg = try std.fmt.allocPrint(allocator, "offset={}", .{try getOffset(executable_path)});
|
||||
defer allocator.free(offsetArg);
|
||||
|
||||
const args_buf = [_][]const u8{ squashfuse_path, "-o", offsetArg, executable_path, filesystem_bundle_dir_null };
|
||||
|
||||
var mountProcess = std.ChildProcess.init(&args_buf, gpa.allocator());
|
||||
_ = try mountProcess.spawnAndWait();
|
||||
|
||||
const overlayfs_options = try std.fmt.allocPrint(allocator, "lowerdir={s},upperdir={s}/upper,workdir={s}/upper", .{
|
||||
filesystem_bundle_dir_null,
|
||||
temp_dir_path,
|
||||
temp_dir_path,
|
||||
});
|
||||
defer allocator.free(overlayfs_options);
|
||||
|
||||
const tmpDir = try std.fs.openDirAbsolute(temp_dir_path, .{});
|
||||
try tmpDir.makeDir("upper");
|
||||
try tmpDir.makeDir("work");
|
||||
try tmpDir.makeDir("mount");
|
||||
|
||||
var overlayfsProcess = std.ChildProcess.init(&[_][]const u8{ overlayfs_path, "-o", overlayfs_options, mount_dir_path }, allocator);
|
||||
_ = try overlayfsProcess.spawnAndWait();
|
||||
|
||||
const file = try tmpDir.openFile("mount/config.json", .{ .mode = .read_write });
|
||||
defer file.close();
|
||||
try processArgs(file, allocator);
|
||||
|
||||
var crunProcess = std.ChildProcess.init(&[_][]const u8{ crun_path, "run", "-b", mount_dir_path, "crun_docker_c_id" }, gpa.allocator());
|
||||
_ = try crunProcess.spawnAndWait();
|
||||
|
||||
var umountOverlayProcess = std.ChildProcess.init(&[_][]const u8{ "umount", mount_dir_path }, gpa.allocator());
|
||||
_ = try umountOverlayProcess.spawnAndWait();
|
||||
|
||||
var umountProcess = std.ChildProcess.init(&[_][]const u8{ "umount", filesystem_bundle_dir_null }, gpa.allocator());
|
||||
_ = try umountProcess.spawnAndWait();
|
||||
|
||||
// TODO: clean up /tmp
|
||||
}
|
BIN
src/tools/crun
(Stored with Git LFS)
Executable file
BIN
src/tools/crun
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
src/tools/fuse-overlayfs
(Stored with Git LFS)
Normal file
BIN
src/tools/fuse-overlayfs
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/tools/mksquashfs
(Stored with Git LFS)
Executable file
BIN
src/tools/mksquashfs
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
src/tools/skopeo
(Stored with Git LFS)
Executable file
BIN
src/tools/skopeo
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
src/tools/squashfuse
(Stored with Git LFS)
Executable file
BIN
src/tools/squashfuse
(Stored with Git LFS)
Executable file
Binary file not shown.
BIN
src/tools/umoci.amd64
(Stored with Git LFS)
Executable file
BIN
src/tools/umoci.amd64
(Stored with Git LFS)
Executable file
Binary file not shown.
Reference in New Issue
Block a user