Skip to content

Commit 1c78df0

Browse files
authored
Merge pull request #1 from voidKandy/hand_rolled_https
Hand rolled https
2 parents 8a81ed3 + 6c5fb1c commit 1c78df0

26 files changed

+1440
-524
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
zig-out/
22
test/
3+
tmp/
34
.zig-cache/
45
.env
56
.DS_Store
6-
7+
ssl-bundle/
8+
local_ssl/

Dockerfile

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,24 @@
1-
FROM alpine:3.13 as builder
1+
FROM debian:12
22

3-
# Set the Zig version explicitly
4-
ARG ZIGVER=0.15.1
5-
6-
# Install curl and xz for downloading and extracting Zig
7-
RUN apk update && \
8-
apk add \
9-
curl \
10-
xz
3+
# Install dependencies
4+
RUN apt-get update && apt-get install -y curl xz-utils libc6-dev && \
5+
rm -rf /var/lib/apt/lists/*
116

12-
# Download and extract the Zig compiler
13-
RUN mkdir -p /deps
7+
# Set up Zig
8+
ARG ZIGVER=0.15.1
149
WORKDIR /deps
15-
16-
# Download the Zig binary for the given version
1710
RUN curl -L https://ziglang.org/download/$ZIGVER/zig-x86_64-linux-$ZIGVER.tar.xz -o zig.tar.xz && \
1811
tar xf zig.tar.xz && \
19-
mv zig-x86_64-linux-$ZIGVER /zig
20-
21-
FROM alpine:3.13
22-
23-
RUN apk --no-cache add \
24-
libc-dev \
25-
curl
26-
27-
COPY --from=builder /zig/ /usr/local/zig/
12+
mv zig-x86_64-linux-$ZIGVER /usr/local/zig
2813

2914
ENV PATH="/usr/local/zig:${PATH}"
3015

31-
WORKDIR ./zortfolio
32-
ARG SPOTIFY_CLIENT_ID
33-
ARG SPOTIFY_CLIENT_SECRET
34-
ARG PORT
35-
36-
ADD . ./
37-
38-
RUN ls serve
39-
# So sourcing .env doesnt lead to failure
40-
RUN touch .env
16+
# Copy source directly into final image
17+
WORKDIR /zortfolio
18+
COPY . .
4119

20+
# Build the project
4221
RUN zig build
4322

23+
# Default command
4424
CMD ["./zig-out/bin/zortfolio"]

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ I built this project as a way to learn the Zig programming language, and I had a
77
+ HTMX for routing
88

99
I also tried to minimize my Zig dependencies, and I managed to only need 2:
10-
+ [zap](https://github.com/zigzap/zap)
11-
+ [zdotenv](https://github.com/BitlyTwiser/zdotenv)
1210

1311
Templating was hand-rolled, see [zemplate](https://github.com/voidKandy/zemplate).
1412

blog/tour_pt1.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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.

blogsMetadata.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"path": "post_zero.md",
4+
"last_modified": 1762446122000000000
5+
},
6+
{
7+
"path": "tour_pt1.md",
8+
"last_modified": 1763079879585690989
9+
}
10+
]

build.zig

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,71 @@
11
const std = @import("std");
2+
const log = std.log.scoped(.BUILD);
3+
4+
fn loadDotEnv(run: *std.Build.Step.Run) void {
5+
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
6+
defer arena_state.deinit();
7+
const arena = arena_state.allocator();
8+
var env_file = std.fs.cwd().openFile(".env", .{}) catch |e| {
9+
switch (e) {
10+
error.FileNotFound => {
11+
log.info(
12+
\\ No .env file found
13+
, .{});
14+
},
15+
else => {
16+
log.err(
17+
\\ build.zig could not open .env file: {any}
18+
, .{e});
19+
},
20+
}
21+
return;
22+
};
23+
24+
defer env_file.close();
25+
26+
const read_buffer = arena.alloc(u8, 2048) catch @panic("out of memory");
27+
var reader = env_file.reader(read_buffer);
28+
29+
const contents = reader.interface.allocRemaining(arena, .unlimited) catch @panic("failed to read");
30+
31+
var lines = std.mem.splitScalar(u8, contents, '\n');
32+
while (lines.next()) |line| {
33+
const trimmed = std.mem.trim(u8, line, " \t\r");
34+
if (trimmed.len == 0 or trimmed[0] == '#') continue;
35+
36+
var parts = std.mem.splitScalar(u8, trimmed, '=');
37+
38+
const key = parts.first();
39+
const value = std.mem.trim(u8, parts.rest(), " \"");
40+
41+
run.setEnvironmentVariable(key, value);
42+
}
43+
}
244

345
pub fn build(b: *std.Build) void {
446
const target = b.standardTargetOptions(.{});
547
const optimize = b.standardOptimizeOption(.{});
648

7-
const zap = b.dependency("zap", .{
8-
.target = target,
9-
.optimize = optimize,
10-
});
1149
const zemplate = b.dependency("zemplate", .{});
12-
const zdotenv = b.dependency("zdotenv", .{});
50+
const mime = b.dependency("mime", .{});
51+
const tls = b.dependency("tls", .{});
52+
53+
// Executable used by github actions to dynamically create blogs metadata
54+
{
55+
const exe = b.addExecutable(.{
56+
.name = "read_blog_posts_metadata",
57+
.root_module = b.createModule(.{
58+
.root_source_file = b.path("tools/read_blog_posts_metadata.zig"),
59+
.target = target,
60+
.optimize = optimize,
61+
}),
62+
});
63+
b.installArtifact(exe);
64+
const run_cmd = b.addRunArtifact(exe);
65+
run_cmd.step.dependOn(b.getInstallStep());
66+
const run_step = b.step("metadata", "get blogs metadata");
67+
run_step.dependOn(&run_cmd.step);
68+
}
1369

1470
const exe = b.addExecutable(.{
1571
.name = "zortfolio",
@@ -20,13 +76,16 @@ pub fn build(b: *std.Build) void {
2076
}),
2177
});
2278

23-
exe.root_module.addImport("zap", zap.module("zap"));
2479
exe.root_module.addImport("zemplate", zemplate.module("zemplate"));
25-
exe.root_module.addImport("zdotenv", zdotenv.module("zdotenv"));
80+
exe.root_module.addImport("mime", mime.module("mime"));
81+
exe.root_module.addImport("tls", tls.module("tls"));
82+
83+
exe.root_module.addAnonymousImport("blogsMetadata.json", .{ .root_source_file = b.path("blogsMetadata.json") });
2684

2785
b.installArtifact(exe);
2886

2987
const run_cmd = b.addRunArtifact(exe);
88+
loadDotEnv(run_cmd);
3089

3190
run_cmd.step.dependOn(b.getInstallStep());
3291

@@ -45,9 +104,9 @@ pub fn build(b: *std.Build) void {
45104
.optimize = optimize,
46105
}),
47106
});
48-
exe_unit_tests.root_module.addImport("zdotenv", zdotenv.module("zdotenv"));
49-
exe_unit_tests.root_module.addImport("zap", zap.module("zap"));
107+
exe_unit_tests.root_module.addImport("tls", tls.module("tls"));
50108
exe_unit_tests.root_module.addImport("zemplate", zemplate.module("zemplate"));
109+
exe_unit_tests.root_module.addImport("mime", mime.module("mime"));
51110

52111
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
53112
const test_step = b.step("test", "Run unit tests");

build.zig.zon

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,17 @@
2424
// Once all dependencies are fetched, `zig build` no longer requires
2525
// internet connectivity.
2626
.dependencies = .{
27-
// .zdotenv = .{
28-
// .url = "https://github.com/BitlyTwiser/zdotenv/archive/refs/tags/v0.1.0.tar.gz",
29-
// .hash = "122016a556577271f89aea3e7a052abb7c9731f7478ac350e1e64b07c6620418528f",
30-
// },
31-
.zap = .{
32-
.url = "git+https://github.com/zigzap/zap?ref=v0.11.0#66c5dc42c781bbb8a9100afda3c7e69ee96eddf3",
33-
.hash = "zap-0.10.6-GoeB8xCEJABLgoiZjWZMMT5TsoZ5OO2EZe6j24RTUYEH",
34-
},
3527
.zemplate = .{
36-
.url = "git+https://github.com/voidKandy/zemplate#e6a033a7825311ae2ad1dda87c9fd282c83db6c3",
37-
.hash = "zemplate-0.0.0-tSKpW-U4AAB6fDUsG5QNo9dyiWQGxhMZwDggCbzT-e8P",
28+
.url = "git+https://github.com/voidKandy/zemplate#268c44d98d5c7e4276c53a066898905f61323a22",
29+
.hash = "zemplate-0.0.0-tSKpWz45AAA66Tj0lwjxFtJnV8sNyumE2l45y4uo0vdx",
3830
},
39-
.dotenv = .{
40-
.url = "git+https://github.com/voidKandy/dotenv-zig#dc63b1be756433a6258cea34d43a57bbe8cea18c",
41-
.hash = "dotenv-0.1.1-MkB8yOgdAAD6Np6jqKChP5wD-aPxtZwOhgIBpEVXQgI8",
31+
.tls = .{
32+
.url = "git+https://github.com/ianic/tls.zig#9d56751c99267075908c63a8a91ffc1e7f252df4",
33+
.hash = "tls-0.1.0-ER2e0gNoBQBLuvbg_hPuGQYoCVsn-kHDuupr0hsapu4P",
4234
},
43-
.zdotenv = .{
44-
.url = "git+https://github.com/voidKandy/zdotenv#cd14f7a3d3014b395069e9b4a9a0da504aa58c66",
45-
.hash = "zdotenv-0.0.0-zLj1WiE0AACBNMevVZXF5qyCyQlc_qDXZKDE3lf03-AT",
35+
.mime = .{
36+
.url = "git+https://github.com/andrewrk/mime#4535b74bd5fcaa03e22f2f25af164a4c6253af00",
37+
.hash = "mime-4.0.0-zwmL--0gAACndOVlROQeDnM0chfojNDS8tpohlsBqMNy",
4638
},
4739
},
4840
.paths = .{

components/blogSelectorList.html

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
* @typedef {Object} BlogPostInfo
5858
* @property {BigInt} last_modified - Timestamp of when the post was last modified.
5959
* @property {string} name - Name of the blog post.
60-
* @property {string} path - Path to the blog post.
60+
* @property {string} uri_path - Path to the blog post.
6161
* @property {string} file_name - The name of the file for the blog post.
6262
* @property {string} content - The content of the blog post.
6363
@@ -70,32 +70,31 @@
7070

7171
const posts = this.posts;
7272
this.selectorContainer = this.querySelector('#posts-container');
73-
<!-- this.selectorContainer.classList.add('crt'); -->
7473
/** @type{string} **/
75-
this.currentPostPath = params.get("post") || posts[0].path;
74+
this.currentPostPath = params.get("post") || posts[0].uri_path;
7675

7776
posts.forEach(
7877
/** @param {BlogPostInfo} post */
7978
(post) => {
8079
const button = document.createElement('button');
8180
button.classList.add('crt');
8281
button.textContent = post.name;
83-
button.id = `${post.path}-selector`;
84-
button.setAttribute("hx-get", `/Blog?post=${post.path}`);
82+
button.id = `${post.uri_path}-selector`;
83+
button.setAttribute("hx-get", `/Blog?post=${post.uri_path}`);
8584
button.setAttribute("hx-target", this.target);
8685
button.setAttribute("hx-select", this.target);
8786
button.setAttribute("hx-swap", "outerHTML");
8887
button.setAttribute("hx-push-url", "true");
8988

9089
button.addEventListener("htmx:xhr:loadend", () => {
91-
if (this.currentPostPath != post.path) {
92-
this.currentPostPath = post.path;
90+
if (this.currentPostPath != post.uri_path) {
91+
this.currentPostPath = post.uri_path;
9392
this.updateButtonStyles();
9493
}
9594
});
9695

9796

98-
if (this.currentPostPath == post.path) {
97+
if (this.currentPostPath == post.uri_path) {
9998
button.click();
10099
}
101100
this.selectorContainer.appendChild(button);

0 commit comments

Comments
 (0)