A Bluesky Playdate client
1const std = @import("std");
2const builtin = @import("builtin");
3
4const name = "bluesky";
5pub fn build(b: *std.Build) !void {
6 const pdx_file_name = name ++ ".pdx";
7 const optimize = b.standardOptimizeOption(.{});
8
9 const writer = b.addWriteFiles();
10 const source_dir = writer.getDirectory();
11 writer.step.name = "write source directory";
12
13 const FORCE_COMPILE_M1_MAC = false;
14 const supported_targets = [_]std.Build.ResolvedTarget{
15 host_or_cross_target(
16 b,
17 .{
18 .abi = .msvc,
19 .os_tag = .windows,
20 .cpu_arch = .x86_64,
21 },
22 false,
23 ),
24 host_or_cross_target(
25 b,
26 .{
27 .abi = .none,
28 .os_tag = .macos,
29 .cpu_arch = .aarch64,
30 },
31 FORCE_COMPILE_M1_MAC,
32 ),
33 host_or_cross_target(
34 b,
35 .{
36 .abi = .gnu,
37 .os_tag = .linux,
38 .cpu_arch = .x86_64,
39 },
40 false,
41 ),
42 };
43 for (supported_targets) |target| {
44 try compile_simulator_binary(b, optimize, target, writer);
45 }
46
47 const playdate_target = b.resolveTargetQuery(try std.Target.Query.parse(.{
48 .arch_os_abi = "thumb-freestanding-eabihf",
49 .cpu_features = "cortex_m7+vfp4d16sp",
50 }));
51 const elf = b.addExecutable(.{
52 .name = "pdex.elf",
53 .root_source_file = b.path("src/main.zig"),
54 .target = playdate_target,
55 .optimize = optimize,
56 .pic = true,
57 .single_threaded = true,
58 });
59 elf.link_emit_relocs = true;
60 elf.entry = .{ .symbol_name = "eventHandler" };
61
62 elf.setLinkerScript(b.path("link_map.ld"));
63 if (optimize == .ReleaseFast) {
64 elf.root_module.omit_frame_pointer = true;
65 }
66 _ = writer.addCopyFile(elf.getEmittedBin(), "pdex.elf");
67 _ = writer.addCopyFile(b.path("pdxinfo"), "pdxinfo");
68
69 try addCopyDirectory(writer, "assets", "./assets");
70
71 const playdate_sdk_path = try std.process.getEnvVarOwned(b.allocator, "PLAYDATE_SDK_PATH");
72 const pdc_path = b.pathJoin(&.{ playdate_sdk_path, "bin", if (builtin.os.tag == .windows) "pdc.exe" else "pdc" });
73 const pd_simulator_path = switch (builtin.os.tag) {
74 .linux => b.pathJoin(&.{ playdate_sdk_path, "bin", "PlaydateSimulator" }),
75 .macos => "open", // `open` focuses the window, while running the simulator directry doesn't.
76 .windows => b.pathJoin(&.{ playdate_sdk_path, "bin", "PlaydateSimulator.exe" }),
77 else => @panic("Unsupported OS"),
78 };
79
80 const pdc = b.addSystemCommand(&.{pdc_path});
81 pdc.addDirectoryArg(source_dir);
82 pdc.setName("pdc");
83 const pdx = pdc.addOutputFileArg(pdx_file_name);
84
85 b.installDirectory(.{
86 .source_dir = pdx,
87 .install_dir = .prefix,
88 .install_subdir = pdx_file_name,
89 });
90 b.installDirectory(.{
91 .source_dir = source_dir,
92 .install_dir = .prefix,
93 .install_subdir = "pdx_source_dir",
94 });
95
96 const run_cmd = b.addSystemCommand(&.{pd_simulator_path});
97 run_cmd.addDirectoryArg(pdx);
98 run_cmd.setName("PlaydateSimulator");
99 const run_step = b.step("run", "Run the app");
100 run_step.dependOn(&run_cmd.step);
101 run_step.dependOn(b.getInstallStep());
102
103 const clean_step = b.step("clean", "Clean all artifacts");
104 clean_step.dependOn(&b.addRemoveDirTree(b.path("zig-out")).step);
105 if (builtin.os.tag != .windows) {
106 //Removing zig-cache from the Zig build script does not work on Windows: https://github.com/ziglang/zig/issues/9216
107 clean_step.dependOn(&b.addRemoveDirTree(b.path("zig-cache")).step);
108 clean_step.dependOn(&b.addRemoveDirTree(b.path(".zig-cache")).step);
109 }
110
111 // Add test step
112 const test_step = b.step("test", "Run unit tests");
113
114 // Create test executables for each test file
115 const test_files = [_][]const u8{
116 "src/test_memory.zig",
117 "src/test_json_parser.zig",
118 "src/test_network.zig",
119 };
120
121 for (test_files) |test_file| {
122 const test_exe = b.addTest(.{
123 .root_source_file = b.path(test_file),
124 .optimize = optimize,
125 });
126
127 const run_test = b.addRunArtifact(test_exe);
128 test_step.dependOn(&run_test.step);
129 }
130}
131
132//The purpose of this function is a result of:
133// 1) This script supports cross-compiling PDX's that work on Mac, Windows or Linux without having
134// to compile on those OS's.
135//
136// 2) Inside of a PDX, there can only be 1 pdex executable per OS regardless of the CPU architecture.
137// This has unexpected consequences where, say, a given PDX file can only work on M1 Macs,
138// but not Intel ones. Or, vice versa.
139//
140// So, in the build() function above, I hardcoded ".cpu_arch = .aarch64", which is for M1 Macs.
141// What this means is that if you compiling your game on, say, Windows, it will generate a .pdx
142// that will only work on M1 Macs, but not Intel Macs.
143// BUT, cruicially, if you compiling your game on an Intel Mac, the resulting PDX will work
144// on Intel Macs, but not M1 Macs. Without this function, the game would fail
145// to run on the machine your compiling the code on (Intel Mac), which I'd like to avoid.
146fn host_or_cross_target(
147 b: *std.Build,
148 cross_target: std.Target.Query,
149 force_use_cross_target: bool,
150) std.Build.ResolvedTarget {
151 const result =
152 if (!force_use_cross_target and b.graph.host.result.os.tag == cross_target.os_tag.?)
153 b.graph.host
154 else
155 b.resolveTargetQuery(cross_target);
156 return result;
157}
158
159fn compile_simulator_binary(
160 b: *std.Build,
161 optimize: std.builtin.OptimizeMode,
162 target: std.Build.ResolvedTarget,
163 writer: *std.Build.Step.WriteFile,
164) !void {
165 const os_tag = target.result.os.tag;
166 const lib = b.addSharedLibrary(.{
167 .name = "pdex",
168 .root_source_file = b.path("src/main.zig"),
169 .optimize = optimize,
170 .target = target,
171 });
172 const pdex_extension = switch (os_tag) {
173 .windows => "dll",
174 .macos => "dylib",
175 .linux => "so",
176 else => @panic("Unsupported OS"),
177 };
178 const pdex_filename = try std.fmt.allocPrint(b.allocator, "pdex.{s}", .{pdex_extension});
179 _ = writer.addCopyFile(lib.getEmittedBin(), pdex_filename);
180
181 if (os_tag == .windows) {
182 _ = writer.addCopyFile(lib.getEmittedPdb(), "pdex.pdb");
183 }
184}
185
186fn addCopyDirectory(
187 wf: *std.Build.Step.WriteFile,
188 src_path: []const u8,
189 dest_path: []const u8,
190) !void {
191 const b = wf.step.owner;
192 var dir = try b.build_root.handle.openDir(
193 src_path,
194 .{ .iterate = true },
195 );
196 defer dir.close();
197 var it = dir.iterate();
198 while (try it.next()) |entry| {
199 const new_src_path = b.pathJoin(&.{ src_path, entry.name });
200 const new_dest_path = b.pathJoin(&.{ dest_path, entry.name });
201 const new_src = b.path(new_src_path);
202 switch (entry.kind) {
203 .file => {
204 _ = wf.addCopyFile(new_src, new_dest_path);
205 },
206 .directory => {
207 try addCopyDirectory(
208 wf,
209 new_src_path,
210 new_dest_path,
211 );
212 },
213 //TODO: possible support for sym links?
214 else => {},
215 }
216 }
217}