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 }