From df8aaad05852b18e5a47aa09d5acdfd312583d1c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 21 May 2026 17:29:31 -0700 Subject: [PATCH] maker: extract some pkg-config logic into reusable API --- lib/compiler/Maker.zig | 7 +- lib/compiler/Maker/PkgConfig.zig | 168 +++++++------------------------ lib/std/zig.zig | 1 + lib/std/zig/PkgConfig.zig | 146 +++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 138 deletions(-) create mode 100644 lib/std/zig/PkgConfig.zig diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index f49090c208..1c26b6052a 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -532,11 +532,7 @@ 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, - }, + .pkg_config = .{ .debug = debug_pkg_config }, .error_style = error_style, .multiline_errors = multiline_errors, @@ -1882,7 +1878,6 @@ pub fn truncatePath( if (graph.verbose) try graph.handleVerbose(.inherit, null, &.{ "truncate", try dest_path.toString(arena), }); - // https://codeberg.org/ziglang/zig/issues/35353 const err = e: { var file = f: { break :f dest_path.root_dir.handle.createFile(io, dest_path.sub_path, .{}) catch |err| switch (err) { diff --git a/lib/compiler/Maker/PkgConfig.zig b/lib/compiler/Maker/PkgConfig.zig index cafcf7c492..09fcbacfbd 100644 --- a/lib/compiler/Maker/PkgConfig.zig +++ b/lib/compiler/Maker/PkgConfig.zig @@ -7,13 +7,8 @@ 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, +pkgs: ?std.zig.PkgConfig = null, debug: bool = false, pub const RunError = error{ @@ -21,10 +16,7 @@ pub const RunError = error{ PkgConfigUnavailable, } || Step.ExtendedMakeError; -pub const Result = struct { - cflags: []const []const u8, - libs: []const []const u8, -}; +pub const Result = std.zig.PkgConfig.Parsed; /// 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. @@ -34,131 +26,50 @@ pub fn run( 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, + /// error.PackageNotFound or error.PkgConfigUnavailable, 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 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, - }); - + const pkg_config_exe = getExe(graph); + const pkgs = try getPkgs(maker, step, progress_node, force); + const found_index = pkgs.find(lib_name) orelse { + if (force) return step.fail(maker, "{s}: package not found: {s}", .{ pkg_config_exe, lib_name }); return error.PackageNotFound; }; + const pkg = pkgs.all[found_index]; - const pkg_config_exe = getExe(graph); const stdout = try captureChildProcess(maker, step, .{ - .argv = &.{ pkg_config_exe, pkg_name, "--cflags", "--libs" }, + .argv = &.{ pkg_config_exe, pkg.name, "--cflags", "--libs" }, .progress_node = progress_node, .allow_failure = !force, }); - var zig_cflags: std.ArrayList([]const u8) = .empty; - var zig_libs: std.ArrayList([]const u8) = .empty; - 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 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.cutPrefix(u8, arg, "-Wl,-rpath,")) |rest| { - try zig_cflags.appendSlice(arena, &.{ "-rpath", rest }); - } else if (force or pc.debug) { - return step.fail(maker, "{s} package {s} unknown flag: {s}", .{ pkg_config_exe, lib_name, arg }); + const parsed = std.zig.PkgConfig.parse(arena, stdout) catch |err| switch (err) { + error.InvalidPkgConfigOutput => { + if (force) return step.fail(maker, "{s} package {s} invalid output: {s}", .{ + pkg_config_exe, lib_name, stdout, + }); + return error.PkgConfigUnavailable; + }, + else => |e| return e, + }; + if (force or pc.debug) { + for (parsed.unknown_flags) |unknown_flag| { + return step.fail(maker, "{s} package {s} unknown flag: {s}", .{ pkg_config_exe, lib_name, unknown_flag }); } } - 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; + return parsed; } fn getExe(graph: *const Graph) []const u8 { - return std.zig.EnvVar.PKG_CONFIG.get(&graph.environ_map) orelse "pkg-config"; + return std.zig.PkgConfig.exe(&graph.environ_map); } -fn getList(maker: *Maker, step: *Step, progress_node: std.Progress.Node, force: bool) RunError![]const Pkg { +fn getPkgs(maker: *Maker, step: *Step, progress_node: std.Progress.Node, force: bool) RunError!std.zig.PkgConfig { const graph = maker.graph; const arena = graph.arena; // TODO don't leak into process arena const io = graph.io; @@ -167,7 +78,7 @@ fn getList(maker: *Maker, step: *Step, progress_node: std.Progress.Node, force: try pc.mutex.lock(io); defer pc.mutex.unlock(io); - if (pc.list) |list| return list; + if (pc.pkgs) |pkgs| return pkgs; const pkg_config_exe = getExe(graph); const stdout = try captureChildProcess(maker, step, .{ @@ -176,25 +87,18 @@ fn getList(maker: *Maker, step: *Step, progress_node: std.Progress.Node, force: .allow_failure = !force, }); - var list: std.ArrayList(Pkg) = .empty; - 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(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); + var diagnostic: std.zig.PkgConfig.Diagnostic = undefined; + const result = std.zig.PkgConfig.init(arena, stdout, &diagnostic) catch |err| switch (err) { + error.InvalidPkgConfigOutput => { + if (force) return step.fail(maker, "{s}: invalid line({d}): {s}", .{ + pkg_config_exe, diagnostic.invalid_line_index + 1, diagnostic.invalid_line, + }); + return error.PkgConfigUnavailable; + }, + else => |e| return e, + }; - const result = list.toOwnedSliceAssert(); - pc.list = result; + pc.pkgs = result; return result; } diff --git a/lib/std/zig.zig b/lib/std/zig.zig index faa816c575..2a3b4792a8 100644 --- a/lib/std/zig.zig +++ b/lib/std/zig.zig @@ -33,6 +33,7 @@ pub const AstRlAnnotate = @import("zig/AstRlAnnotate.zig"); pub const LibCInstallation = @import("zig/LibCInstallation.zig"); pub const WindowsSdk = @import("zig/WindowsSdk.zig"); pub const LibCDirs = @import("zig/LibCDirs.zig"); +pub const PkgConfig = @import("zig/PkgConfig.zig"); pub const target = @import("zig/target.zig"); pub const llvm = @import("zig/llvm.zig"); diff --git a/lib/std/zig/PkgConfig.zig b/lib/std/zig/PkgConfig.zig new file mode 100644 index 0000000000..c37b147643 --- /dev/null +++ b/lib/std/zig/PkgConfig.zig @@ -0,0 +1,146 @@ +//! The more reusable pieces of the build system's pkg-config integration logic. +const PkgConfig = @This(); + +const std = @import("../std.zig"); +const mem = std.mem; +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +all: []const Pkg, + +pub const Pkg = struct { + name: []const u8, + desc: []const u8, +}; + +pub const InitError = Allocator.Error || error{InvalidPkgConfigOutput}; + +pub const Diagnostic = struct { + invalid_line_index: usize, + invalid_line: []const u8, +}; + +/// Parses the output of `pkg-config --list-all`. +pub fn init(arena: Allocator, stdout: []const u8, diagnostic: ?*Diagnostic) InitError!PkgConfig { + var list: std.ArrayList(Pkg) = .empty; + var line_it = mem.tokenizeAny(u8, stdout, "\r\n"); + var line_index: usize = 0; + while (line_it.next()) |line| : (line_index += 1) { + 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 (diagnostic) |d| d.* = .{ + .invalid_line_index = line_index, + .invalid_line = line, + }; + return error.InvalidPkgConfigOutput; + }, + .desc = tok_it.rest(), + }); + } + try list.shrinkToLen(arena); + return .{ .all = list.toOwnedSliceAssert() }; +} + +// Maps 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 +pub fn find(pc: *const PkgConfig, lib_name: []const u8) ?usize { + const all = pc.all; + + // Exact match means instant winner. + for (all, 0..) |pkg, i| { + if (mem.eql(u8, pkg.name, lib_name)) + return i; + } + + // Next we'll try ignoring case. + for (all, 0..) |pkg, i| { + if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) + return i; + } + + // Prefixed "lib" or suffixed ".0". + for (all, 0..) |pkg, i| { + 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; + return i; + } + } + + // Trimming "-1.0". + if (mem.cutSuffix(u8, lib_name, "-1.0")) |trimmed| { + for (all, 0..) |pkg, i| { + if (std.ascii.eqlIgnoreCase(pkg.name, trimmed)) { + return i; + } + } + } + + return null; +} + +pub fn exe(environ_map: *const std.process.Environ.Map) []const u8 { + return std.zig.EnvVar.PKG_CONFIG.get(environ_map) orelse "pkg-config"; +} + +pub const Parsed = struct { + cflags: []const []const u8, + libs: []const []const u8, + unknown_flags: []const []const u8, +}; + +pub const ParseError = Allocator.Error || error{InvalidPkgConfigOutput}; + +/// Parses the output of `pkg-config [name] --cflags --libs`. +pub fn parse(arena: Allocator, stdout: []const u8) ParseError!Parsed { + var zig_cflags: std.ArrayList([]const u8) = .empty; + var zig_libs: std.ArrayList([]const u8) = .empty; + var unknown_flags: std.ArrayList([]const u8) = .empty; + 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.InvalidPkgConfigOutput; + 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.InvalidPkgConfigOutput; + 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.InvalidPkgConfigOutput; + 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.InvalidPkgConfigOutput; + try zig_cflags.appendSlice(arena, &.{ "-D", macro }); + } else if (mem.startsWith(u8, arg, "-D")) { + try zig_cflags.append(arena, arg); + } else if (mem.cutPrefix(u8, arg, "-Wl,-rpath,")) |rest| { + try zig_cflags.appendSlice(arena, &.{ "-rpath", rest }); + } else { + try unknown_flags.append(arena, arg); + } + } + + try zig_cflags.shrinkToLen(arena); + try zig_libs.shrinkToLen(arena); + try unknown_flags.shrinkToLen(arena); + + return .{ + .cflags = zig_cflags.toOwnedSliceAssert(), + .libs = zig_libs.toOwnedSliceAssert(), + .unknown_flags = unknown_flags.toOwnedSliceAssert(), + }; +}