|
| 1 | +# Source Code Tour Pt.1 |
| 2 | +> A tour of the build system |
| 3 | +
|
| 4 | +Today I figured I would start the tour of the source code of this site with the primary building block of the application: `build.zig`. |
| 5 | + |
| 6 | +## `build.zig` |
| 7 | +My build is pretty standard, so I'm not going to share the whole file, instead I will just share the out of the ordinary steps. |
| 8 | +The first is a function I wrote that sources from a `.env` file, and the other is a combination of a tool and a few lines that expose a `.json` file to my program. |
| 9 | + |
| 10 | +### `.env` Sourcing |
| 11 | + `.env` files are generally used to configure the environment of a given application. Generally speaking they are hidden from git or whichever version control system a dev might use. This way "secrets" can live in these `.env` files (such as API keys or other sensitive info) and only the program can actually look into their values at runtime. |
| 12 | +Before, I was using [`zdotenv`](https://github.com/BitlyTwiser/zdotenv), but I've been trying to minimize dependencies and I though it would be fun to try to build `.env` sourcing into the build system. |
| 13 | +#### The code |
| 14 | +```zig |
| 15 | +fn loadDotEnv(run: *std.Build.Step.Run) void { |
| 16 | + var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); |
| 17 | + defer arena_state.deinit(); |
| 18 | + const arena = arena_state.allocator(); |
| 19 | + var env_file = std.fs.cwd().openFile(".env", .{}) catch |e| { |
| 20 | + switch (e) { |
| 21 | + error.FileNotFound => { |
| 22 | + log.info( |
| 23 | + \\ No .env file found |
| 24 | + , .{}); |
| 25 | + }, |
| 26 | + else => { |
| 27 | + log.err( |
| 28 | + \\ build.zig could not open .env file: {any} |
| 29 | + , .{e}); |
| 30 | + }, |
| 31 | + } |
| 32 | + return; |
| 33 | + }; |
| 34 | +
|
| 35 | + defer env_file.close(); |
| 36 | +
|
| 37 | + const read_buffer = arena.alloc(u8, 2048) catch @panic("out of memory"); |
| 38 | + var reader = env_file.reader(read_buffer); |
| 39 | +
|
| 40 | + const contents = reader.interface.allocRemaining(arena, .unlimited) catch @panic("failed to read"); |
| 41 | +
|
| 42 | + var lines = std.mem.splitScalar(u8, contents, '\n'); |
| 43 | + while (lines.next()) |line| { |
| 44 | + const trimmed = std.mem.trim(u8, line, " \t\r"); |
| 45 | + if (trimmed.len == 0 or trimmed[0] == '#') continue; |
| 46 | +
|
| 47 | + var parts = std.mem.splitScalar(u8, trimmed, '='); |
| 48 | +
|
| 49 | + const key = parts.first(); |
| 50 | + const value = std.mem.trim(u8, parts.rest(), " \""); |
| 51 | +
|
| 52 | + run.setEnvironmentVariable(key, value); |
| 53 | + } |
| 54 | +} |
| 55 | +``` |
| 56 | +So the function is pretty simple; It takes a `Run` step (which is basically just a binary that will be built), looks for a `.env` file, if it finds one it parses it and sets environment variables specifically for that `Run` step. This way the environment variables in the `.env` file will be accessible to the program run by that `Run` step just like they would be if any other `.env` library was used. |
| 57 | +### Blog post metadata |
| 58 | +Notice the section at the top of this page that tells you when this post was last edited? That was trickier to implement than you would think. It's easy enough to embed that information in my local environment, but when I ship the source code of this website to a docker container to be run in a server I'm renting, all of the source code gets copied, and the last modified time of all files gets set to the time that they were copied. If I hadn't implemented this step, every blog post would have the same last modified time. Basically, I've written an executable that reads the `blogs` directory and creates a `JSON` map of all the blog posts and their *true* last updated time. Then, when I parse through my blog posts to actually present them I reference the generated `JSON` file to get the true last updated time, rather than the blog post's file's last updated time. This executable is run everytime I make a git commit that includes changes to the blog directory. That way, I don't have to remember to actually run the executable. |
| 59 | +### The code |
| 60 | +```zig |
| 61 | +pub fn main() !void { |
| 62 | + var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); |
| 63 | + defer arena_state.deinit(); |
| 64 | + const arena = arena_state.allocator(); |
| 65 | +
|
| 66 | + const cwd = std.fs.cwd(); |
| 67 | + var dir = try cwd.openDir(BLOG_DIR, .{ .iterate = true }); |
| 68 | + var it = dir.iterate(); |
| 69 | +
|
| 70 | + var files = try std.ArrayList(Metadata).initCapacity(arena, 2048); |
| 71 | +
|
| 72 | + while (try it.next()) |entry| { |
| 73 | + if (entry.kind != .file) continue; |
| 74 | + if (entry.name[0] == '.') continue; |
| 75 | +
|
| 76 | + const stat = try dir.statFile(entry.name); |
| 77 | + log.warn("{s} : {d}\n", .{ entry.name, stat.mtime }); |
| 78 | + try files.append(arena, Metadata{ |
| 79 | + .path = entry.name, |
| 80 | + .last_modified = @as(i64, @intCast(stat.mtime)), |
| 81 | + }); |
| 82 | + } |
| 83 | + var out: std.io.Writer.Allocating = .init(arena); |
| 84 | + try std.json.Stringify.value(files.items, .{ .whitespace = .indent_2 }, &out.writer); |
| 85 | +
|
| 86 | + const json_bytes = out.toOwnedSlice() catch @panic("out of memory"); |
| 87 | +
|
| 88 | + const outfile = "blogsMetadata.json"; |
| 89 | + var file = try std.fs.cwd().createFile(outfile, .{}); |
| 90 | + const realpath = try std.fs.cwd().realpathAlloc(arena, outfile); |
| 91 | + log.warn( |
| 92 | + \\ Writing to file: {s} |
| 93 | + , .{realpath}); |
| 94 | + defer file.close(); |
| 95 | + try file.writeAll(json_bytes); |
| 96 | +} |
| 97 | +``` |
| 98 | +And then in my main `build.zig` file, I've exposed the generated `blogsMetadata.json` file to my program in this single line: |
| 99 | +```zig |
| 100 | +exe.root_module.addAnonymousImport("blogsMetadata.json", .{ .root_source_file = b.path("blogsMetadata.json") }); |
| 101 | +``` |
| 102 | +Now accessing the content of this file is as easy as calling |
| 103 | +```zig |
| 104 | +@embedFile("blogsMetadata.json") |
| 105 | +``` |
| 106 | +It may seem like overkill, and maybe it is but I personally really like having the dates for my blog posts. Thanks for reading this post, next week I plan on going over the frontend. |
0 commit comments