Skip to content

Conversation

@andrewrk
Copy link
Member

@andrewrk andrewrk commented Jun 16, 2023

The main change here is introducing the concept of preferred link mode and search strategy when resolving libraries.

CLI

The four options to control these flags are:

-search_paths_first            For each library search path, check for dynamic
                               lib then static lib before proceeding to next path.
-search_paths_first_static     For each library search path, check for static
                               lib then dynamic lib before proceeding to next path.
-search_dylibs_first           Search for dynamic libs in all library search
                               paths, then static libs.
-search_static_first           Search for static libs in all library search
                               paths, then dynamic libs.
-search_dylibs_only            Only search for dynamic libs.
-search_static_only            Only search for static libs.

-search_paths_first and -search_dylibs_first come from the Darwin toolchain and previously only affected those targets, but now they work the same for all targets. The other flags I added by following the naming convention. These arguments are stateful; they affect all subsequent libraries linked by name, such as the flags -l, -weak-l, and -needed-l.

search_strategy is no longer passed to Compilation at all; instead it is used in the CLI code only.

System libraries are now included in the cache hash, and thus changes to them will correctly cause cache misses.

In the future, lib_dirs should no longer be passed to Compilation at all, because it is a frontend-only concept.

--sysroot also now affects all targets the same instead of only affecting Darwin.

Improved the error reporting for failure to find a system library. An example error now looks like this:

$ zig build-exe test.zig -lfoo -L. -L/a -target x86_64-macos --sysroot /home/andy/local
error: unable to find Dynamic system library 'foo' using strategy 'paths_first'. searched paths:
  ./libfoo.tbd
  ./libfoo.dylib
  ./libfoo.so
  ./libfoo.a
  /home/andy/local/a/libfoo.tbd
  /home/andy/local/a/libfoo.dylib
  /home/andy/local/a/libfoo.so
  /home/andy/local/a/libfoo.a
  /a/libfoo.tbd
  /a/libfoo.dylib
  /a/libfoo.so
  /a/libfoo.a

This is the same default search strategy as when the zig cc frontend is used. However, using the Zig CLI, -search_dylibs_first can now be used for any target:

$ zig build-exe test.zig -search_dylibs_first -lfoo -L. -L/a   --sysroot /home/andy/local
error: unable to find Dynamic system library 'foo' using strategy 'mode_first'. searched paths:
  ./libfoo.so
  /home/andy/local/a/libfoo.so
  /a/libfoo.so
  ./libfoo.a
  /home/andy/local/a/libfoo.a
  /a/libfoo.a

-static now implies -search_static_only:

$ zig build-exe test.zig -static  -lfoo -L. -L/a   --sysroot /home/andy/local
error: unable to find Static system library 'foo' using strategy 'no_fallback'. searched paths:
  ./libfoo.a
  /home/andy/local/a/libfoo.a
  /a/libfoo.a

closes #14963

Build System

Previously the build system exposed -search_paths_first and -search_dylibs_first which had the ability to retroactively affect all libraries. Now, instead, the build.zig script explicitly chooses the search strategy and preferred link mode for each library independently, and the Compile step emits CLI options to communicate this information to the compiler.

macOS

Previously, macOS had a bunch of special casing to make it ignore the native system libraries and header files. This changeset makes macOS do the same thing as Linux, integrating with the host system SDK in the case of a native build.

EDIT:
Fixes #15024
Fixes #16118
Fixes #15963

@andrewrk andrewrk requested review from ifreund and kubkon June 16, 2023 00:24
@andrewrk andrewrk force-pushed the frontend-lib-paths branch from bbcd48b to 889ad14 Compare June 16, 2023 00:34
@andrewrk
Copy link
Member Author

This needs another commit to fix handling of system DLLs on Windows such as kernel32, but it's ready to go for other targets.

Copy link
Member

@kubkon kubkon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

for (search_dirs) |dir| {
if (try resolveLib(arena, dir, "System", ".tbd")) |full_path| {
try out_libs.put(full_path, .{ .needed = true });
try out_libs.put(full_path, .{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps not in this PR (I can follow up with a subsequent PR) but we could move this logic to main.zig too? This way we would have all path handling done in the frontend.

Copy link
Member

@ifreund ifreund left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran into an issue with this right away when testing with one of my projects linking system libraries using std.Build.linkSystemLibrary() which invokes pkg-config to determine the required linker flags. pkg-config assumes the flags will be passed to the system linker and omits flags that would be redundant with the system linker's defaults. For example, pkg-config --libs xkbcommon on my system prints only -lxkbcommon since /usr/lib is a default search path of my system linker.

With this patch we no longer rely on the library searching of the system linker however so libxkbcommon is not found as the code in main.zig is not aware of the /usr/lib system default.

As for how best to fix this change in behavior for my use-case it appears adding --keep-system-libs to the pkg-config invocation in std.Build.Compile is sufficient.

Unfortunately this affects zig cc as well however and users will expect zig cc's linker to continue to have the traditional default search paths. I guess we can probably look at lld's source to determine exactly what these paths are and add the necessary logic to main.zig to match the behavior there.

To recap, this works perfectly for my use-cases with the following patch!

diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig
index 092fdf7e6..51ab882db 100644
--- a/lib/std/Build/Step/Compile.zig
+++ b/lib/std/Build/Step/Compile.zig
@@ -842,6 +842,7 @@ fn runPkgConfig(self: *Compile, lib_name: []const u8) ![]const []const u8 {
         pkg_name,
         "--cflags",
         "--libs",
+        "--keep-system-libs",
     }, &code, .Ignore)) |stdout| stdout else |err| switch (err) {
         error.ProcessTerminated => return error.PkgConfigCrashed,
         error.ExecNotSupported => return error.PkgConfigFailed,

@kubkon
Copy link
Member

kubkon commented Jun 16, 2023

I ran into an issue with this right away when testing with one of my projects linking system libraries using std.Build.linkSystemLibrary() which invokes pkg-config to determine the required linker flags. pkg-config assumes the flags will be passed to the system linker and omits flags that would be redundant with the system linker's defaults. For example, pkg-config --libs xkbcommon on my system prints only -lxkbcommon since /usr/lib is a default search path of my system linker.

With this patch we no longer rely on the library searching of the system linker however so libxkbcommon is not found as the code in main.zig is not aware of the /usr/lib system default.

As for how best to fix this change in behavior for my use-case it appears adding --keep-system-libs to the pkg-config invocation in std.Build.Compile is sufficient.

Unfortunately this affects zig cc as well however and users will expect zig cc's linker to continue to have the traditional default search paths. I guess we can probably look at lld's source to determine exactly what these paths are and add the necessary logic to main.zig to match the behavior there.

To recap, this works perfectly for my use-cases with the following patch!

diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig
index 092fdf7e6..51ab882db 100644
--- a/lib/std/Build/Step/Compile.zig
+++ b/lib/std/Build/Step/Compile.zig
@@ -842,6 +842,7 @@ fn runPkgConfig(self: *Compile, lib_name: []const u8) ![]const []const u8 {
         pkg_name,
         "--cflags",
         "--libs",
+        "--keep-system-libs",
     }, &code, .Ignore)) |stdout| stdout else |err| switch (err) {
         error.ProcessTerminated => return error.PkgConfigCrashed,
         error.ExecNotSupported => return error.PkgConfigFailed,

This looks like an incorrect assumption of the system linker (hello GNU ld). Having said that, zig doesn't use system linker on linux - it uses ld.lld so I am surprised how this issue arises. Anyhow, library search paths, IMHO, should always be passed explicitly to the linker to avoid confusion. There are also a few env vars we could read such as LD_LIBRARY_PATH. If you have a minute, you could check against zld/ELF or mold and see if the same problem happens with and without these changes.

@kubkon
Copy link
Member

kubkon commented Jun 16, 2023

I mentioned zld and mold as neither assumes any default search paths and hence their output differs from that of gold.

@ifreund
Copy link
Member

ifreund commented Jun 16, 2023

@kubkon It looks like I was wrong about where these defaults that pkg-config assumes come from. They are defined in clang and gcc not the linker. (here for clang).

It appears zig 0.10 discovered and passed several library paths to lld that aren't currently being searched for libraries on this branch.

For this 0.10 zig build-exe invocation
/usr/bin/zig
build-exe
/home/ifreund/projects/river/river/main.zig
-lc
-I/usr/include/libevdev-1.0/
-levdev
-linput
-I/home/ifreund/.local/include
-L/home/ifreund/.local/lib
-lwayland-server
-lxkbcommon
-I/usr/include/pixman-1
-lpixman-1
-I/home/ifreund/.local/include
-I/usr/include/libdrm
-I/usr/include/pixman-1
-I/usr/include/elogind
-L/home/ifreund/.local/lib
-lwlroots
-lxcb-render
-lxcb
-cflags
-std=c99
-O2
--
/home/ifreund/projects/river/river/wlroots_log_wrapper.c
-cflags
-std=c99
--
/home/ifreund/projects/river/zig-cache/zig-wayland/river-control-unstable-v1-protocol.c
-cflags
-std=c99
--
/home/ifreund/projects/river/zig-cache/zig-wayland/river-status-unstable-v1-protocol.c
-cflags
-std=c99
--
/home/ifreund/projects/river/zig-cache/zig-wayland/river-layout-v3-protocol.c
-cflags
-std=c99
--
/home/ifreund/projects/river/zig-cache/zig-wayland/wlr-layer-shell-unstable-v1-protocol.c
-cflags
-std=c99
--
/home/ifreund/projects/river/zig-cache/zig-wayland/wlr-output-power-management-unstable-v1-protocol.c
-cflags
-std=c99
--
/home/ifreund/projects/river/zig-cache/zig-wayland/wayland-protocol.c
-cflags
-std=c99
--
/home/ifreund/projects/river/zig-cache/zig-wayland/xdg-shell-protocol.c
-cflags
-std=c99
--
/home/ifreund/projects/river/zig-cache/zig-wayland/ext-session-lock-v1-protocol.c
-cflags
-std=c99
--
/home/ifreund/projects/river/zig-cache/zig-wayland/pointer-gestures-unstable-v1-protocol.c
-cflags
-std=c99
--
/home/ifreund/projects/river/zig-cache/zig-wayland/pointer-constraints-unstable-v1-protocol.c
-cflags
-std=c99
--
/home/ifreund/projects/river/zig-cache/zig-wayland/xdg-decoration-unstable-v1-protocol.c
--verbose-link
-fno-strip
--cache-dir
/home/ifreund/projects/river/zig-cache
--global-cache-dir
/home/ifreund/.cache/zig
--name
river
--pkg-begin
build_options
/home/ifreund/projects/river/zig-cache/options/PFeQBqsbBfUE1Hxk6euUwQxjZyqmNbiHLz7NeGT-cli0bJ0Gwa3C9KRImKOZfRFZ
--pkg-end
--pkg-begin
wayland
/home/ifreund/projects/river/zig-cache/zig-wayland/wayland.zig
--pkg-end
--pkg-begin
xkbcommon
/home/ifreund/projects/river/deps/zig-xkbcommon/src/xkbcommon.zig
--pkg-end
--pkg-begin
pixman
/home/ifreund/projects/river/deps/zig-pixman/pixman.zig
--pkg-end
--pkg-begin
wlroots
/home/ifreund/projects/river/deps/zig-wlroots/src/wlroots.zig
--pkg-begin
wayland
/home/ifreund/projects/river/zig-cache/zig-wayland/wayland.zig
--pkg-end
--pkg-begin
xkbcommon
/home/ifreund/projects/river/deps/zig-xkbcommon/src/xkbcommon.zig
--pkg-end
--pkg-begin
pixman
/home/ifreund/projects/river/deps/zig-pixman/pixman.zig
--pkg-end
--pkg-end
--pkg-begin
flags
/home/ifreund/projects/river/common/flags.zig
--pkg-end
--pkg-begin
globber
/home/ifreund/projects/river/common/globber.zig
--pkg-end
-fno-PIE
--enable-cache
This is the lld invocation, notably with -L paths not passed to zig build-exe
LLD
Link...
ld.lld
--error-limit=0
-O0
-z
stack-size=16777216
--gc-sections
-znow
-m
elf_x86_64
-o
/home/ifreund/projects/river/zig-cache/o/f211c5300e0bd9a8c792222e98477eb8/river
/usr/lib64/gcc/x86_64-unknown-linux-gnu/12.2.0/../../../../lib64/crt1.o
/usr/lib64/gcc/x86_64-unknown-linux-gnu/12.2.0/../../../../lib64/crti.o
-rpath
/home/ifreund/.local/lib
-rpath
/lib64
-rpath
/lib
-rpath
/usr/lib64
-rpath
/usr/lib
-L
/home/ifreund/.local/lib
-L
/home/ifreund/.local/lib
-L
/usr/local/lib64
-L
/usr/local/lib
-L
/usr/lib/x86_64-linux-gnu
-L
/lib64
-L
/lib
-L
/usr/lib64
-L
/usr/lib
-L
/lib/x86_64-linux-gnu
-L
/usr/lib64/gcc/x86_64-unknown-linux-gnu/12.2.0/../../../../lib64
-dynamic-linker
/lib64/ld-linux-x86-64.so.2
/home/ifreund/projects/river/zig-cache/o/6bd51a4e9e3a8206829692dcbfba5597/wlroots_log_wrapper.o
/home/ifreund/projects/river/zig-cache/o/f1c73443dbc049d7a9b343d02f0fd37f/river-control-unstable-v1-protocol.o
/home/ifreund/projects/river/zig-cache/o/ffceb842e67fcfad859fb3c9b043494f/river-status-unstable-v1-protocol.o
/home/ifreund/projects/river/zig-cache/o/564426e805589cea984b22febbd3f658/river-layout-v3-protocol.o
/home/ifreund/projects/river/zig-cache/o/eb48adeb59558bdc61ee7daad7c66441/wlr-layer-shell-unstable-v1-protocol.o
/home/ifreund/projects/river/zig-cache/o/4cf6df4c782bd5c468677cbcfe02a976/wlr-output-power-management-unstable-v1-protocol.o
/home/ifreund/projects/river/zig-cache/o/566938818770f24821f1f2dbf395e812/wayland-protocol.o
/home/ifreund/projects/river/zig-cache/o/2908e3d7ff2c44fbc41069a2541f9a7d/xdg-shell-protocol.o
/home/ifreund/projects/river/zig-cache/o/aa59332b62a050ddd3b50686b5f40d6e/ext-session-lock-v1-protocol.o
/home/ifreund/projects/river/zig-cache/o/fbdadbd90f0aa91adfe677f4c1c0c9d2/pointer-gestures-unstable-v1-protocol.o
/home/ifreund/projects/river/zig-cache/o/0cc1c95f3b0a63875580c4911e1b999b/pointer-constraints-unstable-v1-protocol.o
/home/ifreund/projects/river/zig-cache/o/bcdd16725f5f754203bbb7490f8b18cd/xdg-decoration-unstable-v1-protocol.o
/home/ifreund/projects/river/zig-cache/o/f211c5300e0bd9a8c792222e98477eb8/river.o
--as-needed
-levdev
-linput
-lwayland-server
-lxkbcommon
-lpixman-1
-lwlroots
-lxcb-render
-lxcb
-lm
-lpthread
-lc
-ldl
-lrt
-lutil
/home/ifreund/.cache/zig/o/10ff83478723b881dcc3f83108d8f8d4/libcompiler_rt.a
/usr/lib64/gcc/x86_64-unknown-linux-gnu/12.2.0/../../../../lib64/crtn.o

src/main.zig Outdated
});
}
}
try lib_dirs.appendSlice(paths.lib_dirs.items);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think doing this before searching for libraries would fix the issue I describe.

The order of adding native paths to lib_dirs and searching for libraries has effectively changed with respect to master branch, which I believe is the root cause of the behavior change I see.

Copy link
Member

@ifreund ifreund Jun 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've confirmed that the following patch changing the ordering of these steps fixes this regression for my use-case:

patch
diff --git a/src/main.zig b/src/main.zig
index 56f305e6c..deac5b904 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -2541,6 +2541,54 @@ fn buildOutputType(
     }
     lib_dir_args = undefined; // From here we use lib_dirs instead.
 
+    if (comptime builtin.target.isDarwin()) {
+        // If we want to link against frameworks, we need system headers.
+        if (framework_dirs.items.len > 0 or frameworks.count() > 0)
+            want_native_include_dirs = true;
+    }
+
+    if (sysroot == null and cross_target.isNativeOs() and
+        (system_libs.count() != 0 or want_native_include_dirs))
+    {
+        const paths = std.zig.system.NativePaths.detect(arena, target_info) catch |err| {
+            fatal("unable to detect native system paths: {s}", .{@errorName(err)});
+        };
+        for (paths.warnings.items) |warning| {
+            warn("{s}", .{warning});
+        }
+
+        const has_sysroot = if (comptime builtin.target.isDarwin()) outer: {
+            if (std.zig.system.darwin.isDarwinSDKInstalled(arena)) {
+                const sdk = std.zig.system.darwin.getDarwinSDK(arena, target_info.target) orelse
+                    break :outer false;
+                native_darwin_sdk = sdk;
+                try clang_argv.ensureUnusedCapacity(2);
+                clang_argv.appendAssumeCapacity("-isysroot");
+                clang_argv.appendAssumeCapacity(sdk.path);
+                break :outer true;
+            } else break :outer false;
+        } else false;
+
+        try clang_argv.ensureUnusedCapacity(paths.include_dirs.items.len * 2);
+        const isystem_flag = if (has_sysroot) "-iwithsysroot" else "-isystem";
+        for (paths.include_dirs.items) |include_dir| {
+            clang_argv.appendAssumeCapacity(isystem_flag);
+            clang_argv.appendAssumeCapacity(include_dir);
+        }
+
+        try clang_argv.ensureUnusedCapacity(paths.framework_dirs.items.len * 2);
+        try framework_dirs.ensureUnusedCapacity(paths.framework_dirs.items.len);
+        const iframework_flag = if (has_sysroot) "-iframeworkwithsysroot" else "-iframework";
+        for (paths.framework_dirs.items) |framework_dir| {
+            clang_argv.appendAssumeCapacity(iframework_flag);
+            clang_argv.appendAssumeCapacity(framework_dir);
+            framework_dirs.appendAssumeCapacity(framework_dir);
+        }
+
+        try lib_dirs.appendSlice(paths.lib_dirs.items);
+        try rpath_list.appendSlice(paths.rpaths.items);
+    }
+
     // Now that we have target info, we can find out if any of the system libraries
     // are part of libc or libc++. We remove them from the list and communicate their
     // existence via flags instead.
@@ -2756,54 +2804,6 @@ fn buildOutputType(
         }
     }
 
-    if (comptime builtin.target.isDarwin()) {
-        // If we want to link against frameworks, we need system headers.
-        if (framework_dirs.items.len > 0 or frameworks.count() > 0)
-            want_native_include_dirs = true;
-    }
-
-    if (sysroot == null and cross_target.isNativeOs() and
-        (resolved_system_libs.len != 0 or want_native_include_dirs))
-    {
-        const paths = std.zig.system.NativePaths.detect(arena, target_info) catch |err| {
-            fatal("unable to detect native system paths: {s}", .{@errorName(err)});
-        };
-        for (paths.warnings.items) |warning| {
-            warn("{s}", .{warning});
-        }
-
-        const has_sysroot = if (comptime builtin.target.isDarwin()) outer: {
-            if (std.zig.system.darwin.isDarwinSDKInstalled(arena)) {
-                const sdk = std.zig.system.darwin.getDarwinSDK(arena, target_info.target) orelse
-                    break :outer false;
-                native_darwin_sdk = sdk;
-                try clang_argv.ensureUnusedCapacity(2);
-                clang_argv.appendAssumeCapacity("-isysroot");
-                clang_argv.appendAssumeCapacity(sdk.path);
-                break :outer true;
-            } else break :outer false;
-        } else false;
-
-        try clang_argv.ensureUnusedCapacity(paths.include_dirs.items.len * 2);
-        const isystem_flag = if (has_sysroot) "-iwithsysroot" else "-isystem";
-        for (paths.include_dirs.items) |include_dir| {
-            clang_argv.appendAssumeCapacity(isystem_flag);
-            clang_argv.appendAssumeCapacity(include_dir);
-        }
-
-        try clang_argv.ensureUnusedCapacity(paths.framework_dirs.items.len * 2);
-        try framework_dirs.ensureUnusedCapacity(paths.framework_dirs.items.len);
-        const iframework_flag = if (has_sysroot) "-iframeworkwithsysroot" else "-iframework";
-        for (paths.framework_dirs.items) |framework_dir| {
-            clang_argv.appendAssumeCapacity(iframework_flag);
-            clang_argv.appendAssumeCapacity(framework_dir);
-            framework_dirs.appendAssumeCapacity(framework_dir);
-        }
-
-        try lib_dirs.appendSlice(paths.lib_dirs.items);
-        try rpath_list.appendSlice(paths.rpaths.items);
-    }
-
     const object_format = target_info.target.ofmt;
 
     if (output_mode == .Obj and (object_format == .coff or object_format == .macho)) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I believe that patch is not entirely correct however as it changes the behavior when the only libs in system_libs are part of libc for example.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think this patch should have fully correct behavior:

diff --git a/src/main.zig b/src/main.zig
index 56f305e6c..be2f5e47f 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -2544,44 +2544,31 @@ fn buildOutputType(
     // Now that we have target info, we can find out if any of the system libraries
     // are part of libc or libc++. We remove them from the list and communicate their
     // existence via flags instead.
-    // Similarly, if any libs in this list are statically provided, we omit
-    // them from the resolved list and populate the link_objects array instead.
-    var resolved_system_libs: std.MultiArrayList(struct {
-        name: []const u8,
-        lib: Compilation.SystemLib,
-    }) = .{};
-
     {
-        var test_path = std.ArrayList(u8).init(gpa);
-        defer test_path.deinit();
-
-        var checked_paths = std.ArrayList(u8).init(gpa);
-        defer checked_paths.deinit();
-
-        var failed_libs = std.ArrayList(struct {
-            name: []const u8,
-            strategy: SystemLib.SearchStrategy,
-            checked_paths: []const u8,
-            preferred_mode: std.builtin.LinkMode,
-        }).init(arena);
+        var i: usize = 0;
+        while (i < system_libs.count()) {
+            const lib_name = system_libs.keys()[i];
 
-        syslib: for (system_libs.keys(), system_libs.values()) |lib_name, info| {
             if (target_util.is_libc_lib_name(target_info.target, lib_name)) {
                 link_libc = true;
+                system_libs.orderedRemoveAt(i);
                 continue;
             }
             if (target_util.is_libcpp_lib_name(target_info.target, lib_name)) {
                 link_libcpp = true;
+                system_libs.orderedRemoveAt(i);
                 continue;
             }
             switch (target_util.classifyCompilerRtLibName(target_info.target, lib_name)) {
                 .none => {},
                 .only_libunwind, .both => {
                     link_libunwind = true;
+                    system_libs.orderedRemoveAt(i);
                     continue;
                 },
                 .only_compiler_rt => {
                     std.log.warn("ignoring superfluous library '{s}': this dependency is fulfilled instead by compiler-rt which zig unconditionally provides", .{lib_name});
+                    system_libs.orderedRemoveAt(i);
                     continue;
                 },
             }
@@ -2593,10 +2580,85 @@ fn buildOutputType(
             if (target_info.target.os.tag == .wasi) {
                 if (wasi_libc.getEmulatedLibCRTFile(lib_name)) |crt_file| {
                     try wasi_emulated_libs.append(crt_file);
+                    system_libs.orderedRemoveAt(i);
                     continue;
                 }
             }
 
+            i += 1;
+        }
+    }
+
+    if (comptime builtin.target.isDarwin()) {
+        // If we want to link against frameworks, we need system headers.
+        if (framework_dirs.items.len > 0 or frameworks.count() > 0)
+            want_native_include_dirs = true;
+    }
+
+    if (sysroot == null and cross_target.isNativeOs() and
+        (system_libs.count() != 0 or want_native_include_dirs))
+    {
+        const paths = std.zig.system.NativePaths.detect(arena, target_info) catch |err| {
+            fatal("unable to detect native system paths: {s}", .{@errorName(err)});
+        };
+        for (paths.warnings.items) |warning| {
+            warn("{s}", .{warning});
+        }
+
+        const has_sysroot = if (comptime builtin.target.isDarwin()) outer: {
+            if (std.zig.system.darwin.isDarwinSDKInstalled(arena)) {
+                const sdk = std.zig.system.darwin.getDarwinSDK(arena, target_info.target) orelse
+                    break :outer false;
+                native_darwin_sdk = sdk;
+                try clang_argv.ensureUnusedCapacity(2);
+                clang_argv.appendAssumeCapacity("-isysroot");
+                clang_argv.appendAssumeCapacity(sdk.path);
+                break :outer true;
+            } else break :outer false;
+        } else false;
+
+        try clang_argv.ensureUnusedCapacity(paths.include_dirs.items.len * 2);
+        const isystem_flag = if (has_sysroot) "-iwithsysroot" else "-isystem";
+        for (paths.include_dirs.items) |include_dir| {
+            clang_argv.appendAssumeCapacity(isystem_flag);
+            clang_argv.appendAssumeCapacity(include_dir);
+        }
+
+        try clang_argv.ensureUnusedCapacity(paths.framework_dirs.items.len * 2);
+        try framework_dirs.ensureUnusedCapacity(paths.framework_dirs.items.len);
+        const iframework_flag = if (has_sysroot) "-iframeworkwithsysroot" else "-iframework";
+        for (paths.framework_dirs.items) |framework_dir| {
+            clang_argv.appendAssumeCapacity(iframework_flag);
+            clang_argv.appendAssumeCapacity(framework_dir);
+            framework_dirs.appendAssumeCapacity(framework_dir);
+        }
+
+        try lib_dirs.appendSlice(paths.lib_dirs.items);
+        try rpath_list.appendSlice(paths.rpaths.items);
+    }
+
+    // If any libs in this list are statically provided, we omit them from the resolved list
+    // and populate the link_objects array instead.
+    var resolved_system_libs: std.MultiArrayList(struct {
+        name: []const u8,
+        lib: Compilation.SystemLib,
+    }) = .{};
+
+    {
+        var test_path = std.ArrayList(u8).init(gpa);
+        defer test_path.deinit();
+
+        var checked_paths = std.ArrayList(u8).init(gpa);
+        defer checked_paths.deinit();
+
+        var failed_libs = std.ArrayList(struct {
+            name: []const u8,
+            strategy: SystemLib.SearchStrategy,
+            checked_paths: []const u8,
+            preferred_mode: std.builtin.LinkMode,
+        }).init(arena);
+
+        syslib: for (system_libs.keys(), system_libs.values()) |lib_name, info| {
             checked_paths.clearRetainingCapacity();
 
             switch (info.search_strategy) {
@@ -2756,54 +2818,6 @@ fn buildOutputType(
         }
     }
 
-    if (comptime builtin.target.isDarwin()) {
-        // If we want to link against frameworks, we need system headers.
-        if (framework_dirs.items.len > 0 or frameworks.count() > 0)
-            want_native_include_dirs = true;
-    }
-
-    if (sysroot == null and cross_target.isNativeOs() and
-        (resolved_system_libs.len != 0 or want_native_include_dirs))
-    {
-        const paths = std.zig.system.NativePaths.detect(arena, target_info) catch |err| {
-            fatal("unable to detect native system paths: {s}", .{@errorName(err)});
-        };
-        for (paths.warnings.items) |warning| {
-            warn("{s}", .{warning});
-        }
-
-        const has_sysroot = if (comptime builtin.target.isDarwin()) outer: {
-            if (std.zig.system.darwin.isDarwinSDKInstalled(arena)) {
-                const sdk = std.zig.system.darwin.getDarwinSDK(arena, target_info.target) orelse
-                    break :outer false;
-                native_darwin_sdk = sdk;
-                try clang_argv.ensureUnusedCapacity(2);
-                clang_argv.appendAssumeCapacity("-isysroot");
-                clang_argv.appendAssumeCapacity(sdk.path);
-                break :outer true;
-            } else break :outer false;
-        } else false;
-
-        try clang_argv.ensureUnusedCapacity(paths.include_dirs.items.len * 2);
-        const isystem_flag = if (has_sysroot) "-iwithsysroot" else "-isystem";
-        for (paths.include_dirs.items) |include_dir| {
-            clang_argv.appendAssumeCapacity(isystem_flag);
-            clang_argv.appendAssumeCapacity(include_dir);
-        }
-
-        try clang_argv.ensureUnusedCapacity(paths.framework_dirs.items.len * 2);
-        try framework_dirs.ensureUnusedCapacity(paths.framework_dirs.items.len);
-        const iframework_flag = if (has_sysroot) "-iframeworkwithsysroot" else "-iframework";
-        for (paths.framework_dirs.items) |framework_dir| {
-            clang_argv.appendAssumeCapacity(iframework_flag);
-            clang_argv.appendAssumeCapacity(framework_dir);
-            framework_dirs.appendAssumeCapacity(framework_dir);
-        }
-
-        try lib_dirs.appendSlice(paths.lib_dirs.items);
-        try rpath_list.appendSlice(paths.rpaths.items);
-    }
-
     const object_format = target_info.target.ofmt;
 
     if (output_mode == .Obj and (object_format == .coff or object_format == .macho)) {

Copy link
Member

@kubkon kubkon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You reminded me @ifreund that when we link against system libs on Linux GNU, system libs are actually linker scripts which might included a path such as -lgcc_s or similar which will require knowing library search paths by the linker.

@ifreund
Copy link
Member

ifreund commented Jun 16, 2023

Indeed... why do you have to be like this gnu :/

$ cat /usr/lib/libgcc_s.so
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library.  */
GROUP ( libgcc_s.so.1 -lgcc )

@andrewrk
Copy link
Member Author

So, this may be a bit counter-intuitive, but I actually think there is a strong argument for moving parsing of these fake .so files into the CLI as well. Hear me out...

zig/src/main.zig

Lines 2459 to 2471 in 0f5aff3

switch (target_util.classifyCompilerRtLibName(target_info.target, lib_name)) {
.none => {},
.only_libunwind, .both => {
link_libunwind = true;
system_libs.orderedRemoveAt(i);
continue;
},
.only_compiler_rt => {
std.log.warn("ignoring superfluous library '{s}': this dependency is fulfilled instead by compiler-rt which zig unconditionally provides", .{lib_name});
system_libs.orderedRemoveAt(i);
continue;
},
}

This code here is in master branch, and checks if -lgcc_s is provided on the command line. Look familiar?

The frontend is in the business of knowing which libraries are part of libc, part of compiler_rt, or other, because it needs this information to set up the compilation pipeline - potentially scheduling the creation of compiler_rt to run in parallel, for example.

Also, I think it is telling that inside these files are command line arguments so it actually does make sense if you think about it to handle in the command line argument parsing code.

@andrewrk
Copy link
Member Author

I searched my hard drive for .so files that are actually text files, and came up with a small list of unique test cases. The entire list of unique file basenames in this sample set is:

libbsd.so
libc++.so
libc.so
libgcc_s.so
libm.so
libpthread.so
libtbb.so
libtbbmalloc.so
libtbbmalloc_proxy.so

All the files concatenated together, with redundant contents manually removed:

----------------------------
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library, so try that secondarily.  */
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( /nix/store/2j8jqmnd9l7plihhf713yf291c9vyqjm-glibc-2.35-224/lib/libc.so.6 /nix/store/2j8jqmnd9l7plihhf713yf291c9vyqjm-glibc-2.35-224/lib/libc_nonshared.a  AS_NEEDED ( /nix/store/2j8jqmnd9l7plihhf713yf291c9vyqjm-glibc-2.35-224/lib/ld-linux-x86-64.so.2 ) )
----------------------------
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library.  */
GROUP ( libgcc_s.so.1 -lgcc )
----------------------------
/* GNU ld script
*/
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( /nix/store/2j8jqmnd9l7plihhf713yf291c9vyqjm-glibc-2.35-224/lib/libm.so.6  AS_NEEDED ( /nix/store/2j8jqmnd9l7plihhf713yf291c9vyqjm-glibc-2.35-224/lib/libmvec.so.1 ) )
----------------------------
/* GNU ld script
 * The MD5 functions are provided by the libmd library. */
OUTPUT_FORMAT(elf32-i386)
GROUP(/nix/store/9jjcvqp0x2kyvmbbvaxmp4qfps66sffr-libbsd-0.11.6/lib/libbsd.so.0.11.6 AS_NEEDED(-lmd))
----------------------------
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library, so try that secondarily.  */
OUTPUT_FORMAT(elf32-i386)
GROUP ( /nix/store/bzw74a7dla8iqk2h8wshwi03fbgj2c5h-glibc-2.35-163/lib/libc.so.6 /nix/store/bzw74a7dla8iqk2h8wshwi03fbgj2c5h-glibc-2.35-163/lib/libc_nonshared.a  AS_NEEDED ( /nix/store/bzw74a7dla8iqk2h8wshwi03fbgj2c5h-glibc-2.35-163/lib/ld-linux.so.2 ) )
----------------------------
INPUT (libtbb.so.2)
----------------------------
INPUT (libtbbmalloc.so.2)
----------------------------
INPUT (libtbbmalloc_proxy.so.2)
----------------------------
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library, so try that secondarily.  */
OUTPUT_FORMAT(elf32-i386)
GROUP ( libc.so.6 libc_nonshared.a  AS_NEEDED ( ld-linux.so.2 ) )
----------------------------
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library, so try that secondarily.  */
OUTPUT_FORMAT(elf32-i386)
GROUP ( libpthread.so.0 libpthread_nonshared.a )
----------------------------
INPUT(libc++.so.1 -lc++abi)
----------------------------
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library, so try that secondarily.  */
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( libc.so.6 libc_nonshared.a  AS_NEEDED ( ld-linux-x86-64.so.2 ) )
----------------------------
/* GNU ld script
*/
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( libm.so.6  AS_NEEDED ( libmvec_nonshared.a libmvec.so.1 ) )
----------------------------
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library, so try that secondarily.  */
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( libpthread.so.0 libpthread_nonshared.a )
----------------------------
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library, so try that secondarily.  */
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( /nix/store/vnwdak3n1w2jjil119j65k8mw1z23p84-glibc-2.35-224/lib/libc.so.6 /nix/store/vnwdak3n1w2jjil119j65k8mw1z23p84-glibc-2.35-224/lib/libc_nonshared.a  AS_NEEDED ( /nix/store/vnwdak3n1w2jjil119j65k8mw1z23p84-glibc-2.35-224/lib/ld-linux-x86-64.so.2 ) )
----------------------------

As you can see, there are not very many different concepts at play here. All this stuff directly corresponds to command line arguments.

@kubkon
Copy link
Member

kubkon commented Jun 17, 2023

Yeah, I think we should be able to do this in the CLI frontend. As far as I understand, we only need to support a limited ld script syntax to be able to correctly parse those files, and the good news is I've got that implemented in zld already. I could then upstream the corresponding functionality of the linker and make it part of the CLI which would look something like this:

  1. Preopen each positional and check if it's a linker script.
  2. Parse the script and substitute it for its contents preserving the link state (this involves correctly handling GROUP together with AS_NEEDED).
  3. Repeat 2. recursively as a script may refer another script.
  4. Pass the parsed positionals to the linker.

What do you think @andrewrk and @ifreund?

@andrewrk
Copy link
Member Author

I think it sounds like the path forward!

@kubkon
Copy link
Member

kubkon commented Jun 17, 2023

I think it sounds like the path forward!

Excellent!

@ifreund ifreund added this to the 0.11.0 milestone Jun 20, 2023
@kubkon kubkon force-pushed the frontend-lib-paths branch from 889ad14 to f60d392 Compare July 25, 2023 13:55
@andrewrk andrewrk force-pushed the frontend-lib-paths branch 3 times, most recently from e2f1abe to bf33b7f Compare August 2, 2023 04:20
@andrewrk andrewrk removed this from the 0.11.0 milestone Aug 2, 2023
@andrewrk andrewrk requested a review from ifreund August 2, 2023 05:25
@andrewrk andrewrk force-pushed the frontend-lib-paths branch 2 times, most recently from 430ae4d to 310db02 Compare August 3, 2023 02:49
Copy link
Member

@kubkon kubkon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me! With cd491f9 building on macOS 14 with SDK installed worked flawlessly 👍

return self;
}

// These do not include headers, so the ones that come with the SDK are preferred.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ever valid? I don't think there is a macOS installation that has frameworks exposed at this path? Could you explain your reasoning here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I do see frameworks at this path. I noticed the headers were missing, but upon further inspection the tbd files are missing too, and in fact I don't see anything here that could be remotely useful for us. So I will remove this path. Thanks!

Comment on lines +193 to +196
self.include_dir = try fs.path.join(args.allocator, &.{
sdk.path, "usr/include",
});
self.sys_include_dir = try fs.path.join(args.allocator, &.{
sdk.path, "usr/include",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take it we do not want to make framework dir part of LibCInstallation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It effectively is, since it can always be found at ../../System/Library/Frameworks which is exactly what detectLibCFromLibCInstallation does. Thankfully the Darwin SDK paths are reasonable, and this property holds. On other operating systems such as Linux, the directories are all over the place, so we have to store multiple different named path roots.

andrewrk and others added 16 commits August 3, 2023 09:52
For each library you can specify the preferred mode and search strategy.

The old way of setting global state is eliminated.
I expect this to have no functional change.
Apparently tidy doesn't exit with failure status if the input file is
not found? wtf tidy
The `null` value here was missed in
0a4d4eb. I hope it is the cause of the
CI failures on Windows.

The fact that libc++ depends on libc is not important for the CLI and
Compilation.create already handles that logic.
Make a bunch of ArrayList objects use arena instead of gpa, eliminating
the `defer` expressions, which reduces code size of zig1.wasm by 1%
Regressed in 2006add.

References to native_darwin_sdk are no longer kept in the frontend.
Instead the darwin SDK is detected as part of NativePaths and as part of
LibCInstallation.
* don't assert that the child process doesn't crash
* don't give a false negative on warnings printed to stderr

Also fix getSdk from the same file in the same way
This path actually has nothing useful in it.
@andrewrk andrewrk force-pushed the frontend-lib-paths branch from cd491f9 to d0fd67c Compare August 3, 2023 16:52
@andrewrk andrewrk merged commit c4e62be into master Aug 3, 2023
@andrewrk andrewrk deleted the frontend-lib-paths branch August 3, 2023 16:53
@ypsvlq
Copy link
Contributor

ypsvlq commented Aug 3, 2023

I don't think this accounts for mingw's lib<name>.dll.a libraries

@andrewrk
Copy link
Member Author

andrewrk commented Aug 3, 2023

Please file a bug report and we can get it fixed in 0.11.1. The logic is here:

zig/src/main.zig

Lines 6240 to 6318 in a327d8b

fn accessLibPath(
test_path: *std.ArrayList(u8),
checked_paths: *std.ArrayList(u8),
lib_dir_path: []const u8,
lib_name: []const u8,
target: std.Target,
link_mode: std.builtin.LinkMode,
) !bool {
const sep = fs.path.sep_str;
if (target.isDarwin() and link_mode == .Dynamic) tbd: {
// Prefer .tbd over .dylib.
test_path.clearRetainingCapacity();
try test_path.writer().print("{s}" ++ sep ++ "lib{s}.tbd", .{ lib_dir_path, lib_name });
try checked_paths.writer().print("\n {s}", .{test_path.items});
fs.cwd().access(test_path.items, .{}) catch |err| switch (err) {
error.FileNotFound => break :tbd,
else => |e| fatal("unable to search for tbd library '{s}': {s}", .{
test_path.items, @errorName(e),
}),
};
return true;
}
main_check: {
test_path.clearRetainingCapacity();
try test_path.writer().print("{s}" ++ sep ++ "{s}{s}{s}", .{
lib_dir_path,
target.libPrefix(),
lib_name,
switch (link_mode) {
.Static => target.staticLibSuffix(),
.Dynamic => target.dynamicLibSuffix(),
},
});
try checked_paths.writer().print("\n {s}", .{test_path.items});
fs.cwd().access(test_path.items, .{}) catch |err| switch (err) {
error.FileNotFound => break :main_check,
else => |e| fatal("unable to search for {s} library '{s}': {s}", .{
@tagName(link_mode), test_path.items, @errorName(e),
}),
};
return true;
}
// In the case of Darwin, the main check will be .dylib, so here we
// additionally check for .so files.
if (target.isDarwin() and link_mode == .Dynamic) so: {
test_path.clearRetainingCapacity();
try test_path.writer().print("{s}" ++ sep ++ "lib{s}.so", .{ lib_dir_path, lib_name });
try checked_paths.writer().print("\n {s}", .{test_path.items});
fs.cwd().access(test_path.items, .{}) catch |err| switch (err) {
error.FileNotFound => break :so,
else => |e| fatal("unable to search for so library '{s}': {s}", .{
test_path.items, @errorName(e),
}),
};
return true;
}
// In the case of MinGW, the main check will be .lib but we also need to
// look for `libfoo.a`.
if (target.isMinGW() and link_mode == .Static) mingw: {
test_path.clearRetainingCapacity();
try test_path.writer().print("{s}" ++ sep ++ "lib{s}.a", .{
lib_dir_path, lib_name,
});
try checked_paths.writer().print("\n {s}", .{test_path.items});
fs.cwd().access(test_path.items, .{}) catch |err| switch (err) {
error.FileNotFound => break :mingw,
else => |e| fatal("unable to search for static library '{s}': {s}", .{
test_path.items, @errorName(e),
}),
};
return true;
}
return false;
}

this is growing pains from moving logic from LLD into the frontend, as we move to using our own linker implementations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants