From 29b82d20a4534fc7e3393c21ad637d44d20449a2 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 23 Sep 2019 13:33:43 -0400 Subject: [PATCH] zig build: linkSystemLibrary integrates with pkg-config * add -D CLI option for setting C macros * add std.ascii.allocLowerString * add std.ascii.eqlIgnoreCase * add std.ascii.indexOfIgnoreCasePos * add std.ascii.indexOfIgnoreCase --- src/main.cpp | 7 ++ std/ascii.zig | 60 +++++++++++- std/build.zig | 247 +++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 290 insertions(+), 24 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index cdee60bc5d..68ff97e4db 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -91,6 +91,7 @@ static int print_full_usage(const char *arg0, FILE *file, int return_code) { " --override-std-dir [arg] override path to Zig standard library\n" " --override-lib-dir [arg] override path to Zig lib library\n" " -ffunction-sections places each function in a separate section\n" + " -D[macro]=[value] define C [macro] to [value] (1 if [value] omitted)\n" "\n" "Link Options:\n" " --bundle-compiler-rt for static libraries, include compiler-rt symbols\n" @@ -691,6 +692,9 @@ int main(int argc, char **argv) { bundle_compiler_rt = true; } else if (strcmp(arg, "--test-cmd-bin") == 0) { test_exec_args.append(nullptr); + } else if (arg[1] == 'D' && arg[2] != 0) { + clang_argv.append("-D"); + clang_argv.append(&arg[2]); } else if (arg[1] == 'L' && arg[2] != 0) { // alias for --library-path lib_dirs.append(&arg[2]); @@ -769,6 +773,9 @@ int main(int argc, char **argv) { dynamic_linker = buf_create_from_str(argv[i]); } else if (strcmp(arg, "--libc") == 0) { libc_txt = argv[i]; + } else if (strcmp(arg, "-D") == 0) { + clang_argv.append("-D"); + clang_argv.append(argv[i]); } else if (strcmp(arg, "-isystem") == 0) { clang_argv.append("-isystem"); clang_argv.append(argv[i]); diff --git a/std/ascii.zig b/std/ascii.zig index 93959f8606..2bc11ba3f2 100644 --- a/std/ascii.zig +++ b/std/ascii.zig @@ -7,6 +7,8 @@ // // https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/USASCII_code_chart.png/1200px-USASCII_code_chart.png +const std = @import("std"); + const tIndex = enum(u3) { Alpha, Hex, @@ -25,7 +27,6 @@ const tIndex = enum(u3) { const combinedTable = init: { comptime var table: [256]u8 = undefined; - const std = @import("std"); const mem = std.mem; const alpha = [_]u1{ @@ -215,7 +216,6 @@ pub fn toLower(c: u8) u8 { } test "ascii character classes" { - const std = @import("std"); const testing = std.testing; testing.expect('C' == toUpper('c')); @@ -226,3 +226,59 @@ test "ascii character classes" { testing.expect(!isAlpha('5')); testing.expect(isSpace(' ')); } + +pub fn allocLowerString(allocator: *std.mem.Allocator, ascii_string: []const u8) ![]u8 { + const result = try allocator.alloc(u8, ascii_string.len); + for (result) |*c, i| { + c.* = toLower(ascii_string[i]); + } + return result; +} + +test "allocLowerString" { + var buf: [100]u8 = undefined; + const allocator = &std.heap.FixedBufferAllocator.init(&buf).allocator; + const result = try allocLowerString(allocator, "aBcDeFgHiJkLmNOPqrst0234+💩!"); + std.testing.expect(std.mem.eql(u8, "abcdefghijklmnopqrst0234+💩!", result)); +} + +pub fn eqlIgnoreCase(a: []const u8, b: []const u8) bool { + if (a.len != b.len) return false; + for (a) |a_c, i| { + if (toLower(a_c) != toLower(b[i])) return false; + } + return true; +} + +test "eqlIgnoreCase" { + std.testing.expect(eqlIgnoreCase("HEl💩Lo!", "hel💩lo!")); + std.testing.expect(!eqlIgnoreCase("hElLo!", "hello! ")); + std.testing.expect(!eqlIgnoreCase("hElLo!", "helro!")); +} + +/// Finds `substr` in `container`, starting at `start_index`. +/// TODO boyer-moore algorithm +pub fn indexOfIgnoreCasePos(container: []const u8, start_index: usize, substr: []const u8) ?usize { + if (substr.len > container.len) return null; + + var i: usize = start_index; + const end = container.len - substr.len; + while (i <= end) : (i += 1) { + if (eqlIgnoreCase(container[i .. i + substr.len], substr)) return i; + } + return null; +} + +/// Finds `substr` in `container`, starting at `start_index`. +pub fn indexOfIgnoreCase(container: []const u8, substr: []const u8) ?usize { + return indexOfIgnoreCasePos(container, 0, substr); +} + +test "indexOfIgnoreCase" { + std.testing.expect(indexOfIgnoreCase("one Two Three Four", "foUr").? == 14); + std.testing.expect(indexOfIgnoreCase("one two three FouR", "gOur") == null); + std.testing.expect(indexOfIgnoreCase("foO", "Foo").? == 0); + std.testing.expect(indexOfIgnoreCase("foo", "fool") == null); + + std.testing.expect(indexOfIgnoreCase("FOO foo", "fOo").? == 0); +} diff --git a/std/build.zig b/std/build.zig index 915b255857..7f9a77351d 100644 --- a/std/build.zig +++ b/std/build.zig @@ -55,6 +55,20 @@ pub const Builder = struct { override_std_dir: ?[]const u8, override_lib_dir: ?[]const u8, + pkg_config_pkg_list: ?(PkgConfigError![]const PkgConfigPkg) = null, + + const PkgConfigError = error{ + PkgConfigCrashed, + PkgConfigFailed, + PkgConfigNotInstalled, + PkgConfigInvalidOutput, + }; + + pub const PkgConfigPkg = struct { + name: []const u8, + desc: []const u8, + }; + pub const CStd = enum { C89, C99, @@ -833,20 +847,21 @@ pub const Builder = struct { return error.FileNotFound; } - pub fn exec(self: *Builder, argv: []const []const u8) ![]u8 { + pub fn execAllowFail( + self: *Builder, + argv: []const []const u8, + out_code: *u8, + stderr_behavior: std.ChildProcess.StdIo, + ) ![]u8 { assert(argv.len != 0); - if (self.verbose) { - printCmd(null, argv); - } - const max_output_size = 100 * 1024; const child = try std.ChildProcess.init(argv, self.allocator); defer child.deinit(); child.stdin_behavior = .Ignore; child.stdout_behavior = .Pipe; - child.stderr_behavior = .Inherit; + child.stderr_behavior = stderr_behavior; try child.spawn(); @@ -856,24 +871,48 @@ pub const Builder = struct { var stdout_file_in_stream = child.stdout.?.inStream(); try stdout_file_in_stream.stream.readAllBuffer(&stdout, max_output_size); - const term = child.wait() catch |err| panic("unable to spawn {}: {}", argv[0], err); + const term = try child.wait(); switch (term) { .Exited => |code| { if (code != 0) { - warn("The following command exited with error code {}:\n", code); - printCmd(null, argv); - std.os.exit(@truncate(u8, code)); + out_code.* = @truncate(u8, code); + return error.ExitCodeFailure; } return stdout.toOwnedSlice(); }, .Signal, .Stopped, .Unknown => |code| { + out_code.* = @truncate(u8, code); + return error.ProcessTerminated; + }, + } + } + + pub fn exec(self: *Builder, argv: []const []const u8) ![]u8 { + assert(argv.len != 0); + + if (self.verbose) { + printCmd(null, argv); + } + + var code: u8 = undefined; + return self.execAllowFail(argv, &code, .Inherit) catch |err| switch (err) { + error.FileNotFound => { + warn("Unable to spawn the following command: file not found\n"); + printCmd(null, argv); + std.os.exit(@truncate(u8, code)); + }, + error.ExitCodeFailure => { + warn("The following command exited with error code {}:\n", code); + printCmd(null, argv); + std.os.exit(@truncate(u8, code)); + }, + error.ProcessTerminated => { warn("The following command terminated unexpectedly:\n"); printCmd(null, argv); std.os.exit(@truncate(u8, code)); }, - } - - return stdout.toOwnedSlice(); + else => |e| return e, + }; } pub fn addSearchPrefix(self: *Builder, search_prefix: []const u8) void { @@ -891,13 +930,45 @@ pub const Builder = struct { [_][]const u8{ base_dir, dest_rel_path }, ) catch unreachable; } + + fn execPkgConfigList(self: *Builder, out_code: *u8) ![]const PkgConfigPkg { + const stdout = try self.execAllowFail([_][]const u8{ "pkg-config", "--list-all" }, out_code, .Ignore); + var list = ArrayList(PkgConfigPkg).init(self.allocator); + var line_it = mem.tokenize(stdout, "\r\n"); + while (line_it.next()) |line| { + if (mem.trim(u8, line, " \t").len == 0) continue; + var tok_it = mem.tokenize(line, " \t"); + try list.append(PkgConfigPkg{ + .name = tok_it.next() orelse return error.PkgConfigInvalidOutput, + .desc = tok_it.rest(), + }); + } + return list.toSliceConst(); + } + + fn getPkgConfigList(self: *Builder) ![]const PkgConfigPkg { + if (self.pkg_config_pkg_list) |res| { + return res; + } + var code: u8 = undefined; + if (self.execPkgConfigList(&code)) |list| { + self.pkg_config_pkg_list = list; + return list; + } else |err| { + const result = switch (err) { + error.ProcessTerminated => error.PkgConfigCrashed, + error.ExitCodeFailure => error.PkgConfigFailed, + error.FileNotFound => error.PkgConfigNotInstalled, + error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput, + else => return err, + }; + self.pkg_config_pkg_list = result; + return result; + } + } }; test "builder.findProgram compiles" { - //allocator: *Allocator, - //zig_exe: []const u8, - //build_root: []const u8, - //cache_root: []const u8, const builder = try Builder.create(std.heap.direct_allocator, "zig", "zig-cache", "zig-cache"); _ = builder.findProgram([_][]const u8{}, [_][]const u8{}) catch null; } @@ -1388,6 +1459,7 @@ pub const LibExeObjStep = struct { link_objects: ArrayList(LinkObject), include_dirs: ArrayList(IncludeDir), + c_macros: ArrayList([]const u8), output_dir: ?[]const u8, need_system_paths: bool, is_linking_libc: bool = false, @@ -1491,6 +1563,7 @@ pub const LibExeObjStep = struct { .packages = ArrayList(Pkg).init(builder.allocator), .include_dirs = ArrayList(IncludeDir).init(builder.allocator), .link_objects = ArrayList(LinkObject).init(builder.allocator), + .c_macros = ArrayList([]const u8).init(builder.allocator), .lib_paths = ArrayList([]const u8).init(builder.allocator), .framework_dirs = ArrayList([]const u8).init(builder.allocator), .object_src = undefined, @@ -1617,6 +1690,9 @@ pub const LibExeObjStep = struct { /// Returns whether the library, executable, or object depends on a particular system library. pub fn dependsOnSystemLibrary(self: LibExeObjStep, name: []const u8) bool { + if (isLibCLibrary(name)) { + return self.is_linking_libc; + } for (self.link_objects.toSliceConst()) |link_object| { switch (link_object) { LinkObject.SystemLib => |n| if (mem.eql(u8, n, name)) return true, @@ -1641,15 +1717,137 @@ pub const LibExeObjStep = struct { return self.isDynamicLibrary() or self.kind == .Exe; } - pub fn linkSystemLibrary(self: *LibExeObjStep, name: []const u8) void { - self.link_objects.append(LinkObject{ .SystemLib = self.builder.dupe(name) }) catch unreachable; - if (isLibCLibrary(name)) { + pub fn linkLibC(self: *LibExeObjStep) void { + if (!self.is_linking_libc) { self.is_linking_libc = true; - } else { - self.need_system_paths = true; + self.link_objects.append(LinkObject{ .SystemLib = "c" }) catch unreachable; } } + /// name_and_value looks like [name]=[value]. If the value is omitted, it is set to 1. + pub fn defineCMacro(self: *LibExeObjStep, name_and_value: []const u8) void { + self.c_macros.append(self.builder.dupe(name_and_value)) catch unreachable; + } + + /// This one has no integration with anything, it just puts -lname on the command line. + /// Prefer to use `linkSystemLibrary` instead. + pub fn linkSystemLibraryName(self: *LibExeObjStep, name: []const u8) void { + self.link_objects.append(LinkObject{ .SystemLib = self.builder.dupe(name) }) catch unreachable; + self.need_system_paths = true; + } + + /// This links against a system library, exclusively using pkg-config to find the library. + /// Prefer to use `linkSystemLibrary` instead. + pub fn linkSystemLibraryPkgConfigOnly(self: *LibExeObjStep, lib_name: []const u8) !void { + 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 + const pkgs = try self.builder.getPkgConfigList(); + + // 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; + } + } + + // Now try appending ".0". + for (pkgs) |pkg| { + if (std.ascii.indexOfIgnoreCase(pkg.name, lib_name)) |pos| { + if (pos != 0) continue; + if (mem.eql(u8, pkg.name[lib_name.len..], ".0")) { + 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 stdout = if (self.builder.execAllowFail([_][]const u8{ + "pkg-config", + pkg_name, + "--cflags", + "--libs", + }, &code, .Ignore)) |stdout| stdout else |err| switch (err) { + error.ProcessTerminated => return error.PkgConfigCrashed, + error.ExitCodeFailure => return error.PkgConfigFailed, + error.FileNotFound => return error.PkgConfigNotInstalled, + else => return err, + }; + var it = mem.tokenize(stdout, " \r\n\t"); + while (it.next()) |tok| { + if (mem.eql(u8, tok, "-I")) { + const dir = it.next() orelse return error.PkgConfigInvalidOutput; + self.addIncludeDir(dir); + } else if (mem.startsWith(u8, tok, "-I")) { + self.addIncludeDir(tok["-I".len..]); + } else if (mem.eql(u8, tok, "-L")) { + const dir = it.next() orelse return error.PkgConfigInvalidOutput; + self.addLibPath(dir); + } else if (mem.startsWith(u8, tok, "-L")) { + self.addLibPath(tok["-L".len..]); + } else if (mem.eql(u8, tok, "-l")) { + const lib = it.next() orelse return error.PkgConfigInvalidOutput; + self.linkSystemLibraryName(lib); + } else if (mem.startsWith(u8, tok, "-l")) { + self.linkSystemLibraryName(tok["-l".len..]); + } else if (mem.eql(u8, tok, "-D")) { + const macro = it.next() orelse return error.PkgConfigInvalidOutput; + self.defineCMacro(macro); + } else if (mem.startsWith(u8, tok, "-D")) { + self.defineCMacro(tok["-D".len..]); + } else if (mem.eql(u8, tok, "-pthread")) { + self.linkLibC(); + } else if (self.builder.verbose) { + warn("Ignoring pkg-config flag '{}'\n", tok); + } + } + } + + pub fn linkSystemLibrary(self: *LibExeObjStep, name: []const u8) void { + if (isLibCLibrary(name)) { + self.linkLibC(); + return; + } + if (self.linkSystemLibraryPkgConfigOnly(name)) |_| { + // pkg-config worked, so nothing further needed to do. + return; + } else |err| switch (err) { + error.PkgConfigInvalidOutput, + error.PkgConfigCrashed, + error.PkgConfigFailed, + error.PkgConfigNotInstalled, + error.PackageNotFound, + => {}, + + else => unreachable, + } + + self.linkSystemLibraryName(name); + } + pub fn setNamePrefix(self: *LibExeObjStep, text: []const u8) void { assert(self.kind == Kind.Test); self.name_prefix = text; @@ -2072,6 +2270,11 @@ pub const LibExeObjStep = struct { } } + for (self.c_macros.toSliceConst()) |c_macro| { + try zig_args.append("-D"); + try zig_args.append(c_macro); + } + if (self.target.isDarwin()) { for (self.framework_dirs.toSliceConst()) |dir| { try zig_args.append("-F");