commit fb9118195e4a738aff9556389fcbdb9ebacbd020 (tree)
parent 5644d68f147159d3be14378d6cce06e1fa200dc3
Author: Andrew Kelley <andrew@ziglang.org>
Date: Sun, 17 May 2026 16:42:37 -0700
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`
Diffstat:
4 files changed, 239 insertions(+), 203 deletions(-)
diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig
@@ -23,6 +23,7 @@ const Step = @import("Maker/Step.zig");
const Watch = @import("Maker/Watch.zig");
const WebServer = @import("Maker/WebServer.zig");
const ScannedConfig = @import("Maker/ScannedConfig.zig");
+const PkgConfig = @import("Maker/PkgConfig.zig");
pub const std_options: std.Options = .{
.side_channels_mitigations = .none,
@@ -48,6 +49,7 @@ web_server: if (!builtin.single_threaded) ?WebServer else ?noreturn,
memory_blocked_steps: std.ArrayList(Configuration.Step.Index),
/// Allocated into `gpa`.
step_stack: std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void),
+pkg_config: PkgConfig,
error_style: ErrorStyle,
multiline_errors: MultilineErrors,
@@ -540,6 +542,11 @@ pub fn main(init: process.Init.Minimal) !void {
.web_server = undefined, // set after `prepare`
.memory_blocked_steps = .empty,
.step_stack = .empty,
+ .pkg_config = .{
+ .mutex = .init,
+ .list = null,
+ .debug = debug_pkg_config,
+ },
.error_style = error_style,
.multiline_errors = multiline_errors,
diff --git a/lib/compiler/Maker/PkgConfig.zig b/lib/compiler/Maker/PkgConfig.zig
@@ -0,0 +1,203 @@
+const std = @import("std");
+const Io = std.Io;
+const mem = std.mem;
+
+const Maker = @import("../Maker.zig");
+const Step = @import("Step.zig");
+const Graph = @import("Graph.zig");
+
+pub const Pkg = struct {
+ name: []const u8,
+ desc: []const u8,
+};
+
+mutex: Io.Mutex = .init,
+list: ?[]const Pkg = null,
+debug: bool = false,
+
+pub const RunError = error{
+ PackageNotFound,
+ PkgConfigUnavailable,
+} || Step.ExtendedMakeError;
+
+pub const Result = struct {
+ cflags: []const []const u8,
+ libs: []const []const u8,
+};
+
+/// Run pkg-config for the given library name and parse the output, returning the arguments
+/// that should be passed to zig to link the given library.
+pub fn run(
+ maker: *Maker,
+ step: *Step,
+ progress_node: std.Progress.Node,
+ lib_name: []const u8,
+ /// If true, reports failure error messages on step rather than returning
+ /// error.PackageNotFound or error.PkgConfigInvalidOutput,
+ force: bool,
+) RunError!Result {
+ const pc = &maker.pkg_config;
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into process arena
+ const wl_rpath_prefix = "-Wl,-rpath,";
+
+ const pkg_name = match: {
+ // First we have to map the library name to pkg config name. Unfortunately,
+ // there are several examples where this is not straightforward:
+ // -lSDL2 -> pkg-config sdl2
+ // -lgdk-3 -> pkg-config gdk-3.0
+ // -latk-1.0 -> pkg-config atk
+ // -lpulse -> pkg-config libpulse
+ const pkgs = try getList(maker, step, progress_node, force);
+
+ // Exact match means instant winner.
+ for (pkgs) |pkg| {
+ if (mem.eql(u8, pkg.name, lib_name)) {
+ break :match pkg.name;
+ }
+ }
+
+ // Next we'll try ignoring case.
+ for (pkgs) |pkg| {
+ if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) {
+ break :match pkg.name;
+ }
+ }
+
+ // Prefixed "lib" or suffixed ".0".
+ for (pkgs) |pkg| {
+ if (std.ascii.findIgnoreCase(pkg.name, lib_name)) |pos| {
+ const prefix = pkg.name[0..pos];
+ const suffix = pkg.name[pos + lib_name.len ..];
+ if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue;
+ if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue;
+ break :match pkg.name;
+ }
+ }
+
+ // Trimming "-1.0".
+ if (mem.endsWith(u8, lib_name, "-1.0")) {
+ const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len];
+ for (pkgs) |pkg| {
+ if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) {
+ break :match pkg.name;
+ }
+ }
+ }
+
+ if (force) return step.fail(maker, "{s}: package not found: {s}", .{
+ getExe(graph), lib_name,
+ });
+
+ return error.PackageNotFound;
+ };
+
+ const pkg_config_exe = getExe(graph);
+ const captured = try step.captureChildProcess(maker, progress_node, &.{
+ pkg_config_exe, pkg_name, "--cflags", "--libs",
+ });
+ try step.handleChildProcessTerm(maker, captured.term);
+
+ var zig_cflags: std.ArrayList([]const u8) = .empty;
+ var zig_libs: std.ArrayList([]const u8) = .empty;
+ var arg_it = mem.tokenizeAny(u8, captured.stdout, " \r\n\t");
+
+ while (arg_it.next()) |arg| {
+ if (mem.eql(u8, arg, "-I")) {
+ const dir = arg_it.next() orelse return missingArg(maker, step, pkg_config_exe, lib_name, arg, force);
+ try zig_cflags.appendSlice(arena, &.{ "-I", dir });
+ } else if (mem.startsWith(u8, arg, "-I")) {
+ try zig_cflags.append(arena, arg);
+ } else if (mem.eql(u8, arg, "-L")) {
+ const dir = arg_it.next() orelse return missingArg(maker, step, pkg_config_exe, lib_name, arg, force);
+ try zig_libs.appendSlice(arena, &.{ "-L", dir });
+ } else if (mem.startsWith(u8, arg, "-L")) {
+ try zig_libs.append(arena, arg);
+ } else if (mem.eql(u8, arg, "-l")) {
+ const lib = arg_it.next() orelse return missingArg(maker, step, pkg_config_exe, lib_name, arg, force);
+ try zig_libs.appendSlice(arena, &.{ "-l", lib });
+ } else if (mem.startsWith(u8, arg, "-l")) {
+ try zig_libs.append(arena, arg);
+ } else if (mem.eql(u8, arg, "-D")) {
+ const macro = arg_it.next() orelse return missingArg(maker, step, pkg_config_exe, lib_name, arg, force);
+ try zig_cflags.appendSlice(arena, &.{ "-D", macro });
+ } else if (mem.startsWith(u8, arg, "-D")) {
+ try zig_cflags.append(arena, arg);
+ } else if (mem.startsWith(u8, arg, wl_rpath_prefix)) {
+ try zig_cflags.appendSlice(arena, &.{ "-rpath", arg[wl_rpath_prefix.len..] });
+ } else if (force or pc.debug) {
+ return step.fail(maker, "{s} package {s} unknown flag: {s}", .{ pkg_config_exe, lib_name, arg });
+ }
+ }
+
+ try zig_cflags.shrinkToLen(arena);
+ try zig_libs.shrinkToLen(arena);
+
+ return .{
+ .cflags = zig_cflags.toOwnedSliceAssert(),
+ .libs = zig_libs.toOwnedSliceAssert(),
+ };
+}
+
+fn missingArg(
+ maker: *Maker,
+ step: *Step,
+ pkg_config_exe: []const u8,
+ lib_name: []const u8,
+ arg: []const u8,
+ force: bool,
+) RunError {
+ if (force) return step.fail(maker, "{s} package {s} missing arg after flag: {s}", .{
+ pkg_config_exe, lib_name, arg,
+ });
+ return error.PkgConfigUnavailable;
+}
+
+fn getExe(graph: *const Graph) []const u8 {
+ return std.zig.EnvVar.PKG_CONFIG.get(&graph.environ_map) orelse "pkg-config";
+}
+
+fn getList(maker: *Maker, step: *Step, progress_node: std.Progress.Node, force: bool) RunError![]const Pkg {
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into process arena
+ const io = graph.io;
+ const pc = &maker.pkg_config;
+
+ try pc.mutex.lock(io);
+ defer pc.mutex.unlock(io);
+
+ if (pc.list) |list| return list;
+
+ const pkg_config_exe = getExe(graph);
+ const captured = try step.captureChildProcess(maker, progress_node, &.{ pkg_config_exe, "--list-all" });
+ if (force) {
+ try step.handleChildProcessTerm(maker, captured.term);
+ } else switch (captured.term) {
+ .exited => |code| if (code != 0) return error.PkgConfigUnavailable,
+ else => {
+ try step.handleChildProcessTerm(maker, captured.term);
+ unreachable;
+ },
+ }
+
+ var list: std.ArrayList(Pkg) = .empty;
+ var line_it = mem.tokenizeAny(u8, captured.stdout, "\r\n");
+ while (line_it.next()) |line| {
+ if (mem.trim(u8, line, " \t").len == 0) continue;
+ var tok_it = mem.tokenizeAny(u8, line, " \t");
+ try list.append(arena, .{
+ .name = tok_it.next() orelse {
+ if (force) return step.fail(maker, "{s}: invalid line: {s}", .{
+ pkg_config_exe, line,
+ });
+ return error.PkgConfigUnavailable;
+ },
+ .desc = tok_it.rest(),
+ });
+ }
+ try list.shrinkToLen(arena);
+
+ const result = list.toOwnedSliceAssert();
+ pc.list = result;
+ return result;
+}
diff --git a/lib/compiler/Maker/Step/Compile.zig b/lib/compiler/Maker/Step/Compile.zig
@@ -14,6 +14,7 @@ const allocPrint = std.fmt.allocPrint;
const Step = @import("../Step.zig");
const Maker = @import("../../Maker.zig");
+const PkgConfig = @import("../PkgConfig.zig");
/// Populated when there is compiler process that lives across multiple calls
/// to `make`.
@@ -40,7 +41,7 @@ pub fn make(
// Reset / repopulate persistent state.
compile.zig_args.clearRetainingCapacity();
- try lowerZigArgs(compile, compile_index, maker, &compile.zig_args, false);
+ try lowerZigArgs(compile, compile_index, maker, progress_node, &compile.zig_args, false);
const maybe_output_dir = Step.evalZigProcess(
compile_index,
@@ -148,10 +149,11 @@ const ModuleListContext = struct {
fn lowerZigArgs(
compile: *Compile,
compile_index: Configuration.Step.Index,
- maker: *const Maker,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
zig_args: *std.ArrayList([]const u8),
fuzz: bool,
-) error{ OutOfMemory, MakeFailed }!void {
+) Step.ExtendedMakeError!void {
const step = maker.stepByIndex(compile_index);
const graph = maker.graph;
const arena = graph.arena; // TODO don't leak into the process arena
@@ -312,40 +314,36 @@ fn lowerZigArgs(
if (system_lib.flags.weak) break :prefix "-weak-l";
break :prefix "-l";
};
- switch (system_lib.flags.use_pkg_config) {
- .no => try zig_args.append(gpa, try allocPrint(arena, "{s}{s}", .{
- prefix, system_lib_name,
- })),
- .yes, .force => {
- if (compile.runPkgConfig(maker, system_lib_name)) |result| {
+ l: {
+ pc: {
+ const force = switch (system_lib.flags.use_pkg_config) {
+ .no => break :pc,
+ .yes => false,
+ .force => true,
+ };
+
+ const pkg_conf_node = progress_node.start("pkg-config", 0);
+ defer pkg_conf_node.end();
+
+ if (PkgConfig.run(maker, step, pkg_conf_node, system_lib_name, force)) |result| {
try zig_args.appendSlice(gpa, result.cflags);
try zig_args.appendSlice(gpa, result.libs);
try seen_system_libs.put(arena, system_lib.name, result.cflags);
+ break :l;
} else |err| switch (err) {
- error.PkgConfigInvalidOutput,
- error.PkgConfigCrashed,
- error.PkgConfigFailed,
- error.PkgConfigNotInstalled,
+ error.PkgConfigUnavailable,
error.PackageNotFound,
- => switch (system_lib.flags.use_pkg_config) {
- .yes => {
- // pkg-config failed, so fall back to linking the library
- // by name directly.
- try zig_args.append(gpa, try allocPrint(arena, "{s}{s}", .{
- prefix, system_lib_name,
- }));
- },
- .force => {
- return step.fail(maker, "pkg-config failed for library {s}", .{
- system_lib_name,
- });
- },
- .no => unreachable,
+ => {
+ // pkg-config failed, so fall back to linking the library by name directly.
+ assert(!force);
+ break :pc;
},
-
else => |e| return e,
}
- },
+ }
+ try zig_args.append(gpa, try allocPrint(arena, "{s}{s}", .{
+ prefix, system_lib_name,
+ }));
}
},
.other_step => |other_step_index| {
@@ -961,65 +959,11 @@ pub fn rebuildInFuzzMode(compile: *Compile, maker: *Maker, progress_node: std.Pr
const zig_args = &compile.zig_args;
zig_args.clearRetainingCapacity();
- try lowerZigArgs(compile, maker, zig_args, true);
+ try lowerZigArgs(compile, maker, progress_node, zig_args, true);
const maybe_output_bin_path = try compile.step.evalZigProcess(zig_args.items, progress_node, false, maker);
return maybe_output_bin_path.?;
}
-pub const PkgConfigError = error{
- PkgConfigCrashed,
- PkgConfigFailed,
- PkgConfigNotInstalled,
- PkgConfigInvalidOutput,
-};
-
-pub const PkgConfigPkg = struct {
- name: []const u8,
- desc: []const u8,
-};
-
-fn execPkgConfigList(maker: *Maker, out_code: *u8) (PkgConfigError || Maker.RunError)![]const PkgConfigPkg {
- const graph = maker.graph;
- const process_arena = graph.arena; // TODO don't leak into process arena
- const pkg_config_exe = graph.environ_map.get("PKG_CONFIG") orelse "pkg-config";
- const stdout = try maker.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .ignore);
- var list = std.array_list.Managed(PkgConfigPkg).init(process_arena);
- errdefer list.deinit();
- var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
- while (line_it.next()) |line| {
- if (mem.trim(u8, line, " \t").len == 0) continue;
- var tok_it = mem.tokenizeAny(u8, line, " \t");
- try list.append(PkgConfigPkg{
- .name = tok_it.next() orelse return error.PkgConfigInvalidOutput,
- .desc = tok_it.rest(),
- });
- }
- return list.toOwnedSlice();
-}
-
-fn getPkgConfigList(b: *std.Build) ![]const PkgConfigPkg {
- if (b.pkg_config_pkg_list) |res| {
- return res;
- }
- var code: u8 = undefined;
- if (execPkgConfigList(b, &code)) |list| {
- b.pkg_config_pkg_list = list;
- return list;
- } else |err| {
- const result = switch (err) {
- error.ProcessTerminated => error.PkgConfigCrashed,
- error.ExecNotSupported => error.PkgConfigFailed,
- error.ExitCodeFailure => error.PkgConfigFailed,
- error.FileNotFound => error.PkgConfigNotInstalled,
- error.InvalidName => error.PkgConfigNotInstalled,
- error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput,
- else => return err,
- };
- b.pkg_config_pkg_list = result;
- return result;
- }
-}
-
fn addBool(gpa: Allocator, args: *std.ArrayList([]const u8), arg: []const u8, opt: bool) !void {
if (opt) try args.append(gpa, arg);
}
@@ -1029,125 +973,6 @@ fn addFlag(gpa: Allocator, args: *std.ArrayList([]const u8), comptime name: []co
try args.append(gpa, if (cond) "-f" ++ name else "-fno-" ++ name);
}
-const PkgConfigResult = struct {
- cflags: []const []const u8,
- libs: []const []const u8,
-};
-
-/// Run pkg-config for the given library name and parse the output, returning the arguments
-/// that should be passed to zig to link the given library.
-fn runPkgConfig(compile: *const Compile, maker: *const Maker, lib_name: []const u8) !PkgConfigResult {
- if (true) @panic("TODO runPkgConfig");
- const graph = maker.graph;
- const wl_rpath_prefix = "-Wl,-rpath,";
-
- const b = compile.step.owner;
- const arena = b.allocator;
- const pkg_name = match: {
- // First we have to map the library name to pkg config name. Unfortunately,
- // there are several examples where this is not straightforward:
- // -lSDL2 -> pkg-config sdl2
- // -lgdk-3 -> pkg-config gdk-3.0
- // -latk-1.0 -> pkg-config atk
- // -lpulse -> pkg-config libpulse
- const pkgs = try getPkgConfigList(b);
-
- // Exact match means instant winner.
- for (pkgs) |pkg| {
- if (mem.eql(u8, pkg.name, lib_name)) {
- break :match pkg.name;
- }
- }
-
- // Next we'll try ignoring case.
- for (pkgs) |pkg| {
- if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) {
- break :match pkg.name;
- }
- }
-
- // Prefixed "lib" or suffixed ".0".
- for (pkgs) |pkg| {
- if (std.ascii.findIgnoreCase(pkg.name, lib_name)) |pos| {
- const prefix = pkg.name[0..pos];
- const suffix = pkg.name[pos + lib_name.len ..];
- if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue;
- if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue;
- break :match pkg.name;
- }
- }
-
- // Trimming "-1.0".
- if (mem.endsWith(u8, lib_name, "-1.0")) {
- const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len];
- for (pkgs) |pkg| {
- if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) {
- break :match pkg.name;
- }
- }
- }
-
- return error.PackageNotFound;
- };
-
- var code: u8 = undefined;
- const pkg_config_exe = graph.environ_map.get("PKG_CONFIG") orelse "pkg-config";
- const stdout = if (b.runAllowFail(&[_][]const u8{
- pkg_config_exe,
- pkg_name,
- "--cflags",
- "--libs",
- }, &code, .ignore)) |stdout| stdout else |err| switch (err) {
- error.ProcessTerminated => return error.PkgConfigCrashed,
- error.ExecNotSupported => return error.PkgConfigFailed,
- error.ExitCodeFailure => return error.PkgConfigFailed,
- error.FileNotFound => return error.PkgConfigNotInstalled,
- else => return err,
- };
-
- var zig_cflags: std.ArrayList([]const u8) = .empty;
- defer zig_cflags.deinit(arena);
- var zig_libs: std.ArrayList([]const u8) = .empty;
- defer zig_libs.deinit(arena);
-
- var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t");
- while (arg_it.next()) |arg| {
- if (mem.eql(u8, arg, "-I")) {
- const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
- try zig_cflags.appendSlice(arena, &.{ "-I", dir });
- } else if (mem.startsWith(u8, arg, "-I")) {
- try zig_cflags.append(arena, arg);
- } else if (mem.eql(u8, arg, "-L")) {
- const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
- try zig_libs.appendSlice(arena, &.{ "-L", dir });
- } else if (mem.startsWith(u8, arg, "-L")) {
- try zig_libs.append(arena, arg);
- } else if (mem.eql(u8, arg, "-l")) {
- const lib = arg_it.next() orelse return error.PkgConfigInvalidOutput;
- try zig_libs.appendSlice(arena, &.{ "-l", lib });
- } else if (mem.startsWith(u8, arg, "-l")) {
- try zig_libs.append(arena, arg);
- } else if (mem.eql(u8, arg, "-D")) {
- const macro = arg_it.next() orelse return error.PkgConfigInvalidOutput;
- try zig_cflags.appendSlice(arena, &.{ "-D", macro });
- } else if (mem.startsWith(u8, arg, "-D")) {
- try zig_cflags.append(arena, arg);
- } else if (mem.startsWith(u8, arg, wl_rpath_prefix)) {
- try zig_cflags.appendSlice(arena, &.{ "-rpath", arg[wl_rpath_prefix.len..] });
- } else if (b.debug_pkg_config) {
- return compile.step.fail(maker, "unknown pkg-config flag '{s}'", .{arg});
- }
- }
-
- try zig_cflags.shrinkToLen(arena);
- try zig_libs.shrinkToLen(arena);
-
- return .{
- .cflags = zig_cflags.toOwnedSliceAssert(),
- .libs = zig_libs.toOwnedSliceAssert(),
- };
-}
-
fn checkCompileErrors(compile: *Compile, maker: *Maker) !void {
if (true) @panic("TODO checkCompileErrors");
// Clear this field so that it does not get printed by the build runner.
diff --git a/lib/std/zig.zig b/lib/std/zig.zig
@@ -772,6 +772,7 @@ pub const EnvVar = enum {
CPLUS_INCLUDE_PATH,
LIBRARY_PATH,
CC,
+ PKG_CONFIG,
// Terminal integration
NO_COLOR,