authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-17 16:42:37-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-25 18:54:35-07:00
logfb9118195e4a738aff9556389fcbdb9ebacbd020
treef5dcc0405cb6bed70198c0a42360113af97e932a
parent5644d68f147159d3be14378d6cce06e1fa200dc3

maker: implement pkg-config integration

featuring: * better error reporting * including PKG_CONFIG environment variable in `zig env` * memoizing the output of `pkg-config --list-all`

4 files changed, 239 insertions(+), 203 deletions(-)

lib/compiler/Maker.zig+7
......@@ -23,6 +23,7 @@ const Step = @import("Maker/Step.zig");
2323const Watch = @import("Maker/Watch.zig");
2424const WebServer = @import("Maker/WebServer.zig");
2525const ScannedConfig = @import("Maker/ScannedConfig.zig");
26const PkgConfig = @import("Maker/PkgConfig.zig");
2627
2728pub const std_options: std.Options = .{
2829 .side_channels_mitigations = .none,
......@@ -48,6 +49,7 @@ web_server: if (!builtin.single_threaded) ?WebServer else ?noreturn,
4849memory_blocked_steps: std.ArrayList(Configuration.Step.Index),
4950/// Allocated into `gpa`.
5051step_stack: std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void),
52pkg_config: PkgConfig,
5153
5254error_style: ErrorStyle,
5355multiline_errors: MultilineErrors,
......@@ -540,6 +542,11 @@ pub fn main(init: process.Init.Minimal) !void {
540542 .web_server = undefined, // set after `prepare`
541543 .memory_blocked_steps = .empty,
542544 .step_stack = .empty,
545 .pkg_config = .{
546 .mutex = .init,
547 .list = null,
548 .debug = debug_pkg_config,
549 },
543550
544551 .error_style = error_style,
545552 .multiline_errors = multiline_errors,
lib/compiler/Maker/PkgConfig.zig created+203
......@@ -0,0 +1,203 @@
1const std = @import("std");
2const Io = std.Io;
3const mem = std.mem;
4
5const Maker = @import("../Maker.zig");
6const Step = @import("Step.zig");
7const Graph = @import("Graph.zig");
8
9pub const Pkg = struct {
10 name: []const u8,
11 desc: []const u8,
12};
13
14mutex: Io.Mutex = .init,
15list: ?[]const Pkg = null,
16debug: bool = false,
17
18pub const RunError = error{
19 PackageNotFound,
20 PkgConfigUnavailable,
21} || Step.ExtendedMakeError;
22
23pub const Result = struct {
24 cflags: []const []const u8,
25 libs: []const []const u8,
26};
27
28/// Run pkg-config for the given library name and parse the output, returning the arguments
29/// that should be passed to zig to link the given library.
30pub fn run(
31 maker: *Maker,
32 step: *Step,
33 progress_node: std.Progress.Node,
34 lib_name: []const u8,
35 /// If true, reports failure error messages on step rather than returning
36 /// error.PackageNotFound or error.PkgConfigInvalidOutput,
37 force: bool,
38) RunError!Result {
39 const pc = &maker.pkg_config;
40 const graph = maker.graph;
41 const arena = graph.arena; // TODO don't leak into process arena
42 const wl_rpath_prefix = "-Wl,-rpath,";
43
44 const pkg_name = match: {
45 // First we have to map the library name to pkg config name. Unfortunately,
46 // there are several examples where this is not straightforward:
47 // -lSDL2 -> pkg-config sdl2
48 // -lgdk-3 -> pkg-config gdk-3.0
49 // -latk-1.0 -> pkg-config atk
50 // -lpulse -> pkg-config libpulse
51 const pkgs = try getList(maker, step, progress_node, force);
52
53 // Exact match means instant winner.
54 for (pkgs) |pkg| {
55 if (mem.eql(u8, pkg.name, lib_name)) {
56 break :match pkg.name;
57 }
58 }
59
60 // Next we'll try ignoring case.
61 for (pkgs) |pkg| {
62 if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) {
63 break :match pkg.name;
64 }
65 }
66
67 // Prefixed "lib" or suffixed ".0".
68 for (pkgs) |pkg| {
69 if (std.ascii.findIgnoreCase(pkg.name, lib_name)) |pos| {
70 const prefix = pkg.name[0..pos];
71 const suffix = pkg.name[pos + lib_name.len ..];
72 if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue;
73 if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue;
74 break :match pkg.name;
75 }
76 }
77
78 // Trimming "-1.0".
79 if (mem.endsWith(u8, lib_name, "-1.0")) {
80 const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len];
81 for (pkgs) |pkg| {
82 if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) {
83 break :match pkg.name;
84 }
85 }
86 }
87
88 if (force) return step.fail(maker, "{s}: package not found: {s}", .{
89 getExe(graph), lib_name,
90 });
91
92 return error.PackageNotFound;
93 };
94
95 const pkg_config_exe = getExe(graph);
96 const captured = try step.captureChildProcess(maker, progress_node, &.{
97 pkg_config_exe, pkg_name, "--cflags", "--libs",
98 });
99 try step.handleChildProcessTerm(maker, captured.term);
100
101 var zig_cflags: std.ArrayList([]const u8) = .empty;
102 var zig_libs: std.ArrayList([]const u8) = .empty;
103 var arg_it = mem.tokenizeAny(u8, captured.stdout, " \r\n\t");
104
105 while (arg_it.next()) |arg| {
106 if (mem.eql(u8, arg, "-I")) {
107 const dir = arg_it.next() orelse return missingArg(maker, step, pkg_config_exe, lib_name, arg, force);
108 try zig_cflags.appendSlice(arena, &.{ "-I", dir });
109 } else if (mem.startsWith(u8, arg, "-I")) {
110 try zig_cflags.append(arena, arg);
111 } else if (mem.eql(u8, arg, "-L")) {
112 const dir = arg_it.next() orelse return missingArg(maker, step, pkg_config_exe, lib_name, arg, force);
113 try zig_libs.appendSlice(arena, &.{ "-L", dir });
114 } else if (mem.startsWith(u8, arg, "-L")) {
115 try zig_libs.append(arena, arg);
116 } else if (mem.eql(u8, arg, "-l")) {
117 const lib = arg_it.next() orelse return missingArg(maker, step, pkg_config_exe, lib_name, arg, force);
118 try zig_libs.appendSlice(arena, &.{ "-l", lib });
119 } else if (mem.startsWith(u8, arg, "-l")) {
120 try zig_libs.append(arena, arg);
121 } else if (mem.eql(u8, arg, "-D")) {
122 const macro = arg_it.next() orelse return missingArg(maker, step, pkg_config_exe, lib_name, arg, force);
123 try zig_cflags.appendSlice(arena, &.{ "-D", macro });
124 } else if (mem.startsWith(u8, arg, "-D")) {
125 try zig_cflags.append(arena, arg);
126 } else if (mem.startsWith(u8, arg, wl_rpath_prefix)) {
127 try zig_cflags.appendSlice(arena, &.{ "-rpath", arg[wl_rpath_prefix.len..] });
128 } else if (force or pc.debug) {
129 return step.fail(maker, "{s} package {s} unknown flag: {s}", .{ pkg_config_exe, lib_name, arg });
130 }
131 }
132
133 try zig_cflags.shrinkToLen(arena);
134 try zig_libs.shrinkToLen(arena);
135
136 return .{
137 .cflags = zig_cflags.toOwnedSliceAssert(),
138 .libs = zig_libs.toOwnedSliceAssert(),
139 };
140}
141
142fn missingArg(
143 maker: *Maker,
144 step: *Step,
145 pkg_config_exe: []const u8,
146 lib_name: []const u8,
147 arg: []const u8,
148 force: bool,
149) RunError {
150 if (force) return step.fail(maker, "{s} package {s} missing arg after flag: {s}", .{
151 pkg_config_exe, lib_name, arg,
152 });
153 return error.PkgConfigUnavailable;
154}
155
156fn getExe(graph: *const Graph) []const u8 {
157 return std.zig.EnvVar.PKG_CONFIG.get(&graph.environ_map) orelse "pkg-config";
158}
159
160fn getList(maker: *Maker, step: *Step, progress_node: std.Progress.Node, force: bool) RunError![]const Pkg {
161 const graph = maker.graph;
162 const arena = graph.arena; // TODO don't leak into process arena
163 const io = graph.io;
164 const pc = &maker.pkg_config;
165
166 try pc.mutex.lock(io);
167 defer pc.mutex.unlock(io);
168
169 if (pc.list) |list| return list;
170
171 const pkg_config_exe = getExe(graph);
172 const captured = try step.captureChildProcess(maker, progress_node, &.{ pkg_config_exe, "--list-all" });
173 if (force) {
174 try step.handleChildProcessTerm(maker, captured.term);
175 } else switch (captured.term) {
176 .exited => |code| if (code != 0) return error.PkgConfigUnavailable,
177 else => {
178 try step.handleChildProcessTerm(maker, captured.term);
179 unreachable;
180 },
181 }
182
183 var list: std.ArrayList(Pkg) = .empty;
184 var line_it = mem.tokenizeAny(u8, captured.stdout, "\r\n");
185 while (line_it.next()) |line| {
186 if (mem.trim(u8, line, " \t").len == 0) continue;
187 var tok_it = mem.tokenizeAny(u8, line, " \t");
188 try list.append(arena, .{
189 .name = tok_it.next() orelse {
190 if (force) return step.fail(maker, "{s}: invalid line: {s}", .{
191 pkg_config_exe, line,
192 });
193 return error.PkgConfigUnavailable;
194 },
195 .desc = tok_it.rest(),
196 });
197 }
198 try list.shrinkToLen(arena);
199
200 const result = list.toOwnedSliceAssert();
201 pc.list = result;
202 return result;
203}
lib/compiler/Maker/Step/Compile.zig+28-203
......@@ -14,6 +14,7 @@ const allocPrint = std.fmt.allocPrint;
1414
1515const Step = @import("../Step.zig");
1616const Maker = @import("../../Maker.zig");
17const PkgConfig = @import("../PkgConfig.zig");
1718
1819/// Populated when there is compiler process that lives across multiple calls
1920/// to `make`.
......@@ -40,7 +41,7 @@ pub fn make(
4041 // Reset / repopulate persistent state.
4142 compile.zig_args.clearRetainingCapacity();
4243
43 try lowerZigArgs(compile, compile_index, maker, &compile.zig_args, false);
44 try lowerZigArgs(compile, compile_index, maker, progress_node, &compile.zig_args, false);
4445
4546 const maybe_output_dir = Step.evalZigProcess(
4647 compile_index,
......@@ -148,10 +149,11 @@ const ModuleListContext = struct {
148149fn lowerZigArgs(
149150 compile: *Compile,
150151 compile_index: Configuration.Step.Index,
151 maker: *const Maker,
152 maker: *Maker,
153 progress_node: std.Progress.Node,
152154 zig_args: *std.ArrayList([]const u8),
153155 fuzz: bool,
154) error{ OutOfMemory, MakeFailed }!void {
156) Step.ExtendedMakeError!void {
155157 const step = maker.stepByIndex(compile_index);
156158 const graph = maker.graph;
157159 const arena = graph.arena; // TODO don't leak into the process arena
......@@ -312,40 +314,36 @@ fn lowerZigArgs(
312314 if (system_lib.flags.weak) break :prefix "-weak-l";
313315 break :prefix "-l";
314316 };
315 switch (system_lib.flags.use_pkg_config) {
316 .no => try zig_args.append(gpa, try allocPrint(arena, "{s}{s}", .{
317 prefix, system_lib_name,
318 })),
319 .yes, .force => {
320 if (compile.runPkgConfig(maker, system_lib_name)) |result| {
317 l: {
318 pc: {
319 const force = switch (system_lib.flags.use_pkg_config) {
320 .no => break :pc,
321 .yes => false,
322 .force => true,
323 };
324
325 const pkg_conf_node = progress_node.start("pkg-config", 0);
326 defer pkg_conf_node.end();
327
328 if (PkgConfig.run(maker, step, pkg_conf_node, system_lib_name, force)) |result| {
321329 try zig_args.appendSlice(gpa, result.cflags);
322330 try zig_args.appendSlice(gpa, result.libs);
323331 try seen_system_libs.put(arena, system_lib.name, result.cflags);
332 break :l;
324333 } else |err| switch (err) {
325 error.PkgConfigInvalidOutput,
326 error.PkgConfigCrashed,
327 error.PkgConfigFailed,
328 error.PkgConfigNotInstalled,
334 error.PkgConfigUnavailable,
329335 error.PackageNotFound,
330 => switch (system_lib.flags.use_pkg_config) {
331 .yes => {
332 // pkg-config failed, so fall back to linking the library
333 // by name directly.
334 try zig_args.append(gpa, try allocPrint(arena, "{s}{s}", .{
335 prefix, system_lib_name,
336 }));
337 },
338 .force => {
339 return step.fail(maker, "pkg-config failed for library {s}", .{
340 system_lib_name,
341 });
342 },
343 .no => unreachable,
336 => {
337 // pkg-config failed, so fall back to linking the library by name directly.
338 assert(!force);
339 break :pc;
344340 },
345
346341 else => |e| return e,
347342 }
348 },
343 }
344 try zig_args.append(gpa, try allocPrint(arena, "{s}{s}", .{
345 prefix, system_lib_name,
346 }));
349347 }
350348 },
351349 .other_step => |other_step_index| {
......@@ -961,65 +959,11 @@ pub fn rebuildInFuzzMode(compile: *Compile, maker: *Maker, progress_node: std.Pr
961959
962960 const zig_args = &compile.zig_args;
963961 zig_args.clearRetainingCapacity();
964 try lowerZigArgs(compile, maker, zig_args, true);
962 try lowerZigArgs(compile, maker, progress_node, zig_args, true);
965963 const maybe_output_bin_path = try compile.step.evalZigProcess(zig_args.items, progress_node, false, maker);
966964 return maybe_output_bin_path.?;
967965}
968966
969pub const PkgConfigError = error{
970 PkgConfigCrashed,
971 PkgConfigFailed,
972 PkgConfigNotInstalled,
973 PkgConfigInvalidOutput,
974};
975
976pub const PkgConfigPkg = struct {
977 name: []const u8,
978 desc: []const u8,
979};
980
981fn execPkgConfigList(maker: *Maker, out_code: *u8) (PkgConfigError || Maker.RunError)![]const PkgConfigPkg {
982 const graph = maker.graph;
983 const process_arena = graph.arena; // TODO don't leak into process arena
984 const pkg_config_exe = graph.environ_map.get("PKG_CONFIG") orelse "pkg-config";
985 const stdout = try maker.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .ignore);
986 var list = std.array_list.Managed(PkgConfigPkg).init(process_arena);
987 errdefer list.deinit();
988 var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
989 while (line_it.next()) |line| {
990 if (mem.trim(u8, line, " \t").len == 0) continue;
991 var tok_it = mem.tokenizeAny(u8, line, " \t");
992 try list.append(PkgConfigPkg{
993 .name = tok_it.next() orelse return error.PkgConfigInvalidOutput,
994 .desc = tok_it.rest(),
995 });
996 }
997 return list.toOwnedSlice();
998}
999
1000fn getPkgConfigList(b: *std.Build) ![]const PkgConfigPkg {
1001 if (b.pkg_config_pkg_list) |res| {
1002 return res;
1003 }
1004 var code: u8 = undefined;
1005 if (execPkgConfigList(b, &code)) |list| {
1006 b.pkg_config_pkg_list = list;
1007 return list;
1008 } else |err| {
1009 const result = switch (err) {
1010 error.ProcessTerminated => error.PkgConfigCrashed,
1011 error.ExecNotSupported => error.PkgConfigFailed,
1012 error.ExitCodeFailure => error.PkgConfigFailed,
1013 error.FileNotFound => error.PkgConfigNotInstalled,
1014 error.InvalidName => error.PkgConfigNotInstalled,
1015 error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput,
1016 else => return err,
1017 };
1018 b.pkg_config_pkg_list = result;
1019 return result;
1020 }
1021}
1022
1023967fn addBool(gpa: Allocator, args: *std.ArrayList([]const u8), arg: []const u8, opt: bool) !void {
1024968 if (opt) try args.append(gpa, arg);
1025969}
......@@ -1029,125 +973,6 @@ fn addFlag(gpa: Allocator, args: *std.ArrayList([]const u8), comptime name: []co
1029973 try args.append(gpa, if (cond) "-f" ++ name else "-fno-" ++ name);
1030974}
1031975
1032const PkgConfigResult = struct {
1033 cflags: []const []const u8,
1034 libs: []const []const u8,
1035};
1036
1037/// Run pkg-config for the given library name and parse the output, returning the arguments
1038/// that should be passed to zig to link the given library.
1039fn runPkgConfig(compile: *const Compile, maker: *const Maker, lib_name: []const u8) !PkgConfigResult {
1040 if (true) @panic("TODO runPkgConfig");
1041 const graph = maker.graph;
1042 const wl_rpath_prefix = "-Wl,-rpath,";
1043
1044 const b = compile.step.owner;
1045 const arena = b.allocator;
1046 const pkg_name = match: {
1047 // First we have to map the library name to pkg config name. Unfortunately,
1048 // there are several examples where this is not straightforward:
1049 // -lSDL2 -> pkg-config sdl2
1050 // -lgdk-3 -> pkg-config gdk-3.0
1051 // -latk-1.0 -> pkg-config atk
1052 // -lpulse -> pkg-config libpulse
1053 const pkgs = try getPkgConfigList(b);
1054
1055 // Exact match means instant winner.
1056 for (pkgs) |pkg| {
1057 if (mem.eql(u8, pkg.name, lib_name)) {
1058 break :match pkg.name;
1059 }
1060 }
1061
1062 // Next we'll try ignoring case.
1063 for (pkgs) |pkg| {
1064 if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) {
1065 break :match pkg.name;
1066 }
1067 }
1068
1069 // Prefixed "lib" or suffixed ".0".
1070 for (pkgs) |pkg| {
1071 if (std.ascii.findIgnoreCase(pkg.name, lib_name)) |pos| {
1072 const prefix = pkg.name[0..pos];
1073 const suffix = pkg.name[pos + lib_name.len ..];
1074 if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue;
1075 if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue;
1076 break :match pkg.name;
1077 }
1078 }
1079
1080 // Trimming "-1.0".
1081 if (mem.endsWith(u8, lib_name, "-1.0")) {
1082 const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len];
1083 for (pkgs) |pkg| {
1084 if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) {
1085 break :match pkg.name;
1086 }
1087 }
1088 }
1089
1090 return error.PackageNotFound;
1091 };
1092
1093 var code: u8 = undefined;
1094 const pkg_config_exe = graph.environ_map.get("PKG_CONFIG") orelse "pkg-config";
1095 const stdout = if (b.runAllowFail(&[_][]const u8{
1096 pkg_config_exe,
1097 pkg_name,
1098 "--cflags",
1099 "--libs",
1100 }, &code, .ignore)) |stdout| stdout else |err| switch (err) {
1101 error.ProcessTerminated => return error.PkgConfigCrashed,
1102 error.ExecNotSupported => return error.PkgConfigFailed,
1103 error.ExitCodeFailure => return error.PkgConfigFailed,
1104 error.FileNotFound => return error.PkgConfigNotInstalled,
1105 else => return err,
1106 };
1107
1108 var zig_cflags: std.ArrayList([]const u8) = .empty;
1109 defer zig_cflags.deinit(arena);
1110 var zig_libs: std.ArrayList([]const u8) = .empty;
1111 defer zig_libs.deinit(arena);
1112
1113 var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t");
1114 while (arg_it.next()) |arg| {
1115 if (mem.eql(u8, arg, "-I")) {
1116 const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
1117 try zig_cflags.appendSlice(arena, &.{ "-I", dir });
1118 } else if (mem.startsWith(u8, arg, "-I")) {
1119 try zig_cflags.append(arena, arg);
1120 } else if (mem.eql(u8, arg, "-L")) {
1121 const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
1122 try zig_libs.appendSlice(arena, &.{ "-L", dir });
1123 } else if (mem.startsWith(u8, arg, "-L")) {
1124 try zig_libs.append(arena, arg);
1125 } else if (mem.eql(u8, arg, "-l")) {
1126 const lib = arg_it.next() orelse return error.PkgConfigInvalidOutput;
1127 try zig_libs.appendSlice(arena, &.{ "-l", lib });
1128 } else if (mem.startsWith(u8, arg, "-l")) {
1129 try zig_libs.append(arena, arg);
1130 } else if (mem.eql(u8, arg, "-D")) {
1131 const macro = arg_it.next() orelse return error.PkgConfigInvalidOutput;
1132 try zig_cflags.appendSlice(arena, &.{ "-D", macro });
1133 } else if (mem.startsWith(u8, arg, "-D")) {
1134 try zig_cflags.append(arena, arg);
1135 } else if (mem.startsWith(u8, arg, wl_rpath_prefix)) {
1136 try zig_cflags.appendSlice(arena, &.{ "-rpath", arg[wl_rpath_prefix.len..] });
1137 } else if (b.debug_pkg_config) {
1138 return compile.step.fail(maker, "unknown pkg-config flag '{s}'", .{arg});
1139 }
1140 }
1141
1142 try zig_cflags.shrinkToLen(arena);
1143 try zig_libs.shrinkToLen(arena);
1144
1145 return .{
1146 .cflags = zig_cflags.toOwnedSliceAssert(),
1147 .libs = zig_libs.toOwnedSliceAssert(),
1148 };
1149}
1150
1151976fn checkCompileErrors(compile: *Compile, maker: *Maker) !void {
1152977 if (true) @panic("TODO checkCompileErrors");
1153978 // Clear this field so that it does not get printed by the build runner.
lib/std/zig.zig+1
......@@ -772,6 +772,7 @@ pub const EnvVar = enum {
772772 CPLUS_INCLUDE_PATH,
773773 LIBRARY_PATH,
774774 CC,
775 PKG_CONFIG,
775776
776777 // Terminal integration
777778 NO_COLOR,