authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-21 17:29:31-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-25 18:54:36-07:00
logdf8aaad05852b18e5a47aa09d5acdfd312583d1c
tree61f37952ecddd8213aee39abb3df359db740feeb
parentc678f94daa31f7ba8ea9f459eb4c442b02a8610c

maker: extract some pkg-config logic into reusable API


4 files changed, 184 insertions(+), 138 deletions(-)

lib/compiler/Maker.zig+1-6
......@@ -532,11 +532,7 @@ pub fn main(init: process.Init.Minimal) !void {
532532 .web_server = undefined, // set after `prepare`
533533 .memory_blocked_steps = .empty,
534534 .step_stack = .empty,
535 .pkg_config = .{
536 .mutex = .init,
537 .list = null,
538 .debug = debug_pkg_config,
539 },
535 .pkg_config = .{ .debug = debug_pkg_config },
540536
541537 .error_style = error_style,
542538 .multiline_errors = multiline_errors,
......@@ -1882,7 +1878,6 @@ pub fn truncatePath(
18821878 if (graph.verbose) try graph.handleVerbose(.inherit, null, &.{
18831879 "truncate", try dest_path.toString(arena),
18841880 });
1885 // https://codeberg.org/ziglang/zig/issues/35353
18861881 const err = e: {
18871882 var file = f: {
18881883 break :f dest_path.root_dir.handle.createFile(io, dest_path.sub_path, .{}) catch |err| switch (err) {
lib/compiler/Maker/PkgConfig.zig+36-132
......@@ -7,13 +7,8 @@ const Maker = @import("../Maker.zig");
77const Step = @import("Step.zig");
88const Graph = @import("Graph.zig");
99
10pub const Pkg = struct {
11 name: []const u8,
12 desc: []const u8,
13};
14
1510mutex: Io.Mutex = .init,
16list: ?[]const Pkg = null,
11pkgs: ?std.zig.PkgConfig = null,
1712debug: bool = false,
1813
1914pub const RunError = error{
......@@ -21,10 +16,7 @@ pub const RunError = error{
2116 PkgConfigUnavailable,
2217} || Step.ExtendedMakeError;
2318
24pub const Result = struct {
25 cflags: []const []const u8,
26 libs: []const []const u8,
27};
19pub const Result = std.zig.PkgConfig.Parsed;
2820
2921/// Run pkg-config for the given library name and parse the output, returning the arguments
3022/// that should be passed to zig to link the given library.
......@@ -34,131 +26,50 @@ pub fn run(
3426 progress_node: std.Progress.Node,
3527 lib_name: []const u8,
3628 /// If true, reports failure error messages on step rather than returning
37 /// error.PackageNotFound or error.PkgConfigInvalidOutput,
29 /// error.PackageNotFound or error.PkgConfigUnavailable,
3830 force: bool,
3931) RunError!Result {
4032 const pc = &maker.pkg_config;
4133 const graph = maker.graph;
4234 const arena = graph.arena; // TODO don't leak into process arena
4335
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
36 const pkg_config_exe = getExe(graph);
37 const pkgs = try getPkgs(maker, step, progress_node, force);
38 const found_index = pkgs.find(lib_name) orelse {
39 if (force) return step.fail(maker, "{s}: package not found: {s}", .{ pkg_config_exe, lib_name });
9240 return error.PackageNotFound;
9341 };
42 const pkg = pkgs.all[found_index];
9443
95 const pkg_config_exe = getExe(graph);
9644 const stdout = try captureChildProcess(maker, step, .{
97 .argv = &.{ pkg_config_exe, pkg_name, "--cflags", "--libs" },
45 .argv = &.{ pkg_config_exe, pkg.name, "--cflags", "--libs" },
9846 .progress_node = progress_node,
9947 .allow_failure = !force,
10048 });
10149
102 var zig_cflags: std.ArrayList([]const u8) = .empty;
103 var zig_libs: std.ArrayList([]const u8) = .empty;
104 var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t");
105
106 while (arg_it.next()) |arg| {
107 if (mem.eql(u8, arg, "-I")) {
108 const dir = arg_it.next() orelse return missingArg(maker, step, pkg_config_exe, lib_name, arg, force);
109 try zig_cflags.appendSlice(arena, &.{ "-I", dir });
110 } else if (mem.startsWith(u8, arg, "-I")) {
111 try zig_cflags.append(arena, arg);
112 } else if (mem.eql(u8, arg, "-L")) {
113 const dir = arg_it.next() orelse return missingArg(maker, step, pkg_config_exe, lib_name, arg, force);
114 try zig_libs.appendSlice(arena, &.{ "-L", dir });
115 } else if (mem.startsWith(u8, arg, "-L")) {
116 try zig_libs.append(arena, arg);
117 } else if (mem.eql(u8, arg, "-l")) {
118 const lib = arg_it.next() orelse return missingArg(maker, step, pkg_config_exe, lib_name, arg, force);
119 try zig_libs.appendSlice(arena, &.{ "-l", lib });
120 } else if (mem.startsWith(u8, arg, "-l")) {
121 try zig_libs.append(arena, arg);
122 } else if (mem.eql(u8, arg, "-D")) {
123 const macro = arg_it.next() orelse return missingArg(maker, step, pkg_config_exe, lib_name, arg, force);
124 try zig_cflags.appendSlice(arena, &.{ "-D", macro });
125 } else if (mem.startsWith(u8, arg, "-D")) {
126 try zig_cflags.append(arena, arg);
127 } else if (mem.cutPrefix(u8, arg, "-Wl,-rpath,")) |rest| {
128 try zig_cflags.appendSlice(arena, &.{ "-rpath", rest });
129 } else if (force or pc.debug) {
130 return step.fail(maker, "{s} package {s} unknown flag: {s}", .{ pkg_config_exe, lib_name, arg });
50 const parsed = std.zig.PkgConfig.parse(arena, stdout) catch |err| switch (err) {
51 error.InvalidPkgConfigOutput => {
52 if (force) return step.fail(maker, "{s} package {s} invalid output: {s}", .{
53 pkg_config_exe, lib_name, stdout,
54 });
55 return error.PkgConfigUnavailable;
56 },
57 else => |e| return e,
58 };
59 if (force or pc.debug) {
60 for (parsed.unknown_flags) |unknown_flag| {
61 return step.fail(maker, "{s} package {s} unknown flag: {s}", .{ pkg_config_exe, lib_name, unknown_flag });
13162 }
13263 }
13364
134 try zig_cflags.shrinkToLen(arena);
135 try zig_libs.shrinkToLen(arena);
136
137 return .{
138 .cflags = zig_cflags.toOwnedSliceAssert(),
139 .libs = zig_libs.toOwnedSliceAssert(),
140 };
141}
142
143fn missingArg(
144 maker: *Maker,
145 step: *Step,
146 pkg_config_exe: []const u8,
147 lib_name: []const u8,
148 arg: []const u8,
149 force: bool,
150) RunError {
151 if (force) return step.fail(maker, "{s} package {s} missing arg after flag: {s}", .{
152 pkg_config_exe, lib_name, arg,
153 });
154 return error.PkgConfigUnavailable;
65 return parsed;
15566}
15667
15768fn getExe(graph: *const Graph) []const u8 {
158 return std.zig.EnvVar.PKG_CONFIG.get(&graph.environ_map) orelse "pkg-config";
69 return std.zig.PkgConfig.exe(&graph.environ_map);
15970}
16071
161fn getList(maker: *Maker, step: *Step, progress_node: std.Progress.Node, force: bool) RunError![]const Pkg {
72fn getPkgs(maker: *Maker, step: *Step, progress_node: std.Progress.Node, force: bool) RunError!std.zig.PkgConfig {
16273 const graph = maker.graph;
16374 const arena = graph.arena; // TODO don't leak into process arena
16475 const io = graph.io;
......@@ -167,7 +78,7 @@ fn getList(maker: *Maker, step: *Step, progress_node: std.Progress.Node, force:
16778 try pc.mutex.lock(io);
16879 defer pc.mutex.unlock(io);
16980
170 if (pc.list) |list| return list;
81 if (pc.pkgs) |pkgs| return pkgs;
17182
17283 const pkg_config_exe = getExe(graph);
17384 const stdout = try captureChildProcess(maker, step, .{
......@@ -176,25 +87,18 @@ fn getList(maker: *Maker, step: *Step, progress_node: std.Progress.Node, force:
17687 .allow_failure = !force,
17788 });
17889
179 var list: std.ArrayList(Pkg) = .empty;
180 var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
181 while (line_it.next()) |line| {
182 if (mem.trim(u8, line, " \t").len == 0) continue;
183 var tok_it = mem.tokenizeAny(u8, line, " \t");
184 try list.append(arena, .{
185 .name = tok_it.next() orelse {
186 if (force) return step.fail(maker, "{s}: invalid line: {s}", .{
187 pkg_config_exe, line,
188 });
189 return error.PkgConfigUnavailable;
190 },
191 .desc = tok_it.rest(),
192 });
193 }
194 try list.shrinkToLen(arena);
90 var diagnostic: std.zig.PkgConfig.Diagnostic = undefined;
91 const result = std.zig.PkgConfig.init(arena, stdout, &diagnostic) catch |err| switch (err) {
92 error.InvalidPkgConfigOutput => {
93 if (force) return step.fail(maker, "{s}: invalid line({d}): {s}", .{
94 pkg_config_exe, diagnostic.invalid_line_index + 1, diagnostic.invalid_line,
95 });
96 return error.PkgConfigUnavailable;
97 },
98 else => |e| return e,
99 };
195100
196 const result = list.toOwnedSliceAssert();
197 pc.list = result;
101 pc.pkgs = result;
198102 return result;
199103}
200104
lib/std/zig.zig+1
......@@ -33,6 +33,7 @@ pub const AstRlAnnotate = @import("zig/AstRlAnnotate.zig");
3333pub const LibCInstallation = @import("zig/LibCInstallation.zig");
3434pub const WindowsSdk = @import("zig/WindowsSdk.zig");
3535pub const LibCDirs = @import("zig/LibCDirs.zig");
36pub const PkgConfig = @import("zig/PkgConfig.zig");
3637pub const target = @import("zig/target.zig");
3738pub const llvm = @import("zig/llvm.zig");
3839
lib/std/zig/PkgConfig.zig created+146
......@@ -0,0 +1,146 @@
1//! The more reusable pieces of the build system's pkg-config integration logic.
2const PkgConfig = @This();
3
4const std = @import("../std.zig");
5const mem = std.mem;
6const Allocator = std.mem.Allocator;
7const assert = std.debug.assert;
8
9all: []const Pkg,
10
11pub const Pkg = struct {
12 name: []const u8,
13 desc: []const u8,
14};
15
16pub const InitError = Allocator.Error || error{InvalidPkgConfigOutput};
17
18pub const Diagnostic = struct {
19 invalid_line_index: usize,
20 invalid_line: []const u8,
21};
22
23/// Parses the output of `pkg-config --list-all`.
24pub fn init(arena: Allocator, stdout: []const u8, diagnostic: ?*Diagnostic) InitError!PkgConfig {
25 var list: std.ArrayList(Pkg) = .empty;
26 var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
27 var line_index: usize = 0;
28 while (line_it.next()) |line| : (line_index += 1) {
29 if (mem.trim(u8, line, " \t").len == 0) continue;
30 var tok_it = mem.tokenizeAny(u8, line, " \t");
31 try list.append(arena, .{
32 .name = tok_it.next() orelse {
33 if (diagnostic) |d| d.* = .{
34 .invalid_line_index = line_index,
35 .invalid_line = line,
36 };
37 return error.InvalidPkgConfigOutput;
38 },
39 .desc = tok_it.rest(),
40 });
41 }
42 try list.shrinkToLen(arena);
43 return .{ .all = list.toOwnedSliceAssert() };
44}
45
46// Maps the library name to pkg config name. Unfortunately, there are several
47// examples where this is not straightforward:
48// * -lSDL2 -> pkg-config sdl2
49// * -lgdk-3 -> pkg-config gdk-3.0
50// * -latk-1.0 -> pkg-config atk
51// * -lpulse -> pkg-config libpulse
52pub fn find(pc: *const PkgConfig, lib_name: []const u8) ?usize {
53 const all = pc.all;
54
55 // Exact match means instant winner.
56 for (all, 0..) |pkg, i| {
57 if (mem.eql(u8, pkg.name, lib_name))
58 return i;
59 }
60
61 // Next we'll try ignoring case.
62 for (all, 0..) |pkg, i| {
63 if (std.ascii.eqlIgnoreCase(pkg.name, lib_name))
64 return i;
65 }
66
67 // Prefixed "lib" or suffixed ".0".
68 for (all, 0..) |pkg, i| {
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 return i;
75 }
76 }
77
78 // Trimming "-1.0".
79 if (mem.cutSuffix(u8, lib_name, "-1.0")) |trimmed| {
80 for (all, 0..) |pkg, i| {
81 if (std.ascii.eqlIgnoreCase(pkg.name, trimmed)) {
82 return i;
83 }
84 }
85 }
86
87 return null;
88}
89
90pub fn exe(environ_map: *const std.process.Environ.Map) []const u8 {
91 return std.zig.EnvVar.PKG_CONFIG.get(environ_map) orelse "pkg-config";
92}
93
94pub const Parsed = struct {
95 cflags: []const []const u8,
96 libs: []const []const u8,
97 unknown_flags: []const []const u8,
98};
99
100pub const ParseError = Allocator.Error || error{InvalidPkgConfigOutput};
101
102/// Parses the output of `pkg-config [name] --cflags --libs`.
103pub fn parse(arena: Allocator, stdout: []const u8) ParseError!Parsed {
104 var zig_cflags: std.ArrayList([]const u8) = .empty;
105 var zig_libs: std.ArrayList([]const u8) = .empty;
106 var unknown_flags: std.ArrayList([]const u8) = .empty;
107 var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t");
108
109 while (arg_it.next()) |arg| {
110 if (mem.eql(u8, arg, "-I")) {
111 const dir = arg_it.next() orelse return error.InvalidPkgConfigOutput;
112 try zig_cflags.appendSlice(arena, &.{ "-I", dir });
113 } else if (mem.startsWith(u8, arg, "-I")) {
114 try zig_cflags.append(arena, arg);
115 } else if (mem.eql(u8, arg, "-L")) {
116 const dir = arg_it.next() orelse return error.InvalidPkgConfigOutput;
117 try zig_libs.appendSlice(arena, &.{ "-L", dir });
118 } else if (mem.startsWith(u8, arg, "-L")) {
119 try zig_libs.append(arena, arg);
120 } else if (mem.eql(u8, arg, "-l")) {
121 const lib = arg_it.next() orelse return error.InvalidPkgConfigOutput;
122 try zig_libs.appendSlice(arena, &.{ "-l", lib });
123 } else if (mem.startsWith(u8, arg, "-l")) {
124 try zig_libs.append(arena, arg);
125 } else if (mem.eql(u8, arg, "-D")) {
126 const macro = arg_it.next() orelse return error.InvalidPkgConfigOutput;
127 try zig_cflags.appendSlice(arena, &.{ "-D", macro });
128 } else if (mem.startsWith(u8, arg, "-D")) {
129 try zig_cflags.append(arena, arg);
130 } else if (mem.cutPrefix(u8, arg, "-Wl,-rpath,")) |rest| {
131 try zig_cflags.appendSlice(arena, &.{ "-rpath", rest });
132 } else {
133 try unknown_flags.append(arena, arg);
134 }
135 }
136
137 try zig_cflags.shrinkToLen(arena);
138 try zig_libs.shrinkToLen(arena);
139 try unknown_flags.shrinkToLen(arena);
140
141 return .{
142 .cflags = zig_cflags.toOwnedSliceAssert(),
143 .libs = zig_libs.toOwnedSliceAssert(),
144 .unknown_flags = unknown_flags.toOwnedSliceAssert(),
145 };
146}