zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

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:
Mlib/compiler/Maker.zig | 7+++++++
Alib/compiler/Maker/PkgConfig.zig | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/compiler/Maker/Step/Compile.zig | 231++++++++++---------------------------------------------------------------------
Mlib/std/zig.zig | 1+
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,