1const std = @import("std");
2const string = []const u8;
3const ansi = @import("ansi");
4const extras = @import("extras");
5const nfs = @import("nfs");
6const nio = @import("nio");
7
8const zigmod = @import("../lib.zig");
9const u = @import("./../util/funcs.zig");
10const common = @import("./../common.zig");
11const license = @import("./license.zig");
12
13//
14//
15
16pub fn execute(self_name: []const u8, args: []const [:0]const u8) !void {
17 _ = self_name;
18
19 const gpa = std.heap.c_allocator;
20 const cachepath = try u.find_cachepath();
21 const dir = nfs.cwd();
22 const should_update = !(args.len >= 1 and std.mem.eql(u8, args[0], "--no-update"));
23
24 var options = common.CollectOptions{
25 .log = should_update,
26 .update = should_update,
27 .alloc = gpa,
28 };
29 const top_module = try common.collect_deps_deep(cachepath, dir, &options);
30
31 var list = std.array_list.Managed(zigmod.Module).init(gpa);
32 try common.collect_pkgs(top_module, &list);
33
34 try create_depszig(gpa, cachepath, dir, top_module, &list);
35
36 try create_lockfile(gpa, &list, cachepath, dir);
37
38 try diff_lockfile(gpa);
39
40 options.update = false;
41
42 var outfile = try dir.createFile("licenses.txt", .{});
43 defer outfile.close();
44
45 try license.do(cachepath, dir, &options, outfile);
46}
47
48pub fn create_depszig(alloc: std.mem.Allocator, cachepath: string, dir: nfs.Dir, top_module: zigmod.Module, list: *std.array_list.Managed(zigmod.Module)) !void {
49 const f = try dir.createFile("deps.zig", .{});
50 defer f.close();
51
52 const w = f;
53 try w.writeAll("// zig fmt: off\n");
54 try w.writeAll("const std = @import(\"std\");\n");
55 try w.writeAll("const builtin = @import(\"builtin\");\n");
56 try w.writeAll("const string = []const u8;\n");
57 try w.writeAll("\n");
58 try w.print("pub const cache = \"{}\";\n", .{u.altStringEscape(cachepath)});
59 try w.writeAll("pub var skip_libc = false;\n");
60 try w.writeAll("\n");
61 try w.writeAll(
62 \\pub fn addAllTo(exe: *std.Build.Step.Compile) void {
63 \\ checkMinZig(builtin.zig_version, exe);
64 \\ @setEvalBranchQuota(1_000_000);
65 \\ for (packages) |pkg| {
66 \\ const module = pkg.module(exe);
67 \\ exe.root_module.addImport(pkg.import.?[0], module);
68 \\ }
69 \\ for (package_data._root.system_libs) |libname| {
70 \\ exe.root_module.linkSystemLibrary(libname, .{});
71 \\ exe.root_module.link_libc = true;
72 \\ }
73 \\ // clear module memo cache so addAllTo can be called more than once in the same build.zig
74 \\ module_memo.clearAndFree(exe.step.owner.allocator);
75 \\}
76 \\
77 \\var link_lib_c = false;
78 \\var module_memo: std.StringArrayHashMapUnmanaged(*std.Build.Module) = .empty;
79 \\pub const Package = struct {
80 \\ id: string,
81 \\ directory: string,
82 \\ import: ?struct { string, std.Build.LazyPath } = null,
83 \\ dependencies: []const *const Package,
84 \\ c_include_dirs: []const string = &.{},
85 \\ c_source_files: []const string = &.{},
86 \\ c_source_flags: []const string = &.{},
87 \\ system_libs: []const string = &.{},
88 \\ frameworks: []const string = &.{},
89 \\
90 \\ pub fn module(self: *const Package, exe: *std.Build.Step.Compile) *std.Build.Module {
91 \\ if (module_memo.get(self.id)) |cached| {
92 \\ return cached;
93 \\ }
94 \\ const b = exe.step.owner;
95 \\ const result = b.createModule(.{
96 \\ .target = exe.root_module.resolved_target,
97 \\ });
98 \\ const target = result.resolved_target.?.result;
99 \\ if (self.import) |capture| {
100 \\ result.root_source_file = capture[1];
101 \\ }
102 \\ for (self.dependencies) |item| {
103 \\ const module_dep = item.module(exe);
104 \\ if (module_dep.root_source_file != null) {
105 \\ result.addImport(item.import.?[0], module_dep);
106 \\ }
107 \\ for (module_dep.include_dirs.items) |jtem| {
108 \\ switch (jtem) {
109 \\ .path => result.addIncludePath(jtem.path),
110 \\ .path_system, .path_after, .framework_path, .framework_path_system, .other_step, .config_header_step => {},
111 \\ .embed_path => {},
112 \\ }
113 \\ }
114 \\ }
115 \\ for (self.c_include_dirs) |item| {
116 \\ result.addIncludePath(.{ .cwd_relative = (b.fmt("{s}/{s}", .{ self.directory, item })) });
117 \\ exe.root_module.addIncludePath(.{ .cwd_relative = (b.fmt("{s}/{s}", .{ self.directory, item })) });
118 \\ link_lib_c = true;
119 \\ }
120 \\ for (self.c_source_files) |item| {
121 \\ exe.root_module.addCSourceFile(.{ .file = .{ .cwd_relative = (b.fmt("{s}/{s}", .{ self.directory, item })) }, .flags = self.c_source_flags });
122 \\ link_lib_c = true;
123 \\ }
124 \\ for (self.system_libs) |item| {
125 \\ if (skip_libc and std.zig.target.isLibCLibName(&target, item)) continue;
126 \\ result.linkSystemLibrary(item, .{});
127 \\ exe.root_module.linkSystemLibrary(item, .{});
128 \\ link_lib_c = true;
129 \\ }
130 \\ for (self.frameworks) |item| {
131 \\ result.linkFramework(item, .{});
132 \\ exe.root_module.linkFramework(item, .{});
133 \\ link_lib_c = true;
134 \\ }
135 \\ if (link_lib_c and !skip_libc) {
136 \\ result.link_libc = true;
137 \\ exe.root_module.link_libc = true;
138 \\ }
139 \\ module_memo.putNoClobber(b.allocator, self.id, result) catch @panic("OOM");
140 \\ return result;
141 \\ }
142 \\};
143 \\
144 \\
145 );
146
147 try w.print(
148 \\fn checkMinZig(current: std.SemanticVersion, exe: *std.Build.Step.Compile) void {{
149 \\ const min = std.SemanticVersion.parse("{?}") catch return;
150 \\ if (current.order(min).compare(.lt)) @panic(exe.step.owner.fmt("Your Zig version v{{f}} does not meet the minimum build requirement of v{{f}}", .{{current, min}}));
151 \\}}
152 \\
153 \\
154 , .{if (top_module.minZigVersion()) |sv| u.altSemanticVersion(sv) else null});
155
156 try w.writeAll("pub const dirs = struct {\n");
157 try print_dirs(w, list.items, alloc);
158 try w.writeAll("};\n\n");
159
160 try w.writeAll("pub const package_data = struct {\n");
161 var duped = std.array_list.Managed(zigmod.Module).init(alloc);
162 var done = std.array_list.Managed(zigmod.Module).init(alloc);
163 for (list.items) |mod| {
164 if (mod.type == .system_lib or mod.type == .framework) {
165 continue;
166 }
167 try duped.append(mod);
168 }
169 try print_pkg_data_to(w, &duped, &done);
170 try w.writeAll("};\n\n");
171
172 try w.writeAll("pub const packages = ");
173 try print_deps(w, top_module);
174 try w.writeAll(";\n\n");
175
176 try w.writeAll("pub const pkgs = ");
177 try print_pkgs(alloc, w, top_module);
178 try w.writeAll(";\n\n");
179
180 try w.writeAll("pub const imports = struct {\n");
181 try print_imports(alloc, w, top_module, cachepath);
182 try w.writeAll("};\n");
183}
184
185fn create_lockfile(alloc: std.mem.Allocator, list: *std.array_list.Managed(zigmod.Module), path: string, dir: nfs.Dir) !void {
186 const fl = try dir.createFile("zigmod.lock", .{});
187 defer fl.close();
188
189 std.mem.sort(zigmod.Module, list.items, {}, zigmod.Module.lessThan);
190
191 const wl = fl;
192 try wl.writeAll("2\n");
193 for (list.items) |m| {
194 if (m.dep) |md| {
195 if (md.type.isLocal()) continue;
196 const mpath = try std.fs.path.joinZ(alloc, &.{ path, m.clean_path });
197 const version = try md.exact_version(alloc, mpath);
198 try wl.print("{s} {s} {s}\n", .{ @tagName(md.type), md.path, version });
199 }
200 }
201}
202
203const DiffChange = struct {
204 from: string,
205 to: string,
206};
207
208fn diff_lockfile(alloc: std.mem.Allocator) !void {
209 const max = std.math.maxInt(usize);
210
211 if (try nfs.cwd().existsDir(".git")) {
212 const result = try u.run_cmd_raw(alloc, null, &.{ "git", "diff", "zigmod.lock" });
213 var stdout = nio.FixedBufferStream([]const u8).init(result.stdout);
214 const r = &stdout;
215 while (try r.readUntilDelimiterOrEofAlloc(alloc, '\n', max)) |line| {
216 if (std.mem.startsWith(u8, line, "@@")) break;
217 }
218
219 var rems = std.array_list.Managed(string).init(alloc);
220 var adds = std.array_list.Managed(string).init(alloc);
221 while (try r.readUntilDelimiterOrEofAlloc(alloc, '\n', max)) |line_full| {
222 const line = line_full[0 .. line_full.len - 1];
223 if (line[0] == ' ') continue;
224 if (line[0] == '-') try rems.append(line[1..]);
225 if (line[0] == '+') if (line[1] == '2') break else try adds.append(line[1..]);
226 }
227
228 var changes = std.StringHashMap(DiffChange).init(alloc);
229
230 var didbreak = false;
231 var i: usize = 0;
232 while (i < rems.items.len) {
233 const it = rems.items[i];
234 const sni = u.indexOfN(it, ' ', 2).?;
235
236 var j: usize = 0;
237 while (j < adds.items.len) {
238 const jt = adds.items[j];
239 const snj = u.indexOfN(jt, ' ', 2).?;
240
241 if (std.mem.eql(u8, it[0..sni], jt[0..snj])) {
242 try changes.put(it[0..sni], .{
243 .from = it[u.indexOfAfter(it, '-', sni).? + 1 .. it.len],
244 .to = jt[u.indexOfAfter(jt, '-', snj).? + 1 .. jt.len],
245 });
246 _ = rems.orderedRemove(i);
247 _ = adds.orderedRemove(j);
248 didbreak = true;
249 break;
250 }
251 if (!didbreak) j += 1;
252 }
253 if (!didbreak) i += 1;
254 if (didbreak) didbreak = false;
255 }
256
257 if (adds.items.len > 0) {
258 std.debug.print(comptime ansi.color.Faint("Newly added packages:\n"), .{});
259 defer std.debug.print("\n", .{});
260
261 for (adds.items) |it| {
262 std.debug.print("- {s}\n", .{it});
263 }
264 }
265
266 if (rems.items.len > 0) {
267 std.debug.print(comptime ansi.color.Faint("Removed packages:\n"), .{});
268 defer std.debug.print("\n", .{});
269
270 for (rems.items) |it| {
271 std.debug.print("- {s}\n", .{it});
272 }
273 }
274
275 if (changes.unmanaged.size > 0) std.debug.print(comptime ansi.color.Faint("Updated packages:\n"), .{});
276 var iter = changes.iterator();
277 while (iter.next()) |it| {
278 if (diff_printchange("git https://github.com", "- {s}/compare/{s}...{s}\n", it)) continue;
279 if (diff_printchange("git https://gitlab.com", "- {s}/-/compare/{s}...{s}\n", it)) continue;
280 if (diff_printchange("git https://gitea.com", "- {s}/compare/{s}...{s}\n", it)) continue;
281
282 std.debug.print("- {s}\n", .{it.key_ptr.*});
283 std.debug.print(" - {s} ... {s}\n", .{ it.value_ptr.from, it.value_ptr.to });
284 }
285 }
286}
287
288fn diff_printchange(comptime testt: string, comptime replacement: string, item: std.StringHashMap(DiffChange).Entry) bool {
289 if (std.mem.startsWith(u8, item.key_ptr.*, testt)) {
290 if (std.mem.eql(u8, item.value_ptr.from, item.value_ptr.to)) return true;
291 std.debug.print(replacement, .{ item.key_ptr.*[4..], item.value_ptr.from, item.value_ptr.to });
292 return true;
293 }
294 return false;
295}
296
297fn print_dirs(w: nfs.File, list: []const zigmod.Module, alloc: std.mem.Allocator) !void {
298 for (list) |mod| {
299 if (mod.type == .system_lib or mod.type == .framework) continue;
300 if (std.mem.eql(u8, &mod.id, &zigmod.Module.ROOT)) {
301 try w.writeAll(" pub const _root = \"\";\n");
302 continue;
303 }
304 if (std.mem.eql(u8, mod.clean_path, "../..")) {
305 const cwd_realpath = try nfs.cwd().realpathAlloc(alloc, ".");
306 try w.print(" pub const _{s} = \"{}\";\n", .{ mod.short_id(), u.altStringEscape(cwd_realpath) });
307 continue;
308 }
309 try w.print(" pub const _{s} = cache ++ \"/{}\";\n", .{ mod.short_id(), u.altStringEscape(mod.clean_path) });
310 }
311}
312
313fn print_deps(w: nfs.File, m: zigmod.Module) !void {
314 try w.writeAll("&[_]*const Package{\n");
315 for (m.deps) |d| {
316 if (d.main.len == 0) {
317 continue;
318 }
319 if (d.for_build) {
320 continue;
321 }
322 try w.print(" &package_data._{s},\n", .{d.id[0..12]});
323 }
324 try w.writeAll("}");
325}
326
327fn print_pkg_data_to(w: nfs.File, notdone: *std.array_list.Managed(zigmod.Module), done: *std.array_list.Managed(zigmod.Module)) !void {
328 var len: usize = notdone.items.len;
329 while (notdone.items.len > 0) {
330 for (notdone.items, 0..) |mod, i| {
331 if (contains_all(mod.deps, done.items)) {
332 try w.print(
333 \\ pub const _{s} = Package{{
334 \\ .id = "{s}",
335 \\ .directory = dirs._{s},
336 \\
337 , .{
338 mod.short_id(),
339 mod.short_id(),
340 mod.short_id(),
341 });
342 if (mod.main.len > 0 and !std.mem.eql(u8, &mod.id, &zigmod.Module.ROOT)) {
343 try w.print(
344 \\ .import = .{{ "{s}", .{{ .cwd_relative = dirs._{s} ++ "/{s}" }} }},
345 \\
346 , .{
347 mod.name,
348 mod.short_id(),
349 mod.main,
350 });
351 }
352 {
353 try w.writeAll(" .dependencies =");
354 try w.writeAll(" &.{");
355 for (mod.deps, 0..) |moddep, j| {
356 if (moddep.type == .system_lib) continue;
357 if (moddep.type == .framework) continue;
358 try w.print(" &_{s}", .{moddep.id[0..12]});
359 if (j != mod.deps.len - 1) try w.writeAll(",");
360 }
361 try w.writeAll(" },\n");
362 }
363 if (mod.c_include_dirs.len > 0) {
364 try w.writeAll(" .c_include_dirs = &.{");
365 for (mod.c_include_dirs, 0..) |item, j| {
366 try w.print(" \"{}\"", .{u.altStringEscape(item)});
367 if (j != mod.c_include_dirs.len - 1) try w.writeAll(",");
368 }
369 try w.writeAll(" },\n");
370 }
371 if (mod.c_source_files.len > 0) {
372 try w.writeAll(" .c_source_files = &.{");
373 for (mod.c_source_files, 0..) |item, j| {
374 try w.print(" \"{}\"", .{u.altStringEscape(item)});
375 if (j != mod.c_source_files.len - 1) try w.writeAll(",");
376 }
377 try w.writeAll(" },\n");
378 }
379 if (mod.c_source_flags.len > 0) {
380 try w.writeAll(" .c_source_flags = &.{");
381 for (mod.c_source_flags, 0..) |item, j| {
382 try w.print(" \"{}\"", .{u.altStringEscape(item)});
383 if (j != mod.c_source_flags.len - 1) try w.writeAll(",");
384 }
385 try w.writeAll(" },\n");
386 }
387 if (mod.has_syslib_deps()) {
388 try w.writeAll(" .system_libs = &.{");
389 for (mod.deps, 0..) |item, j| {
390 if (!(item.type == .system_lib)) continue;
391 try w.print(" \"{}\"", .{u.altStringEscape(item.name)});
392 if (j != mod.deps.len - 1) try w.writeAll(",");
393 }
394 try w.writeAll(" },\n");
395 }
396 if (mod.has_framework_deps()) {
397 try w.writeAll(" .frameworks = &.{");
398 for (mod.deps, 0..) |item, j| {
399 if (!(item.type == .system_lib)) continue;
400 try w.print(" \"{}\"", .{u.altStringEscape(item.name)});
401 if (j != mod.deps.len - 1) try w.writeAll(",");
402 }
403 try w.writeAll(" },\n");
404 }
405 try w.writeAll(" };\n");
406
407 try done.append(mod);
408 _ = notdone.orderedRemove(i);
409 break;
410 }
411 }
412 if (notdone.items.len == len) {
413 u.fail("notdone still has {d} items", .{len});
414 }
415 len = notdone.items.len;
416 }
417}
418
419/// returns if all of the zig modules in needles are in haystack
420fn contains_all(needles: []zigmod.Module, haystack: []const zigmod.Module) bool {
421 for (needles) |item| {
422 if (item.main.len > 0 and !extras.containsAggregate(zigmod.Module, haystack, item)) {
423 return false;
424 }
425 }
426 return true;
427}
428
429fn print_pkgs(alloc: std.mem.Allocator, w: nfs.File, m: zigmod.Module) !void {
430 try w.writeAll("struct {\n");
431 for (m.deps) |d| {
432 if (d.main.len == 0) {
433 continue;
434 }
435 if (d.for_build) {
436 continue;
437 }
438 const ident = try zig_name_from_pkg_name(alloc, d.name);
439 try w.print(" pub const {s} = &package_data._{s};\n", .{ ident, d.id[0..12] });
440 }
441 try w.writeAll("}");
442}
443
444fn print_imports(alloc: std.mem.Allocator, w: nfs.File, m: zigmod.Module, path: string) !void {
445 for (m.deps) |d| {
446 if (d.main.len == 0) {
447 continue;
448 }
449 if (!d.for_build) {
450 continue;
451 }
452 const ident = try zig_name_from_pkg_name(alloc, d.name);
453 try w.print(" pub const {s} = @import(\"{}/{}/{s}\");\n", .{ ident, u.altStringEscape(path), u.altStringEscape(d.clean_path), d.main });
454 }
455}
456
457fn zig_name_from_pkg_name(alloc: std.mem.Allocator, name: string) !string {
458 var legal = name;
459 legal = try std.mem.replaceOwned(u8, alloc, legal, "-", "_");
460 legal = try std.mem.replaceOwned(u8, alloc, legal, "/", "_");
461 legal = try std.mem.replaceOwned(u8, alloc, legal, ".", "_");
462 return legal;
463}