lib/tldr/src/library.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const sys = @import("sys");
  3 const archive = @import("archive.zig");
  4 
  5 const Allocator = std.mem.Allocator;
  6 const max_linker_script_bytes = 64 * 1024;
  7 
  8 pub fn resolvePath(allocator: Allocator, search_paths: []const []const u8, library: []const u8, static_library_search: bool) ![]const u8 {
  9     if (search_paths.len == 0) return error.LibraryNotFound;
 10     for (search_paths) |dir| {
 11         if (std.mem.startsWith(u8, library, ":")) {
 12             const path = try std.fs.path.join(allocator, &.{ dir, library[1..] });
 13             if (fileExists(path)) return path;
 14             continue;
 15         }
 16         if (!static_library_search) {
 17             const shared_path = try std.fmt.allocPrint(allocator, "{s}/lib{s}.so", .{ dir, library });
 18             if (fileExists(shared_path)) return shared_path;
 19         }
 20         const archive_path = try std.fmt.allocPrint(allocator, "{s}/lib{s}.a", .{ dir, library });
 21         if (fileExists(archive_path)) return archive_path;
 22     }
 23     return error.LibraryNotFound;
 24 }
 25 
 26 pub fn resolveInputPaths(allocator: Allocator, search_paths: []const []const u8, library: []const u8, static_library_search: bool) anyerror![]const []const u8 {
 27     const path = try resolvePath(allocator, search_paths, library, static_library_search);
 28     if (pathLooksLikeBinaryInput(path)) return try allocator.dupe([]const u8, &.{path});
 29     if (try linkerScriptGroupPaths(allocator, path, search_paths, static_library_search)) |group_paths| {
 30         if (group_paths.len == 0) return error.InvalidArguments;
 31         return group_paths;
 32     }
 33     return error.InvalidArguments;
 34 }
 35 
 36 pub fn appendResolvedInputPaths(
 37     allocator: Allocator,
 38     input_paths: *std.ArrayListUnmanaged([]const u8),
 39     search_paths: []const []const u8,
 40     library: []const u8,
 41     static_library_search: bool,
 42 ) !void {
 43     const paths = try resolveInputPaths(allocator, search_paths, library, static_library_search);
 44     try input_paths.appendSlice(allocator, paths);
 45 }
 46 
 47 pub fn pathLooksLikeBinaryInput(path: []const u8) bool {
 48     var header: [8]u8 = undefined;
 49     const len = readFileHeader(path, &header) orelse return false;
 50     if (archive.isArchive(header[0..len])) return true;
 51     if (len >= std.elf.MAGIC.len and std.mem.eql(u8, header[0..std.elf.MAGIC.len], std.elf.MAGIC)) return true;
 52     return false;
 53 }
 54 
 55 pub fn linkerScriptGroupPaths(allocator: Allocator, path: []const u8, search_paths: []const []const u8, static_library_search: bool) anyerror!?[]const []const u8 {
 56     const bytes = try readSmallPathAlloc(allocator, path);
 57     return try parseLinkerScriptGroupPaths(allocator, path, bytes, search_paths, static_library_search);
 58 }
 59 
 60 fn fileExists(path: []const u8) bool {
 61     var file = if (std.fs.path.isAbsolute(path))
 62         sys.fs.openAbsoluteFile(path, .{}) catch return false
 63     else
 64         sys.fs.cwd().openFile(sys.fs.debugIo(), path, .{}) catch return false;
 65     file.close(sys.fs.debugIo());
 66     return true;
 67 }
 68 
 69 fn readFileHeader(path: []const u8, header: *[8]u8) ?usize {
 70     var file = if (std.fs.path.isAbsolute(path))
 71         sys.fs.openAbsoluteFile(path, .{}) catch return null
 72     else
 73         sys.fs.cwd().openFile(sys.fs.debugIo(), path, .{}) catch return null;
 74     defer file.close(sys.fs.debugIo());
 75     return sys.fs.readHandle(file, header) catch null;
 76 }
 77 
 78 fn readSmallPathAlloc(allocator: Allocator, path: []const u8) ![]const u8 {
 79     if (!std.fs.path.isAbsolute(path)) return try sys.fs.cwd().readFileAlloc(sys.fs.debugIo(), path, allocator, .limited(max_linker_script_bytes));
 80     const parent = std.fs.path.dirname(path) orelse "/";
 81     const name = std.fs.path.basename(path);
 82     var dir = try sys.fs.openAbsoluteDir(parent, .{});
 83     defer dir.close(sys.fs.debugIo());
 84     return try dir.readFileAlloc(sys.fs.debugIo(), name, allocator, .limited(max_linker_script_bytes));
 85 }
 86 
 87 fn parseLinkerScriptGroupPaths(
 88     allocator: Allocator,
 89     script_path: []const u8,
 90     bytes: []const u8,
 91     search_paths: []const []const u8,
 92     static_library_search: bool,
 93 ) anyerror!?[]const []const u8 {
 94     var index: usize = 0;
 95     while (nextLinkerScriptToken(bytes, &index)) |token| {
 96         if (!linkerScriptGroupCommand(token)) continue;
 97         const open = nextLinkerScriptToken(bytes, &index) orelse return error.InvalidArguments;
 98         if (!std.mem.eql(u8, open, "(")) return error.InvalidArguments;
 99         var depth: usize = 1;
100         var paths: std.ArrayListUnmanaged([]const u8) = .empty;
101         while (nextLinkerScriptToken(bytes, &index)) |group_token| {
102             if (std.mem.eql(u8, group_token, "(")) {
103                 depth += 1;
104                 continue;
105             }
106             if (std.mem.eql(u8, group_token, ")")) {
107                 depth -= 1;
108                 if (depth == 0) return try paths.toOwnedSlice(allocator);
109                 continue;
110             }
111             if (linkerScriptNestingCommand(group_token)) continue;
112             try appendLinkerScriptGroupTokenPaths(allocator, &paths, script_path, group_token, search_paths, static_library_search);
113         }
114         return error.InvalidArguments;
115     }
116     return null;
117 }
118 
119 fn nextLinkerScriptToken(bytes: []const u8, index: *usize) ?[]const u8 {
120     while (index.* < bytes.len) {
121         const byte = bytes[index.*];
122         if (std.ascii.isWhitespace(byte)) {
123             index.* += 1;
124             continue;
125         }
126         if (byte == '/' and index.* + 1 < bytes.len and bytes[index.* + 1] == '*') {
127             index.* += 2;
128             while (index.* + 1 < bytes.len and !(bytes[index.*] == '*' and bytes[index.* + 1] == '/')) index.* += 1;
129             if (index.* + 1 >= bytes.len) return null;
130             index.* += 2;
131             continue;
132         }
133         break;
134     }
135     if (index.* >= bytes.len) return null;
136     const start = index.*;
137     const byte = bytes[index.*];
138     if (byte == '(' or byte == ')') {
139         index.* += 1;
140         return bytes[start..index.*];
141     }
142     if (byte == '\'' or byte == '"') {
143         index.* += 1;
144         const token_start = index.*;
145         while (index.* < bytes.len and bytes[index.*] != byte) index.* += 1;
146         const token = bytes[token_start..index.*];
147         if (index.* < bytes.len) index.* += 1;
148         return token;
149     }
150     while (index.* < bytes.len and !std.ascii.isWhitespace(bytes[index.*]) and bytes[index.*] != '(' and bytes[index.*] != ')') index.* += 1;
151     return bytes[start..index.*];
152 }
153 
154 fn linkerScriptGroupCommand(token: []const u8) bool {
155     return std.mem.eql(u8, token, "GROUP") or std.mem.eql(u8, token, "INPUT");
156 }
157 
158 fn linkerScriptNestingCommand(token: []const u8) bool {
159     return std.mem.eql(u8, token, "AS_NEEDED");
160 }
161 
162 fn appendLinkerScriptGroupTokenPaths(
163     allocator: Allocator,
164     paths: *std.ArrayListUnmanaged([]const u8),
165     script_path: []const u8,
166     token: []const u8,
167     search_paths: []const []const u8,
168     static_library_search: bool,
169 ) anyerror!void {
170     if (token.len == 0) return error.InvalidArguments;
171     if (std.mem.startsWith(u8, token, "-l") and token.len > 2) {
172         const resolved = try resolveInputPaths(allocator, search_paths, token[2..], static_library_search);
173         try paths.appendSlice(allocator, resolved);
174         return;
175     }
176     if (std.mem.startsWith(u8, token, "-")) return error.InvalidArguments;
177     try paths.append(allocator, try resolveLinkerScriptGroupPath(allocator, script_path, token, search_paths));
178 }
179 
180 fn resolveLinkerScriptGroupPath(allocator: Allocator, script_path: []const u8, token: []const u8, search_paths: []const []const u8) ![]const u8 {
181     if (std.fs.path.isAbsolute(token)) return try allocator.dupe(u8, token);
182     const parent = std.fs.path.dirname(script_path);
183     if (parent) |dir| {
184         const relative_to_script = try std.fs.path.join(allocator, &.{ dir, token });
185         if (fileExists(relative_to_script)) return relative_to_script;
186     }
187     for (search_paths) |dir| {
188         const relative_to_search = try std.fs.path.join(allocator, &.{ dir, token });
189         if (fileExists(relative_to_search)) return relative_to_search;
190     }
191     if (parent) |dir| return try std.fs.path.join(allocator, &.{ dir, token });
192     return try allocator.dupe(u8, token);
193 }
194 
195 test "library search resolves archives and linker script groups" {
196     var tmp = std.testing.tmpDir(.{});
197     defer tmp.cleanup();
198 
199     var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
200     defer arena_state.deinit();
201     const arena = arena_state.allocator();
202 
203     const root = try tmp.dir.realPathFileAlloc(sys.fs.debugIo(), ".", arena);
204     const library_dir = try std.fs.path.join(arena, &.{ root, "lib" });
205     try sys.fs.createDirPath(library_dir);
206     const direct_path = try std.fs.path.join(arena, &.{ library_dir, "libdirect.a" });
207     try sys.fs.writeFile(direct_path, archive.magic);
208     const member_path = try std.fs.path.join(arena, &.{ library_dir, "member.a" });
209     try sys.fs.writeFile(member_path, archive.magic);
210     const script_path = try std.fs.path.join(arena, &.{ library_dir, "libscript.a" });
211     try sys.fs.writeFile(script_path, "/* GNU ld script */\nGROUP ( member.a )\n");
212     const shared_path = try std.fs.path.join(arena, &.{ library_dir, "libdual.so" });
213     try sys.fs.writeFile(shared_path, std.elf.MAGIC ++ "shared");
214     const shared_archive_path = try std.fs.path.join(arena, &.{ library_dir, "libdual.a" });
215     try sys.fs.writeFile(shared_archive_path, archive.magic);
216     const alternate_dir = try std.fs.path.join(arena, &.{ root, "alternate" });
217     try sys.fs.createDirPath(alternate_dir);
218     const searched_member_path = try std.fs.path.join(arena, &.{ alternate_dir, "libsearched.so.1" });
219     try sys.fs.writeFile(searched_member_path, std.elf.MAGIC ++ "searched");
220     const dependency_path = try std.fs.path.join(arena, &.{ alternate_dir, "libdependency.a" });
221     try sys.fs.writeFile(dependency_path, archive.magic);
222     const dynamic_script_path = try std.fs.path.join(arena, &.{ library_dir, "libdynamic.so" });
223     try sys.fs.writeFile(dynamic_script_path, "/* GNU ld script */\nGROUP ( libsearched.so.1 -ldependency )\n");
224 
225     const direct = try resolveInputPaths(arena, &.{library_dir}, "direct", true);
226     try std.testing.expectEqual(@as(usize, 1), direct.len);
227     try std.testing.expectEqualStrings(direct_path, direct[0]);
228 
229     const script = try resolveInputPaths(arena, &.{library_dir}, "script", true);
230     try std.testing.expectEqual(@as(usize, 1), script.len);
231     try std.testing.expectEqualStrings(member_path, script[0]);
232 
233     const dynamic = try resolveInputPaths(arena, &.{ library_dir, alternate_dir }, "dynamic", false);
234     try std.testing.expectEqual(@as(usize, 2), dynamic.len);
235     try std.testing.expectEqualStrings(searched_member_path, dynamic[0]);
236     try std.testing.expectEqualStrings(dependency_path, dynamic[1]);
237 
238     const shared = try resolveInputPaths(arena, &.{library_dir}, "dual", false);
239     try std.testing.expectEqual(@as(usize, 1), shared.len);
240     try std.testing.expectEqualStrings(shared_path, shared[0]);
241 
242     const static = try resolveInputPaths(arena, &.{library_dir}, "dual", true);
243     try std.testing.expectEqual(@as(usize, 1), static.len);
244     try std.testing.expectEqualStrings(shared_archive_path, static[0]);
245 }