├── .github └── workflows │ └── build.yml ├── README.md ├── build.zig └── src ├── assets ├── file1.txt └── foo.png └── main.zig /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, macos-latest] 10 | runs-on: ${{matrix.os}} 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | path: zig-embeddir 16 | - name: Setup Zig 17 | uses: mlugg/setup-zig@v1 18 | with: 19 | version: master 20 | - name: Build test 21 | run: zig build 22 | working-directory: zig-embeddir 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # embeddir 2 | 3 | An example of using the zig build system to aid in embeddeding multiple files in an executable. 4 | 5 | Toby Jaffey, https://mastodon.me.uk/@tobyjaffey 6 | 7 | Zig has `@embedFile` (https://ziglang.org/documentation/master/#embedFile) which embeds the contents of a file in the executable at compile time. In some situations, it's useful to embed all files from a directory without hardcoding their names into the source code. 8 | 9 | # How does it work? 10 | 11 | In `build.zig`, we add a custom function `addAssetsOption()`. This opens the `assets` directory, lists files and passes the list to the main program in a module called `assets` in a field called `files`. 12 | 13 | ... 14 | options.addOption([]const []const u8, "files", files.items); 15 | exe.step.dependOn(&options.step); 16 | 17 | const assets = b.createModule(.{ 18 | .source_file = options.getSource(), 19 | .dependencies = &.{}, 20 | }); 21 | exe.addModule("assets", assets); 22 | ... 23 | 24 | In `src/main`, we build a `ComptimeStringMap` using `assets.files` as the keys and the result of `@embedFile()` on each key as the value. 25 | 26 | At runtime, the data can then be accessed by filename using: 27 | 28 | const data = embeddedFilesMap.get(filename).?; 29 | 30 | The list of files can be found in `assets.files` 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn addAssetsOption(b: *std.Build, exe:anytype, target:anytype, optimize:anytype) !void { 4 | var options = b.addOptions(); 5 | 6 | var files = std.ArrayList([]const u8).init(b.allocator); 7 | defer files.deinit(); 8 | 9 | var buf: [std.fs.max_path_bytes]u8 = undefined; 10 | const path = try std.fs.cwd().realpath("src/assets", buf[0..]); 11 | 12 | var dir = try std.fs.openDirAbsolute(path, .{.iterate=true}); 13 | var it = dir.iterate(); 14 | while (try it.next()) |file| { 15 | if (file.kind != .file) { 16 | continue; 17 | } 18 | try files.append(b.dupe(file.name)); 19 | } 20 | options.addOption([]const []const u8, "files", files.items); 21 | exe.step.dependOn(&options.step); 22 | 23 | const assets = b.addModule("assets", .{ 24 | .root_source_file = options.getOutput(), 25 | .target = target, 26 | .optimize = optimize, 27 | }); 28 | 29 | exe.root_module.addImport("assets", assets); 30 | } 31 | 32 | 33 | // Although this function looks imperative, note that its job is to 34 | // declaratively construct a build graph that will be executed by an external 35 | // runner. 36 | pub fn build(b: *std.Build) void { 37 | // Standard target options allows the person running `zig build` to choose 38 | // what target to build for. Here we do not override the defaults, which 39 | // means any target is allowed, and the default is native. Other options 40 | // for restricting supported target set are available. 41 | const target = b.standardTargetOptions(.{}); 42 | 43 | // Standard optimization options allow the person running `zig build` to select 44 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 45 | // set a preferred release mode, allowing the user to decide how to optimize. 46 | const optimize = b.standardOptimizeOption(.{}); 47 | 48 | const exe = b.addExecutable(.{ 49 | .name = "embdir", 50 | // In this case the main source file is merely a path, however, in more 51 | // complicated build scripts, this could be a generated file. 52 | .root_source_file = b.path("src/main.zig"), 53 | .target = target, 54 | .optimize = optimize, 55 | }); 56 | 57 | addAssetsOption(b, exe, target, optimize) catch |err| { 58 | std.log.err("Problem adding assets: {!}", .{err}); 59 | }; 60 | 61 | // This declares intent for the executable to be installed into the 62 | // standard location when the user invokes the "install" step (the default 63 | // step when running `zig build`). 64 | b.installArtifact(exe); 65 | 66 | 67 | // This *creates* a Run step in the build graph, to be executed when another 68 | // step is evaluated that depends on it. The next line below will establish 69 | // such a dependency. 70 | const run_cmd = b.addRunArtifact(exe); 71 | 72 | // By making the run step depend on the install step, it will be run from the 73 | // installation directory rather than directly from within the cache directory. 74 | // This is not necessary, however, if the application depends on other installed 75 | // files, this ensures they will be present and in the expected location. 76 | run_cmd.step.dependOn(b.getInstallStep()); 77 | 78 | // This allows the user to pass arguments to the application in the build 79 | // command itself, like this: `zig build run -- arg1 arg2 etc` 80 | if (b.args) |args| { 81 | run_cmd.addArgs(args); 82 | } 83 | 84 | run_cmd.step.dependOn(b.getInstallStep()); 85 | 86 | // This creates a build step. It will be visible in the `zig build --help` menu, 87 | // and can be selected like this: `zig build run` 88 | // This will evaluate the `run` step rather than the default, which is "install". 89 | const run_step = b.step("run", "Run the app"); 90 | run_step.dependOn(&run_cmd.step); 91 | 92 | } 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/assets/file1.txt: -------------------------------------------------------------------------------- 1 | Contents of file1 2 | -------------------------------------------------------------------------------- /src/assets/foo.png: -------------------------------------------------------------------------------- 1 | Shh, this isn't really a PNG, but you get the idea 2 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assets = @import("assets"); 3 | 4 | // kv pair type used to fill ComptimeStringMap 5 | const EmbeddedAsset = struct { 6 | []const u8, 7 | []const u8, 8 | }; 9 | 10 | // declare a StaticStringMap and fill it with our filenames and data 11 | const embeddedFilesMap = std.StaticStringMap([]const u8).initComptime(genMap()); 12 | 13 | fn genMap() [assets.files.len]EmbeddedAsset { 14 | var embassets: [assets.files.len]EmbeddedAsset = undefined; 15 | comptime var i = 0; 16 | inline for (assets.files) |file| { 17 | embassets[i][0] = file; 18 | embassets[i][1] = @embedFile("assets/" ++ file); 19 | i += 1; 20 | } 21 | return embassets; 22 | } 23 | 24 | pub fn main() !void { 25 | for (assets.files) |filename| { 26 | const data = embeddedFilesMap.get(filename).?; 27 | std.debug.print("'{s}':{s}\n", .{filename, data}); 28 | } 29 | 30 | } 31 | 32 | --------------------------------------------------------------------------------