-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
// Copyright 2025 Shota FUJI
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
//! This file defines how `zig build` command behaves.
//! Run `zig build --help` for available subcommands and options.
//!
//! Learn more at
//! https://ziglang.org/learn/build-system/
const std = @import("std");
const BuildError = error{
NonValaSourceInSourceListError,
};
const RoonExtension = struct {
id: []const u8,
name: []const u8,
version: []const u8,
};
const app_name = "jp.pocka.plac.gtk-adwaita";
const glib_schemas_dir = "share/glib-2.0/schemas";
const glib_compiled_schema_name = "gschemas.compiled";
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const freelog = b.option(bool, "freelog", "Enable logging memory releases") orelse false;
const extension = RoonExtension{
.id = b.option([]const u8, "extension-id", "Roon extension ID") orelse app_name,
.name = b.option([]const u8, "extension-name", "Roon extension name") orelse "Plac for GTK",
.version = b.option([]const u8, "extension-version", "Roon extension version") orelse "0.0.0-dev",
};
const compile_gschema = b.option(bool, "compile-gschema", "Compile gschema XML file for local run") orelse false;
const zig_lib = zig_lib: {
const mod = b.createModule(.{
.root_source_file = b.path("src/core/main.zig"),
.link_libc = true,
.target = target,
.optimize = optimize,
});
mod.addImport("sood", b.dependency("sood", .{}).module("sood"));
mod.addImport("moo", b.dependency("libmoo", .{}).module("moo"));
mod.addImport("websocket", b.dependency("websocket", .{}).module("websocket"));
const options = b.addOptions();
options.addOption(bool, "freelog", freelog);
options.addOption(RoonExtension, "extension", extension);
mod.addOptions("config", options);
const obj = b.addObject(.{
.name = "plac",
.root_module = mod,
});
obj.linkSystemLibrary2("glib-2.0", .{ .preferred_link_mode = .dynamic });
obj.installHeader(b.path("src/core/plac.h"), "plac.h");
obj.installHeader(b.path("src/core/plac.vapi"), "plac.vapi");
break :zig_lib obj;
};
// Vala source codes to compile.
const vala_sources: [][]const u8 = vala_sources: {
var list = std.ArrayList([]const u8).init(b.allocator);
var dir = try std.fs.cwd().openDir("src", .{ .iterate = true });
defer dir.close();
var walker = try dir.walk(b.allocator);
defer walker.deinit();
while (try walker.next()) |entry| {
const ext = std.fs.path.extension(entry.basename);
if (std.mem.eql(u8, ".vala", ext)) {
try list.append(b.dupe(entry.path));
}
}
break :vala_sources try list.toOwnedSlice();
};
// System libraries to link.
const system_libraries = [_][]const u8{
"gtk4",
"libadwaita-1",
// Neither GTK4 nor Libadwaita provides SVG rendering. We need to use librsvg for
// that purpose, whilst undocumented.
"librsvg-2.0",
"gee-0.8",
"libsoup-3.0",
};
// A directory containing C source files compiled from Vala source code.
// If you use this LazyPath in your artifact, the artifact automatically depends on
// this Vala to C complication step. You don't have to manually `a.dependOn()`.
const vala_cdir = vala: {
const valac = b.addSystemCommand(&.{"valac"});
// Tell Vala compiler to emit C rather than compile using system C compiler.
valac.addArg("--ccode");
// Vala's GTK binding incorrectly mark `Gtk.StyleContext` as deprecated, even though
// its static method `add_provider_for_display` is the only future proof way to add
// CSS. As Vala compiler has no CLI flag or magic comment to suppress specific use of
// "deprecated" API, I resorted to suppress entire deprecation warnings. This has risk
// of accidentally using deprecated API. It's still better than getting used to see
// warnings on build and starting to ignore warnings.
valac.addArg("--enable-deprecated");
// Vala compiler uses GResource XML to statically check attributes.
valac.addArg("--gresources");
valac.addFileArg(b.path("data/gresource.xml"));
valac.addArg("--vapidir");
valac.addDirectoryArg(b.path("src/core"));
// Tell Vala what system libraries to use. Perhaps type checking things?
for (system_libraries) |lib| {
valac.addArgs(&.{ "--pkg", lib });
}
valac.addArgs(&.{ "--pkg", "posix" });
valac.addArgs(&.{ "--pkg", "plac" });
// Tell Vala to emit C source files under the output directory.
// The directory usually is Zig cache directory (like ".zig-cache/o/xxx").
// For example, when there is "src/Foo.vala", this step produces
// ".zig-cache/o/hash-or-something-idk/vala/src/Foo.c"
valac.addArg("--directory");
const dir = valac.addOutputDirectoryArg("vala");
// Let valac emit C source code without "src/" prefix.
valac.addArg("--basedir");
valac.addDirectoryArg(b.path("src"));
// Vala compiler takes a list of Vala source files, then process them all.
for (vala_sources) |src| {
valac.addFileArg(b.path(b.pathJoin(&.{ "src", src })));
}
const t = b.addInstallDirectory(.{
.source_dir = dir,
.install_dir = .{ .custom = "src" },
.install_subdir = "",
});
const step = b.step("valac", "Compile C source code from Vala files for debugging purpose");
step.dependOn(&t.step);
break :vala dir;
};
const gresouce_c = gresource: {
const compiler = b.addSystemCommand(&.{"glib-compile-resources"});
compiler.addArg("--sourcedir");
compiler.addDirectoryArg(b.path("data"));
compiler.addFileArg(b.path("data/gresource.xml"));
compiler.addArg("--target");
const built_c = compiler.addOutputFileArg("gresource.c");
compiler.addArg("--dependency-file");
_ = compiler.addDepFileOutputArg("resource.d");
compiler.addArg("--generate-source");
break :gresource built_c;
};
const gschema_compiled = gschema: {
const compiler = b.addSystemCommand(&.{"glib-compile-schemas"});
compiler.addArg("--targetdir");
const out = compiler.addOutputDirectoryArg("compiled");
compiler.addArg("--strict");
compiler.addDirectoryArg(b.path("data/"));
compiler.addFileInput(b.path(b.fmt("data/{s}.gschema.xml", .{app_name})));
break :gschema out.path(b, glib_compiled_schema_name);
};
// An executable.
const exe = exe: {
const exe = b.addExecutable(.{
.name = app_name,
.root_module = b.createModule(.{
// This is a standard C application, so libc is required.
.link_libc = true,
.target = target,
.optimize = optimize,
}),
});
// Vala does not bundle system libraries (of course): we have to tell the
// linker.
for (system_libraries) |lib| {
exe.linkSystemLibrary(lib);
}
exe.root_module.addObject(zig_lib);
exe.addCSourceFile(.{ .file = gresouce_c });
// At this point, build does not run yet—we can't enumerate C source
// directory. Since we already have a list of Vala source code, we can
// build a list of paths of to-be-generated C source file.
for (vala_sources) |src| {
// Basically unreachable. Don't put non Vala source into `vala_sources`.
if (!std.mem.endsWith(u8, src, ".vala")) {
return BuildError.NonValaSourceInSourceListError;
}
// Looks fragile but it works. Even if it produced incorrect paths,
// filesystem and Zig compiler will tell us a file is missing.
const rel_path = b.fmt("{s}.c", .{src[0..(src.len - 5)]});
// src = "src/Foo.vala"
// rel_path = "src/Foo.c"
// file = "(step cache directory)/vala/src/Foo.c"
exe.addCSourceFile(.{
.file = try vala_cdir.join(b.allocator, rel_path),
});
}
break :exe exe;
};
const install_compiled_gschema = b.addInstallFile(
gschema_compiled,
b.pathJoin(&.{ glib_schemas_dir, glib_compiled_schema_name }),
);
// 256x256 PNG icon (required for `.DirIcon`, icon file specific for AppImage)
const icon_png256 = icon_png256: {
const rsvg_convert = b.addSystemCommand(&.{"rsvg-convert"});
rsvg_convert.addArgs(&.{ "--width=256", "--height=256" });
rsvg_convert.addArg("--output");
const output = rsvg_convert.addOutputFileArg(b.fmt("{s}.png", .{app_name}));
// Input filename must be the last argument.
rsvg_convert.addFileArg(b.path("data/app-icon.svg"));
break :icon_png256 output;
};
// Default install step
{
const step = b.getInstallStep();
// Application binary
b.installArtifact(exe);
// GLib schema file
b.installFile(
b.fmt("data/{s}.gschema.xml", .{app_name}),
b.pathJoin(&.{ glib_schemas_dir, b.fmt("{s}.gschema.xml", .{app_name}) }),
);
// GLib schema file (compiled)
if (compile_gschema) {
step.dependOn(&install_compiled_gschema.step);
}
// Desktop entry
b.installFile(
"data/plac.desktop",
b.pathJoin(&.{ "share/applications", b.fmt("{s}.desktop", .{app_name}) }),
);
// Icons
b.installFile(
"data/app-icon.svg",
b.pathJoin(&.{ "share/icons/hicolor/16x16/apps", b.fmt("{s}.svg", .{app_name}) }),
);
step.dependOn(&b.addInstallFile(
icon_png256,
b.pathJoin(&.{ "share/icons/hicolor/256x256/apps", b.fmt("{s}.png", .{app_name}) }),
).step);
}
// `zig build run`
{
const run = b.addRunArtifact(exe);
run.step.dependOn(&install_compiled_gschema.step);
run.setEnvironmentVariable("GSETTINGS_SCHEMA_DIR", b.pathJoin(&.{ b.install_path, glib_schemas_dir }));
const step = b.step("run", "Build and run Plac GTK-Adwaita app");
step.dependOn(&run.step);
}
// `zig build test`
{
const t = b.addTest(.{
.root_module = zig_lib.root_module,
});
const run = b.addRunArtifact(t);
const step = b.step("test", "Run unit tests");
step.dependOn(&run.step);
}
}