MIRROR: javascript for 馃悳's, a tiny runtime with big ambitions
1const std = @import("std");
2const builtin = @import("builtin");
3
4pub const cli = @import("cli.zig");
5pub const lockfile = @import("lockfile.zig");
6pub const cache = @import("cache.zig");
7pub const fetcher = @import("fetcher.zig");
8pub const extractor = @import("extractor.zig");
9pub const linker = @import("linker.zig");
10pub const resolver = @import("resolver.zig");
11pub const intern = @import("intern.zig");
12pub const json = @import("json.zig");
13pub const debug = @import("debug.zig");
14
15const global_allocator: std.mem.Allocator = std.heap.c_allocator;
16
17fn getHomeDir(allocator: std.mem.Allocator) ![]const u8 {
18 if (builtin.os.tag == .windows) {
19 const home_w = std.process.getenvW(
20 std.unicode.utf8ToUtf16LeStringLiteral("USERPROFILE")
21 ) orelse return error.NoHomeDir;
22 return std.unicode.utf16LeToUtf8Alloc(allocator, home_w) catch error.NoHomeDir;
23 }
24 const home = std.posix.getenv("HOME") orelse return error.NoHomeDir;
25 return allocator.dupe(u8, home);
26}
27
28fn getAbsoluteEnv(name: [:0]const u8) ?[]const u8 {
29 if (builtin.os.tag == .windows) return null;
30 const value = std.posix.getenv(name) orelse return null;
31 if (value.len == 0 or !std.fs.path.isAbsolute(value)) return null;
32 return value;
33}
34
35fn getLegacyAntDirIfExists(allocator: std.mem.Allocator) !?[]const u8 {
36 const home = try getHomeDir(allocator);
37 defer allocator.free(home);
38
39 const dir = try std.fmt.allocPrint(allocator, "{s}/.ant", .{home});
40 std.fs.cwd().access(dir, .{}) catch {
41 allocator.free(dir);
42 return null;
43 };
44 return dir;
45}
46
47pub const PkgError = enum(c_int) {
48 ok = 0,
49 out_of_memory = -1,
50 invalid_lockfile = -2,
51 io_error = -3,
52 network_error = -4,
53 cache_error = -5,
54 extract_error = -6,
55 resolve_error = -7,
56 invalid_argument = -8,
57 not_found = -9,
58 integrity_mismatch = -10,
59};
60
61pub const ProgressCallback = ?*const fn (
62 user_data: ?*anyopaque,
63 phase: Phase,
64 current: u32,
65 total: u32,
66 message: [*:0]const u8,
67) callconv(.c) void;
68
69pub const Phase = enum(c_int) {
70 resolving = 0,
71 fetching = 1,
72 extracting = 2,
73 linking = 3,
74 caching = 4,
75 postinstall = 5,
76};
77
78pub const PkgOptions = extern struct {
79 cache_dir: ?[*:0]const u8 = null,
80 registry_url: ?[*:0]const u8 = null,
81 max_connections: u32 = 6,
82 progress_callback: ProgressCallback = null,
83 user_data: ?*anyopaque = null,
84 verbose: bool = false,
85};
86
87pub const CacheStats = extern struct {
88 total_size: u64,
89 db_size: u64,
90 package_count: u32,
91};
92
93pub const AddedPackage = extern struct {
94 name: [*:0]const u8,
95 version: [*:0]const u8,
96 direct: bool,
97};
98
99pub const InstallResult = extern struct {
100 package_count: u32,
101 cache_hits: u32,
102 cache_misses: u32,
103 files_linked: u32,
104 files_copied: u32,
105 packages_installed: u32,
106 packages_skipped: u32,
107 elapsed_ms: u64,
108};
109
110pub const LifecycleScript = extern struct {
111 name: [*:0]const u8,
112 script: [*:0]const u8,
113};
114
115const PkgInfo = extern struct {
116 name: [*:0]const u8,
117 version: [*:0]const u8,
118 description: [*:0]const u8,
119 license: [*:0]const u8,
120 homepage: [*:0]const u8,
121 tarball: [*:0]const u8,
122 shasum: [*:0]const u8,
123 integrity: [*:0]const u8,
124 keywords: [*:0]const u8,
125 published: [*:0]const u8,
126 dep_count: u32,
127 version_count: u32,
128 unpacked_size: u64,
129};
130
131const DistTag = extern struct {
132 tag: [*:0]const u8,
133 version: [*:0]const u8,
134};
135
136const Maintainer = extern struct {
137 name: [*:0]const u8,
138 email: [*:0]const u8,
139};
140
141const Dependency = extern struct {
142 name: [*:0]const u8,
143 version: [*:0]const u8,
144};
145
146pub const PkgContext = struct {
147 allocator: std.mem.Allocator,
148 arena_state: std.heap.ArenaAllocator,
149 string_pool: intern.StringPool,
150 cache_db: ?*cache.CacheDB,
151 http: ?*fetcher.Fetcher,
152 options: PkgOptions,
153 last_error: ?[:0]u8,
154 cache_dir: []const u8,
155 metadata_cache: std.StringHashMap(resolver.PackageMetadata),
156 last_install_result: InstallResult,
157 added_packages: std.ArrayListUnmanaged(AddedPackage),
158 added_packages_storage: std.ArrayListUnmanaged([:0]u8),
159 lifecycle_scripts: std.ArrayListUnmanaged(LifecycleScript),
160 lifecycle_scripts_storage: std.ArrayListUnmanaged([:0]u8),
161 info_dist_tags: std.ArrayListUnmanaged(DistTag),
162 info_maintainers: std.ArrayListUnmanaged(Maintainer),
163 info_dependencies: std.ArrayListUnmanaged(Dependency),
164 info_storage: std.ArrayListUnmanaged([:0]u8),
165
166 pub fn init(allocator: std.mem.Allocator, options: PkgOptions) !*PkgContext {
167 const ctx = try allocator.create(PkgContext);
168 errdefer allocator.destroy(ctx);
169
170 const default_cache_path = if (options.cache_dir == null)
171 try getDefaultCacheDir(allocator) else null;
172
173 defer { if (default_cache_path) |path| allocator.free(path); }
174 const cache_path = if (options.cache_dir) |dir| std.mem.span(dir) else default_cache_path.?;
175
176 ctx.* = .{
177 .allocator = allocator,
178 .arena_state = std.heap.ArenaAllocator.init(allocator),
179 .string_pool = intern.StringPool.init(allocator),
180 .cache_db = null,
181 .http = null,
182 .options = options,
183 .last_error = null,
184 .cache_dir = try allocator.dupe(u8, cache_path),
185 .metadata_cache = std.StringHashMap(resolver.PackageMetadata).init(allocator),
186 .last_install_result = .{
187 .package_count = 0,
188 .cache_hits = 0,
189 .cache_misses = 0,
190 .files_linked = 0,
191 .files_copied = 0,
192 .packages_installed = 0,
193 .packages_skipped = 0,
194 .elapsed_ms = 0
195 },
196 .added_packages = .{},
197 .added_packages_storage = .{},
198 .lifecycle_scripts = .{},
199 .lifecycle_scripts_storage = .{},
200 .info_dist_tags = .{},
201 .info_maintainers = .{},
202 .info_dependencies = .{},
203 .info_storage = .{},
204 };
205
206 debug.enabled = options.verbose;
207 debug.log("init: cache_dir={s}", .{ctx.cache_dir});
208 ctx.cache_db = cache.CacheDB.open(ctx.cache_dir) catch |err| {
209 ctx.setErrorFmt("Failed to open cache database: {}", .{err});
210 return error.CacheError;
211 };
212
213 debug.log("init: cache database opened", .{});
214 const registry = if (options.registry_url) |url|
215 std.mem.span(url)
216 else
217 "registry.npmjs.org";
218
219 ctx.http = fetcher.Fetcher.init(allocator, registry) catch |err| {
220 ctx.setErrorFmt("Failed to initialize fetcher: {}", .{err});
221 return error.NetworkError;
222 };
223 debug.log("init: http fetcher ready, registry={s}", .{registry});
224
225 return ctx;
226 }
227
228 pub fn deinit(self: *PkgContext) void {
229 if (self.http) |h| h.deinit();
230 if (self.cache_db) |db| db.close();
231 var meta_iter = self.metadata_cache.valueIterator();
232 while (meta_iter.next()) |meta| meta.deinit();
233 self.metadata_cache.deinit();
234 self.string_pool.deinit();
235 self.arena_state.deinit();
236 if (self.last_error) |e| self.allocator.free(e);
237 for (self.added_packages_storage.items) |s| self.allocator.free(s);
238 self.added_packages_storage.deinit(self.allocator);
239 self.added_packages.deinit(self.allocator);
240 for (self.lifecycle_scripts_storage.items) |s| self.allocator.free(s);
241 self.lifecycle_scripts_storage.deinit(self.allocator);
242 self.lifecycle_scripts.deinit(self.allocator);
243 for (self.info_storage.items) |s| self.allocator.free(s);
244 self.info_storage.deinit(self.allocator);
245 self.info_dist_tags.deinit(self.allocator);
246 self.info_maintainers.deinit(self.allocator);
247 self.info_dependencies.deinit(self.allocator);
248 self.allocator.free(self.cache_dir);
249 self.allocator.destroy(self);
250 }
251
252 pub fn setErrorFmt(self: *PkgContext, comptime fmt: []const u8, args: anytype) void {
253 if (self.last_error) |e| self.allocator.free(e);
254 self.last_error = std.fmt.allocPrintSentinel(self.allocator, fmt, args, 0) catch null;
255 }
256
257 pub fn setError(self: *PkgContext, msg: []const u8) void {
258 if (self.last_error) |e| self.allocator.free(e);
259 self.last_error = self.allocator.dupeZ(u8, msg) catch null;
260 }
261
262 fn getDefaultCacheDir(allocator: std.mem.Allocator) ![]const u8 {
263 if (builtin.os.tag != .windows) {
264 if (try getLegacyAntDirIfExists(allocator)) |dir| {
265 defer allocator.free(dir);
266 return std.fmt.allocPrint(allocator, "{s}/pkg", .{dir});
267 }
268 if (getAbsoluteEnv("XDG_CACHE_HOME")) |base| {
269 return std.fmt.allocPrint(allocator, "{s}/ant/pkg", .{base});
270 }
271 const home = try getHomeDir(allocator);
272 defer allocator.free(home);
273 return std.fmt.allocPrint(allocator, "{s}/.cache/ant/pkg", .{home});
274 }
275
276 const home = try getHomeDir(allocator);
277 defer allocator.free(home);
278 return std.fmt.allocPrint(allocator, "{s}/.ant/pkg", .{home});
279 }
280
281 fn reportProgress(self: *PkgContext, phase: Phase, current: u32, total: u32, message: [:0]const u8) void {
282 if (self.options.progress_callback) |cb| {
283 cb(self.options.user_data, phase, current, total, message.ptr);
284 }
285 }
286
287 fn clearAddedPackages(self: *PkgContext) void {
288 for (self.added_packages_storage.items) |s| self.allocator.free(s);
289 self.added_packages_storage.clearRetainingCapacity();
290 self.added_packages.clearRetainingCapacity();
291 }
292
293 fn clearLifecycleScripts(self: *PkgContext) void {
294 for (self.lifecycle_scripts_storage.items) |s| self.allocator.free(s);
295 self.lifecycle_scripts_storage.clearRetainingCapacity();
296 self.lifecycle_scripts.clearRetainingCapacity();
297 }
298
299 fn clearInfo(self: *PkgContext) void {
300 for (self.info_storage.items) |s| self.allocator.free(s);
301 self.info_storage.clearRetainingCapacity();
302 self.info_dist_tags.clearRetainingCapacity();
303 self.info_maintainers.clearRetainingCapacity();
304 self.info_dependencies.clearRetainingCapacity();
305 }
306
307 fn storeInfoString(self: *PkgContext, str: []const u8) ![*:0]const u8 {
308 const z = try self.allocator.dupeZ(u8, str);
309 try self.info_storage.append(self.allocator, z);
310 return z.ptr;
311 }
312
313 fn addLifecycleScript(self: *PkgContext, name: []const u8, script: []const u8) !void {
314 const name_z = try self.allocator.dupeZ(u8, name);
315 errdefer self.allocator.free(name_z);
316 const script_z = try self.allocator.dupeZ(u8, script);
317 errdefer self.allocator.free(script_z);
318
319 try self.lifecycle_scripts_storage.append(self.allocator, name_z);
320 try self.lifecycle_scripts_storage.append(self.allocator, script_z);
321 try self.lifecycle_scripts.append(self.allocator, .{
322 .name = name_z.ptr,
323 .script = script_z.ptr,
324 });
325 }
326
327 fn addPackageToResults(self: *PkgContext, name: []const u8, version: []const u8, direct: bool) !void {
328 for (self.added_packages.items) |pkg| {
329 if (std.mem.eql(u8, std.mem.span(pkg.name), name)) return;
330 }
331
332 const name_z = try self.allocator.dupeZ(u8, name);
333 errdefer self.allocator.free(name_z);
334 const version_z = try self.allocator.dupeZ(u8, version);
335 errdefer self.allocator.free(version_z);
336
337 try self.added_packages_storage.append(self.allocator, name_z);
338 try self.added_packages_storage.append(self.allocator, version_z);
339 try self.added_packages.append(self.allocator, .{
340 .name = name_z.ptr,
341 .version = version_z.ptr,
342 .direct = direct,
343 });
344 }
345
346 pub fn install(self: *PkgContext, lockfile_path: []const u8, node_modules_path: []const u8) !void {
347 _ = self.arena_state.reset(.retain_capacity);
348 const arena_alloc = self.arena_state.allocator();
349
350 self.clearAddedPackages();
351
352 var timer = std.time.Timer.start() catch return error.OutOfMemory;
353 var stage_start: u64 = @intCast(std.time.nanoTimestamp());
354
355 debug.log("install start: lockfile={s} node_modules={s}", .{ lockfile_path, node_modules_path });
356
357 var lf = lockfile.Lockfile.open(lockfile_path) catch {
358 self.setError("Failed to open lockfile");
359 return error.InvalidLockfile;
360 };
361 defer lf.close();
362
363 const pkg_count = lf.header.package_count;
364 stage_start = debug.timer("lockfile open", stage_start);
365 debug.log(" packages in lockfile: {d}", .{pkg_count});
366
367 var integrities = try arena_alloc.alloc([64]u8, pkg_count);
368 for (lf.packages, 0..) |pkg, i| {
369 integrities[i] = pkg.integrity;
370 }
371
372 const db = self.cache_db orelse return error.CacheError;
373 var cache_hits = try db.batchLookup(integrities, arena_alloc);
374 defer cache_hits.deinit();
375 stage_start = debug.timer("cache lookup", stage_start);
376
377 var hit_set = std.AutoHashMap(u32, u32).init(arena_alloc);
378 for (cache_hits.items) |hit| {
379 try hit_set.put(hit.index, hit.file_count);
380 }
381
382 var misses = std.ArrayListUnmanaged(u32){};
383 for (0..pkg_count) |i| {
384 if (!hit_set.contains(@intCast(i))) {
385 try misses.append(arena_alloc, @intCast(i));
386 }
387 }
388 debug.log(" cache hits: {d}, misses: {d}", .{ cache_hits.items.len, misses.items.len });
389
390 std.sort.heap(cache.CacheDB.BatchHit, cache_hits.items, &lf, batchHitLessThanParentFirst);
391
392 var pkg_linker = linker.Linker.init(self.allocator);
393 defer pkg_linker.deinit();
394 try pkg_linker.setNodeModulesPath(node_modules_path);
395
396 for (cache_hits.items, 0..) |hit, i| {
397 const pkg = &lf.packages[hit.index];
398 const pkg_name = pkg.name.slice(lf.string_table);
399 const cache_path = try db.getPackagePath(&pkg.integrity, arena_alloc);
400 const parent_path = pkg.parent_path.slice(lf.string_table);
401
402 const msg = std.fmt.allocPrintSentinel(arena_alloc, "{s}", .{pkg_name}, 0) catch continue;
403 self.reportProgress(.linking, @intCast(i), @intCast(cache_hits.items.len), msg);
404
405 try pkg_linker.linkPackage(.{
406 .cache_path = cache_path,
407 .node_modules_path = node_modules_path,
408 .name = pkg_name,
409 .parent_path = if (parent_path.len > 0) parent_path else null,
410 .file_count = hit.file_count,
411 .has_bin = pkg.flags.has_bin,
412 });
413
414 if (pkg.flags.direct) {
415 const version_str = pkg.versionString(arena_alloc, lf.string_table) catch continue;
416 self.addPackageToResults(pkg_name, version_str, true) catch {};
417 }
418 }
419 stage_start = debug.timer("link cache hits", stage_start);
420
421 if (misses.items.len > 0) {
422 const http = self.http orelse return error.NetworkError;
423 const PkgExtractCtx = struct {
424 ext: *extractor.Extractor,
425 pkg_idx: u32,
426 integrity: [64]u8,
427 cache_path: []const u8,
428 pkg_name: []const u8,
429 version_str: []const u8,
430 direct: bool,
431 parent_path: ?[]const u8,
432 has_bin: bool,
433 completed: bool,
434 has_error: bool,
435 };
436
437 var extract_contexts = try arena_alloc.alloc(PkgExtractCtx, misses.items.len);
438 var valid_count: usize = 0;
439
440 debug.log("queuing {d} tarball fetches...", .{misses.items.len});
441
442 for (misses.items, 0..) |pkg_idx, i| {
443 const pkg = &lf.packages[pkg_idx];
444 const pkg_name = pkg.name.slice(lf.string_table);
445 const tarball_url = pkg.tarball_url.slice(lf.string_table);
446 const version_str = pkg.versionString(arena_alloc, lf.string_table) catch continue;
447 const cache_path = db.getPackagePath(&pkg.integrity, arena_alloc) catch continue;
448
449 const msg = std.fmt.allocPrintSentinel(arena_alloc, "{s}", .{pkg_name}, 0) catch continue;
450 self.reportProgress(.fetching, @intCast(i), @intCast(misses.items.len), msg);
451
452 if (self.options.verbose) {
453 debug.log(" queue: {s}@{s}", .{ pkg_name, version_str });
454 }
455
456 const ext = extractor.Extractor.init(self.allocator, cache_path) catch continue;
457 const parent_path_str = pkg.parent_path.slice(lf.string_table);
458
459 extract_contexts[valid_count] = .{
460 .ext = ext,
461 .pkg_idx = pkg_idx,
462 .integrity = pkg.integrity,
463 .cache_path = cache_path,
464 .pkg_name = pkg_name,
465 .version_str = version_str,
466 .direct = pkg.flags.direct,
467 .parent_path = if (parent_path_str.len > 0) parent_path_str else null,
468 .has_bin = pkg.flags.has_bin,
469 .completed = false,
470 .has_error = false,
471 };
472
473 http.fetchTarball(tarball_url, fetcher.StreamHandler.init(
474 struct {
475 fn onData(data: []const u8, user_data: ?*anyopaque) void {
476 const ctx: *PkgExtractCtx = @ptrCast(@alignCast(user_data));
477 ctx.ext.feedCompressed(data) catch {
478 ctx.has_error = true;
479 };
480 }
481 }.onData,
482 struct {
483 fn onComplete(_: u16, user_data: ?*anyopaque) void {
484 const ctx: *PkgExtractCtx = @ptrCast(@alignCast(user_data));
485 ctx.completed = true;
486 }
487 }.onComplete,
488 struct {
489 fn onError(_: fetcher.FetchError, user_data: ?*anyopaque) void {
490 const ctx: *PkgExtractCtx = @ptrCast(@alignCast(user_data));
491 ctx.has_error = true;
492 ctx.completed = true;
493 }
494 }.onError,
495 &extract_contexts[valid_count],
496 )) catch continue;
497
498 valid_count += 1;
499 }
500
501 stage_start = debug.timer("queue fetches", stage_start);
502 debug.log("running event loop for {d} fetches...", .{valid_count});
503
504 http.run() catch {};
505 stage_start = debug.timer("fetch + extract", stage_start);
506
507 var success_count: usize = 0;
508 var error_count: usize = 0;
509 std.sort.heap(PkgExtractCtx, extract_contexts[0..valid_count], {}, struct {
510 fn lessThan(_: void, a: PkgExtractCtx, b: PkgExtractCtx) bool {
511 const depth_a = linkPathDepth(a.parent_path);
512 const depth_b = linkPathDepth(b.parent_path);
513 if (depth_a != depth_b) return depth_a < depth_b;
514
515 const parent_a = a.parent_path orelse "";
516 const parent_b = b.parent_path orelse "";
517 const parent_order = std.mem.order(u8, parent_a, parent_b);
518 if (parent_order != .eq) return parent_order == .lt;
519 return std.mem.order(u8, a.pkg_name, b.pkg_name) == .lt;
520 }
521 }.lessThan);
522
523 for (extract_contexts[0..valid_count], 0..) |*ctx, i| {
524 defer ctx.ext.deinit();
525
526 if (ctx.has_error) {
527 error_count += 1;
528 debug.log(" error: {s}", .{ctx.pkg_name});
529 continue;
530 }
531 success_count += 1;
532
533 const msg = std.fmt.allocPrintSentinel(arena_alloc, "{s}", .{ctx.pkg_name}, 0) catch continue;
534 self.reportProgress(.linking, @intCast(i), @intCast(valid_count), msg);
535
536 const stats = ctx.ext.stats();
537 if (stats.files >= 100) {
538 debug.log(" extracted {s}: {d} files, {d} bytes", .{ ctx.pkg_name, stats.files, stats.bytes });
539 }
540
541 db.insert(&.{
542 .integrity = ctx.integrity,
543 .path = ctx.cache_path,
544 .unpacked_size = stats.bytes,
545 .file_count = stats.files,
546 .cached_at = std.time.timestamp(),
547 }, ctx.pkg_name, ctx.version_str) catch continue;
548
549 self.addPackageToResults(ctx.pkg_name, ctx.version_str, ctx.direct) catch {};
550
551 pkg_linker.linkPackage(.{
552 .cache_path = ctx.cache_path,
553 .node_modules_path = node_modules_path,
554 .name = ctx.pkg_name,
555 .parent_path = ctx.parent_path,
556 .file_count = stats.files,
557 .has_bin = ctx.has_bin,
558 }) catch {};
559 }
560 stage_start = debug.timer("cache insert + link misses", stage_start);
561 debug.log(" fetched: {d} success, {d} errors", .{ success_count, error_count });
562 }
563
564 db.sync();
565 stage_start = debug.timer("cache sync", stage_start);
566
567 const link_stats = pkg_linker.getStats();
568 self.last_install_result = .{
569 .package_count = pkg_count,
570 .cache_hits = @intCast(cache_hits.items.len),
571 .cache_misses = @intCast(misses.items.len),
572 .files_linked = link_stats.files_linked,
573 .files_copied = link_stats.files_copied,
574 .packages_installed = link_stats.packages_installed,
575 .packages_skipped = link_stats.packages_skipped,
576 .elapsed_ms = timer.read() / 1_000_000,
577 };
578 }
579};
580
581fn trySetHttpResolveError(c: *PkgContext, err: anyerror) bool {
582 if (err != error.ResponseError) return false;
583
584 const http = c.http orelse return false;
585 const info = http.getLastHttpError() orelse return false;
586
587 c.setErrorFmt("error: GET {s} - {d}", .{ info.url, info.status });
588 return true;
589}
590
591fn setResolveError(c: *PkgContext, target: ?[]const u8, err: anyerror) void {
592 if (trySetHttpResolveError(c, err)) return;
593
594 if (target) |name| c.setErrorFmt("Failed to resolve {s}: {}", .{ name, err })
595 else c.setErrorFmt("Failed to resolve dependencies: {}", .{ err });
596}
597
598fn linkPathDepth(parent_path: ?[]const u8) u32 {
599 const parent = parent_path orelse return 0;
600 if (parent.len == 0) return 0;
601
602 var depth: u32 = 1;
603 var it: usize = 0;
604 while (std.mem.indexOfPos(u8, parent, it, "/node_modules/")) |idx| {
605 depth += 1;
606 it = idx + "/node_modules/".len;
607 }
608 return depth;
609}
610
611fn packageLinkLessThanParentFirst(_: void, a: linker.PackageLink, b: linker.PackageLink) bool {
612 const a_depth = linkPathDepth(a.parent_path);
613 const b_depth = linkPathDepth(b.parent_path);
614 if (a_depth != b_depth) return a_depth < b_depth;
615
616 const a_parent = a.parent_path orelse "";
617 const b_parent = b.parent_path orelse "";
618 const parent_order = std.mem.order(u8, a_parent, b_parent);
619 if (parent_order != .eq) return parent_order == .lt;
620
621 return std.mem.order(u8, a.name, b.name) == .lt;
622}
623
624fn batchHitLessThanParentFirst(lf: *const lockfile.Lockfile, a: cache.CacheDB.BatchHit, b: cache.CacheDB.BatchHit) bool {
625 const pkg_a = &lf.packages[a.index];
626 const pkg_b = &lf.packages[b.index];
627
628 const parent_a = pkg_a.parent_path.slice(lf.string_table);
629 const parent_b = pkg_b.parent_path.slice(lf.string_table);
630 const depth_a = linkPathDepth(if (parent_a.len > 0) parent_a else null);
631 const depth_b = linkPathDepth(if (parent_b.len > 0) parent_b else null);
632 if (depth_a != depth_b) return depth_a < depth_b;
633
634 const name_a = pkg_a.name.slice(lf.string_table);
635 const name_b = pkg_b.name.slice(lf.string_table);
636 const parent_order = std.mem.order(u8, parent_a, parent_b);
637 if (parent_order != .eq) return parent_order == .lt;
638 return std.mem.order(u8, name_a, name_b) == .lt;
639}
640
641export fn pkg_init(options: *const PkgOptions) ?*PkgContext {
642 return PkgContext.init(global_allocator, options.*) catch null;
643}
644
645export fn pkg_free(ctx: ?*PkgContext) void {
646 if (ctx) |c| c.deinit();
647}
648
649export fn pkg_install(
650 ctx: ?*PkgContext,
651 package_json_path: [*:0]const u8,
652 lockfile_path: [*:0]const u8,
653 node_modules_path: [*:0]const u8,
654) PkgError {
655 const c = ctx orelse return .invalid_argument;
656
657 _ = c.arena_state.reset(.retain_capacity);
658 const arena_alloc = c.arena_state.allocator();
659
660 c.install(
661 std.mem.span(lockfile_path),
662 std.mem.span(node_modules_path),
663 ) catch |err| {
664 return switch (err) {
665 error.InvalidLockfile => .invalid_lockfile,
666 error.CacheError => .cache_error,
667 error.NetworkError => .network_error,
668 error.OutOfMemory => .out_of_memory,
669 else => .io_error,
670 };
671 };
672
673 var pkg_json = json.PackageJson.parse(arena_alloc, std.mem.span(package_json_path)) catch return .ok;
674 defer pkg_json.deinit(arena_alloc);
675 if (pkg_json.trusted_dependencies.count() > 0) {
676 runTrustedPostinstall(c, &pkg_json.trusted_dependencies, std.mem.span(node_modules_path), arena_alloc);
677 }
678
679 return .ok;
680}
681
682export fn pkg_get_install_result(ctx: ?*PkgContext, out: *InstallResult) PkgError {
683 const c = ctx orelse return .invalid_argument;
684 out.* = c.last_install_result;
685 return .ok;
686}
687
688export fn pkg_get_added_count(ctx: ?*const PkgContext) u32 {
689 const c = ctx orelse return 0;
690 return @intCast(c.added_packages.items.len);
691}
692
693export fn pkg_get_added_package(ctx: ?*const PkgContext, index: u32, out: *AddedPackage) PkgError {
694 const c = ctx orelse return .invalid_argument;
695 if (index >= c.added_packages.items.len) return .invalid_argument;
696 out.* = c.added_packages.items[index];
697 return .ok;
698}
699
700export fn pkg_get_latest_available_version(
701 ctx: ?*PkgContext,
702 package_name: [*:0]const u8,
703 installed_version: [*:0]const u8,
704 out_version: [*]u8,
705 out_version_len: usize,
706) c_int {
707 const c = ctx orelse return 0;
708 if (out_version_len == 0) return 0;
709 out_version[0] = 0;
710
711 const name = std.mem.span(package_name);
712 const installed_str = std.mem.span(installed_version);
713 const installed = resolver.Version.parse(installed_str) catch return 0;
714 const meta = c.metadata_cache.get(name) orelse return 0;
715
716 var best: ?*const resolver.VersionInfo = null;
717 for (meta.versions.items) |*v| {
718 if (v.version.prerelease != null) continue;
719 if (!v.matchesPlatform()) continue;
720 if (best == null or v.version.order(best.?.version) == .gt) best = v;
721 }
722 if (best == null) {
723 for (meta.versions.items) |*v| {
724 if (!v.matchesPlatform()) continue;
725 if (best == null or v.version.order(best.?.version) == .gt) best = v;
726 }
727 }
728
729 const latest = best orelse return 0;
730 if (latest.version.order(installed) != .gt) return 0;
731
732 if (latest.version_str.len + 1 > out_version_len) return 0;
733 @memcpy(out_version[0..latest.version_str.len], latest.version_str);
734 out_version[latest.version_str.len] = 0;
735 return 1;
736}
737
738export fn pkg_count_installed(node_modules_path: [*:0]const u8) u32 {
739 const nm_path = std.mem.span(node_modules_path);
740 if (!std.mem.endsWith(u8, nm_path, "node_modules")) return 0;
741
742 const base = nm_path[0 .. nm_path.len - "node_modules".len];
743 var buf: [std.fs.max_path_bytes]u8 = undefined;
744 const lp = std.fmt.bufPrint(&buf, "{s}ant.lockb", .{base}) catch return 0;
745
746 var lf = lockfile.Lockfile.open(lp) catch return 0;
747 defer lf.close();
748 return lf.header.package_count;
749}
750
751export fn pkg_discover_lifecycle_scripts(
752 ctx: ?*PkgContext,
753 node_modules_path: [*:0]const u8,
754) PkgError {
755 const c = ctx orelse return .invalid_argument;
756 c.clearLifecycleScripts();
757
758 const nm_path = std.mem.span(node_modules_path);
759 var nm_dir = std.fs.cwd().openDir(nm_path, .{ .iterate = true }) catch return .io_error;
760 defer nm_dir.close();
761
762 var iter = nm_dir.iterate();
763 while (iter.next() catch null) |entry| {
764 if (entry.kind != .directory) continue;
765 if (entry.name[0] == '@') {
766 var scope_dir = nm_dir.openDir(entry.name, .{ .iterate = true }) catch continue;
767 defer scope_dir.close();
768 var scope_iter = scope_dir.iterate();
769 while (scope_iter.next() catch null) |scoped_entry| {
770 if (scoped_entry.kind != .directory) continue;
771 const full_name = std.fmt.allocPrint(c.allocator, "@{s}/{s}", .{ entry.name[1..], scoped_entry.name }) catch continue;
772 defer c.allocator.free(full_name);
773 discoverPackageScript(c, nm_path, full_name, scope_dir, scoped_entry.name);
774 }
775 } else {
776 discoverPackageScript(c, nm_path, entry.name, nm_dir, entry.name);
777 }
778 }
779
780 return .ok;
781}
782
783fn discoverPackageScript(ctx: *PkgContext, nm_path: []const u8, pkg_name: []const u8, parent_dir: std.fs.Dir, dir_name: []const u8) void {
784 var pkg_dir = parent_dir.openDir(dir_name, .{}) catch return;
785 defer pkg_dir.close();
786
787 pkg_dir.access(".postinstall", .{}) catch |err| {
788 if (err != error.FileNotFound) return;
789 const pkg_json = pkg_dir.openFile("package.json", .{}) catch return;
790 defer pkg_json.close();
791
792 const content = pkg_json.readToEndAlloc(ctx.allocator, 1024 * 1024) catch return;
793 defer ctx.allocator.free(content);
794
795 var doc = json.JsonDoc.parse(content) catch return;
796 defer doc.deinit();
797
798 const root = doc.root();
799 if (root.getObject("scripts")) |scripts| {
800 const script = scripts.getString("postinstall") orelse
801 scripts.getString("install") orelse return;
802
803 if (std.mem.eql(u8, pkg_name, "esbuild")) return;
804 ctx.addLifecycleScript(pkg_name, script) catch return;
805 }
806 return;
807 };
808 _ = nm_path;
809}
810
811export fn pkg_get_lifecycle_script_count(ctx: ?*const PkgContext) u32 {
812 const c = ctx orelse return 0;
813 return @intCast(c.lifecycle_scripts.items.len);
814}
815
816export fn pkg_get_lifecycle_script(ctx: ?*const PkgContext, index: u32, out: *LifecycleScript) PkgError {
817 const c = ctx orelse return .invalid_argument;
818 if (index >= c.lifecycle_scripts.items.len) return .invalid_argument;
819 out.* = c.lifecycle_scripts.items[index];
820 return .ok;
821}
822
823export fn pkg_run_postinstall(
824 ctx: ?*PkgContext,
825 node_modules_path: [*:0]const u8,
826 package_names: [*]const [*:0]const u8,
827 count: u32,
828) PkgError {
829 const c = ctx orelse return .invalid_argument;
830 _ = c.arena_state.reset(.retain_capacity);
831 const arena_alloc = c.arena_state.allocator();
832
833 var trusted = std.StringHashMap(void).init(arena_alloc);
834 for (0..count) |i| {
835 trusted.put(std.mem.span(package_names[i]), {}) catch continue;
836 }
837
838 runTrustedPostinstall(c, &trusted, std.mem.span(node_modules_path), arena_alloc);
839 return .ok;
840}
841
842export fn pkg_add_trusted_dependencies(
843 package_json_path: [*:0]const u8,
844 package_names: [*]const [*:0]const u8,
845 count: u32,
846) PkgError {
847 const allocator = std.heap.c_allocator;
848 const path = std.mem.span(package_json_path);
849 const path_z = package_json_path;
850
851 debug.log("[trust] pkg_add_trusted_dependencies: path={s} count={d}", .{ path, count });
852
853 const file = std.fs.cwd().openFile(path, .{ .mode = .read_write }) catch |err| {
854 debug.log("[trust] failed to open file: {}", .{err});
855 return .io_error;
856 };
857 defer file.close();
858
859 const content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch |err| {
860 debug.log("[trust] failed to read file: {}", .{err});
861 return .io_error;
862 };
863 defer allocator.free(content);
864
865 debug.log("[trust] read {d} bytes from package.json", .{content.len});
866
867 const doc = json.yyjson.yyjson_read(content.ptr, content.len, 0);
868 if (doc == null) {
869 debug.log("[trust] failed to parse JSON", .{});
870 return .io_error;
871 }
872 defer json.yyjson.yyjson_doc_free(doc);
873
874 const mdoc = json.yyjson.yyjson_doc_mut_copy(doc, null);
875 if (mdoc == null) {
876 debug.log("[trust] failed to create mutable doc", .{});
877 return .out_of_memory;
878 }
879 defer json.yyjson.yyjson_mut_doc_free(mdoc);
880
881 const root = json.yyjson.yyjson_mut_doc_get_root(mdoc);
882 if (root == null) {
883 debug.log("[trust] failed to get root", .{});
884 return .io_error;
885 }
886
887 var trusted_arr = json.yyjson.yyjson_mut_obj_get(root, "trustedDependencies");
888 if (trusted_arr == null) {
889 debug.log("[trust] creating new trustedDependencies array", .{});
890 trusted_arr = json.yyjson.yyjson_mut_arr(mdoc);
891 if (trusted_arr == null) {
892 debug.log("[trust] failed to create array", .{});
893 return .out_of_memory;
894 }
895 _ = json.yyjson.yyjson_mut_obj_add_val(mdoc, root, "trustedDependencies", trusted_arr);
896 } else {
897 debug.log("[trust] trustedDependencies array already exists", .{});
898 }
899
900 var string_copies = std.ArrayListUnmanaged([:0]u8){};
901 defer {
902 for (string_copies.items) |s| allocator.free(s);
903 string_copies.deinit(allocator);
904 }
905
906 var added: u32 = 0;
907 for (0..count) |i| {
908 const pkg_name = std.mem.span(package_names[i]);
909 var exists = false;
910
911 var iter = json.yyjson.yyjson_mut_arr_iter{};
912 _ = json.yyjson.yyjson_mut_arr_iter_init(trusted_arr, &iter);
913 while (json.yyjson.yyjson_mut_arr_iter_next(&iter)) |val| {
914 if (json.yyjson.yyjson_mut_is_str(val)) {
915 const existing = json.yyjson.yyjson_mut_get_str(val);
916 if (existing != null and std.mem.eql(u8, std.mem.span(existing.?), pkg_name)) {
917 exists = true; break;
918 }
919 }
920 }
921
922 if (!exists) {
923 const name_copy = allocator.dupeZ(u8, pkg_name) catch continue;
924 string_copies.append(allocator, name_copy) catch {
925 allocator.free(name_copy);
926 continue;
927 };
928 const val = json.yyjson.yyjson_mut_str(mdoc, name_copy.ptr);
929 if (val != null) {
930 _ = json.yyjson.yyjson_mut_arr_append(trusted_arr, val);
931 added += 1;
932 debug.log("[trust] added {s}", .{pkg_name});
933 }
934 } else {
935 debug.log("[trust] {s} already in trustedDependencies", .{pkg_name});
936 }
937 }
938 debug.log("[trust] added {d} packages, writing file", .{added});
939
940 var write_err: json.yyjson.yyjson_write_err = undefined;
941 const flags = json.yyjson.YYJSON_WRITE_PRETTY_TWO_SPACES | json.yyjson.YYJSON_WRITE_ESCAPE_UNICODE;
942 const written = json.yyjson.yyjson_mut_write_file(path_z, mdoc, flags, null, &write_err);
943 if (!written) {
944 const msg = if (write_err.msg) |m| std.mem.span(m) else "unknown";
945 debug.log("[trust] failed to write file: code={d} msg={s}", .{ write_err.code, msg });
946 return .io_error;
947 }
948
949 debug.log("[trust] successfully wrote package.json", .{});
950 return .ok;
951}
952
953const InterleavedExtractCtx = struct {
954 ext: *extractor.Extractor,
955 integrity: [64]u8,
956 cache_path: []const u8,
957 pkg_name: []const u8,
958 version_str: []const u8,
959 direct: bool,
960 parent_path: ?[]const u8,
961 has_bin: bool,
962 completed: bool,
963 has_error: bool,
964 queued: bool,
965 parent: *InterleavedContext,
966};
967
968const InterleavedContext = struct {
969 allocator: std.mem.Allocator,
970 arena_alloc: std.mem.Allocator,
971 db: *cache.CacheDB,
972 http: *fetcher.Fetcher,
973 pkg_ctx: *PkgContext,
974 extract_contexts: std.ArrayListUnmanaged(*InterleavedExtractCtx),
975 queued_integrities: std.AutoHashMap([64]u8, void),
976 callbacks_received: usize,
977 integrity_duplicates: usize,
978 cache_hits: usize,
979 tarballs_queued: usize,
980 tarballs_completed: std.atomic.Value(u32),
981
982 fn init(allocator: std.mem.Allocator, arena_alloc: std.mem.Allocator, db: *cache.CacheDB, http: *fetcher.Fetcher, pkg_ctx: *PkgContext) InterleavedContext {
983 return .{
984 .allocator = allocator,
985 .arena_alloc = arena_alloc,
986 .db = db,
987 .http = http,
988 .pkg_ctx = pkg_ctx,
989 .extract_contexts = .{},
990 .queued_integrities = std.AutoHashMap([64]u8, void).init(arena_alloc),
991 .callbacks_received = 0,
992 .integrity_duplicates = 0,
993 .cache_hits = 0,
994 .tarballs_queued = 0,
995 .tarballs_completed = std.atomic.Value(u32).init(0),
996 };
997 }
998
999 fn deinit(self: *InterleavedContext) void {
1000 self.extract_contexts.deinit(self.arena_alloc);
1001 self.queued_integrities.deinit();
1002 }
1003
1004 fn onPackageResolved(pkg: *const resolver.ResolvedPackage, user_data: ?*anyopaque) void {
1005 const self: *InterleavedContext = @ptrCast(@alignCast(user_data));
1006 self.callbacks_received += 1;
1007
1008 const pkg_name = pkg.name.slice();
1009 const current: u32 = @intCast(self.callbacks_received);
1010 const msg = std.fmt.allocPrintSentinel(self.arena_alloc, "{s}", .{pkg_name}, 0) catch return;
1011 self.pkg_ctx.reportProgress(.resolving, current, current, msg);
1012
1013 if (self.queued_integrities.contains(pkg.integrity)) {
1014 self.integrity_duplicates += 1; return;
1015 } self.queued_integrities.put(pkg.integrity, {}) catch return;
1016
1017 if (self.db.hasIntegrity(&pkg.integrity)) {
1018 self.cache_hits += 1; return;
1019 } self.tarballs_queued += 1;
1020
1021 const cache_path = self.db.getPackagePath(&pkg.integrity, self.arena_alloc) catch return;
1022 const version_str = pkg.version.format(self.arena_alloc) catch return;
1023
1024 const ext = extractor.Extractor.init(self.allocator, cache_path) catch return;
1025 const ctx = self.arena_alloc.create(InterleavedExtractCtx) catch {
1026 ext.deinit(); return;
1027 };
1028
1029 ctx.* = .{
1030 .ext = ext,
1031 .integrity = pkg.integrity,
1032 .cache_path = cache_path,
1033 .pkg_name = pkg.name.slice(),
1034 .version_str = version_str,
1035 .direct = pkg.direct,
1036 .parent_path = pkg.parent_path,
1037 .has_bin = pkg.has_bin,
1038 .completed = false,
1039 .has_error = false,
1040 .queued = false,
1041 .parent = self,
1042 };
1043
1044 self.http.fetchTarball(pkg.tarball_url, fetcher.StreamHandler.init(
1045 struct {
1046 fn onData(data: []const u8, ud: ?*anyopaque) void {
1047 const c: *InterleavedExtractCtx = @ptrCast(@alignCast(ud));
1048 if (c.has_error) return;
1049 c.ext.feedCompressed(data) catch { c.has_error = true; };
1050 }
1051 }.onData,
1052 struct {
1053 fn onComplete(_: u16, ud: ?*anyopaque) void {
1054 const c: *InterleavedExtractCtx = @ptrCast(@alignCast(ud));
1055 c.completed = true;
1056
1057 const completed = c.parent.tarballs_completed.fetchAdd(1, .monotonic) + 1;
1058 const total: u32 = @intCast(c.parent.tarballs_queued);
1059
1060 var msg_buf: [256]u8 = undefined;
1061 const msg_len = std.fmt.bufPrint(&msg_buf, "{s}", .{c.pkg_name}) catch return;
1062
1063 msg_buf[msg_len.len] = 0;
1064 c.parent.pkg_ctx.reportProgress(.fetching, completed, total, msg_buf[0..msg_len.len :0]);
1065 }
1066 }.onComplete,
1067 struct {
1068 fn onError(_: fetcher.FetchError, ud: ?*anyopaque) void {
1069 const c: *InterleavedExtractCtx = @ptrCast(@alignCast(ud));
1070 c.has_error = true;
1071 c.completed = true;
1072 }
1073 }.onError,
1074 ctx,
1075 )) catch {
1076 ext.deinit();
1077 self.arena_alloc.destroy(ctx);
1078 return;
1079 };
1080
1081 ctx.queued = true;
1082 self.extract_contexts.append(self.arena_alloc, ctx) catch return;
1083
1084 debug.log(" queued tarball: {s}@{s}", .{ pkg.name.slice(), version_str });
1085 }
1086};
1087
1088export fn pkg_resolve_and_install(
1089 ctx: ?*PkgContext,
1090 package_json_path: [*:0]const u8,
1091 lockfile_path: [*:0]const u8,
1092 node_modules_path: [*:0]const u8,
1093) PkgError {
1094 const c = ctx orelse return .invalid_argument;
1095 _ = c.arena_state.reset(.retain_capacity);
1096 const arena_alloc = c.arena_state.allocator();
1097
1098 var timer = std.time.Timer.start() catch return .out_of_memory;
1099 var stage_start: u64 = @intCast(std.time.nanoTimestamp());
1100
1101 debug.log("resolve+install (interleaved): package_json={s} lockfile={s} node_modules={s}", .{
1102 std.mem.span(package_json_path),
1103 std.mem.span(lockfile_path),
1104 std.mem.span(node_modules_path),
1105 });
1106
1107 const http = c.http orelse return .network_error;
1108 http.resetMetaClients();
1109 const db = c.cache_db orelse return .cache_error;
1110
1111 const pkg_json_path_z = arena_alloc.dupeZ(u8, std.mem.span(package_json_path)) catch return .out_of_memory;
1112 var pkg_json = json.PackageJson.parse(arena_alloc, pkg_json_path_z) catch {
1113 c.setError("Failed to parse package.json");
1114 return .io_error;
1115 }; defer pkg_json.deinit(arena_alloc);
1116
1117 if (pkg_json.trusted_dependencies.count() > 0) {
1118 debug.log(" trusted dependencies: {d}", .{pkg_json.trusted_dependencies.count()});
1119 }
1120
1121 var interleaved = InterleavedContext.init(c.allocator, arena_alloc, db, http, c);
1122 defer interleaved.deinit();
1123
1124 var res = resolver.Resolver.init(
1125 arena_alloc,
1126 c.allocator,
1127 &c.string_pool,
1128 http,
1129 db,
1130 if (c.options.registry_url) |url| std.mem.span(url) else "https://registry.npmjs.org",
1131 &c.metadata_cache,
1132 ); defer res.deinit();
1133
1134 res.setOnPackageResolved(InterleavedContext.onPackageResolved, &interleaved);
1135 res.resolveFromPackageJson(std.mem.span(package_json_path)) catch |err| {
1136 setResolveError(c, null, err);
1137 return .resolve_error;
1138 };
1139
1140 stage_start = debug.timer("resolve + queue tarballs", stage_start);
1141 debug.log(" resolved {d} packages, callbacks={d} (dupes={d}), cache hits={d}, queued={d}", .{
1142 res.resolved.count(),
1143 interleaved.callbacks_received,
1144 interleaved.integrity_duplicates,
1145 interleaved.cache_hits,
1146 interleaved.tarballs_queued,
1147 });
1148
1149 var direct_iter = res.resolved.valueIterator();
1150 while (direct_iter.next()) |pkg_ptr| {
1151 const pkg = pkg_ptr.*;
1152 if (pkg.direct) {
1153 const version_str = pkg.version.format(arena_alloc) catch continue;
1154 c.addPackageToResults(pkg.name.slice(), version_str, true) catch {};
1155 }
1156 }
1157
1158 var pkg_linker = linker.Linker.init(c.allocator);
1159 defer pkg_linker.deinit();
1160 pkg_linker.setNodeModulesPath(std.mem.span(node_modules_path)) catch return .io_error;
1161
1162 var cache_hit_jobs = std.ArrayListUnmanaged(linker.PackageLink){};
1163 var pkg_iter = res.resolved.valueIterator();
1164 while (pkg_iter.next()) |pkg_ptr| {
1165 const pkg = pkg_ptr.*;
1166 if (interleaved.queued_integrities.contains(pkg.integrity)) {
1167 var is_download = false;
1168 for (interleaved.extract_contexts.items) |ext_ctx| {
1169 if (std.mem.eql(u8, &ext_ctx.integrity, &pkg.integrity)) { is_download = true; break; }
1170 }
1171 if (is_download) continue;
1172 }
1173
1174 var cache_entry = db.lookup(&pkg.integrity) orelse continue;
1175 defer cache_entry.deinit();
1176 const cache_path = arena_alloc.dupe(u8, cache_entry.path) catch continue;
1177
1178 cache_hit_jobs.append(arena_alloc, .{
1179 .cache_path = cache_path,
1180 .node_modules_path = std.mem.span(node_modules_path),
1181 .name = pkg.name.slice(),
1182 .parent_path = pkg.parent_path,
1183 .file_count = cache_entry.file_count,
1184 .has_bin = pkg.has_bin,
1185 }) catch continue;
1186 }
1187
1188 var tarball_thread: ?std.Thread = null;
1189 if (interleaved.tarballs_queued > 0) {
1190 debug.log("finishing {d} tarball downloads (pending={d})...", .{
1191 interleaved.tarballs_queued,
1192 http.pending.items.len,
1193 });
1194 tarball_thread = std.Thread.spawn(.{}, struct {
1195 fn work(h: *fetcher.Fetcher) void { h.run() catch {}; }
1196 }.work, .{http}) catch |err| blk: {
1197 debug.log("warning: failed to spawn tarball thread: {}, running synchronously", .{err});
1198 http.run() catch {};
1199 break :blk null;
1200 };
1201 }
1202
1203 std.sort.heap(linker.PackageLink, cache_hit_jobs.items, {}, packageLinkLessThanParentFirst);
1204
1205 var linked_count: usize = 0;
1206 for (cache_hit_jobs.items, 0..) |job, i| {
1207 const msg = std.fmt.allocPrintSentinel(arena_alloc, "{s}", .{job.name}, 0) catch continue;
1208 c.reportProgress(.linking, @intCast(i), @intCast(cache_hit_jobs.items.len), msg);
1209 pkg_linker.linkPackage(job) catch continue;
1210 linked_count += 1;
1211 }
1212
1213 if (tarball_thread) |t| {
1214 t.join();
1215 stage_start = debug.timer("finish tarballs + link cache hits", stage_start);
1216 } else stage_start = debug.timer("link cache hits", stage_start);
1217 debug.log(" linked {d} from cache", .{linked_count});
1218
1219 res.writeLockfile(std.mem.span(lockfile_path)) catch |err| {
1220 c.setErrorFmt("Failed to write lockfile: {}", .{err});
1221 return .io_error;
1222 };
1223 stage_start = debug.timer("write lockfile", stage_start);
1224
1225 var success_count: usize = 0;
1226 var error_count: usize = 0;
1227
1228 const LinkJobWithSize = struct {
1229 job: linker.PackageLink,
1230 size: u64,
1231 };
1232
1233 var cache_entries = std.ArrayListUnmanaged(cache.CacheDB.NamedCacheEntry){};
1234 var link_jobs = std.ArrayListUnmanaged(LinkJobWithSize){};
1235 const current_time = std.time.timestamp();
1236 const nm_path = std.mem.span(node_modules_path);
1237
1238 for (interleaved.extract_contexts.items) |ext_ctx| {
1239 if (ext_ctx.has_error or !ext_ctx.completed) {
1240 error_count += 1;
1241 debug.log(" error: {s}", .{ext_ctx.pkg_name}); continue;
1242 }
1243 success_count += 1;
1244
1245 const stats = ext_ctx.ext.stats();
1246 cache_entries.append(arena_alloc, .{
1247 .entry = .{
1248 .integrity = ext_ctx.integrity,
1249 .path = ext_ctx.cache_path,
1250 .unpacked_size = stats.bytes,
1251 .file_count = stats.files,
1252 .cached_at = current_time,
1253 },
1254 .name = ext_ctx.pkg_name,
1255 .version = ext_ctx.version_str,
1256 }) catch continue;
1257
1258 link_jobs.append(arena_alloc, .{
1259 .job = .{
1260 .cache_path = ext_ctx.cache_path,
1261 .node_modules_path = nm_path,
1262 .name = ext_ctx.pkg_name,
1263 .parent_path = ext_ctx.parent_path,
1264 .file_count = stats.files,
1265 .has_bin = ext_ctx.has_bin,
1266 },
1267 .size = stats.bytes,
1268 }) catch continue;
1269
1270 c.addPackageToResults(ext_ctx.pkg_name, ext_ctx.version_str, ext_ctx.direct) catch {};
1271 }
1272
1273 for (cache_entries.items, 0..) |entry, i| {
1274 const msg = std.fmt.allocPrintSentinel(arena_alloc, "{s}", .{entry.name}, 0) catch continue;
1275 c.reportProgress(.caching, @intCast(i), @intCast(cache_entries.items.len), msg);
1276 }
1277 db.batchInsertNamed(cache_entries.items) catch {};
1278 stage_start = debug.timer("cache insert (batch)", stage_start);
1279
1280 const total_jobs: u32 = @intCast(link_jobs.items.len);
1281 var link_counter = std.atomic.Value(u32).init(0);
1282 std.sort.heap(LinkJobWithSize, link_jobs.items, {}, struct {
1283 fn lessThan(_: void, a: LinkJobWithSize, b: LinkJobWithSize) bool {
1284 const depth_a = linkPathDepth(a.job.parent_path);
1285 const depth_b = linkPathDepth(b.job.parent_path);
1286 if (depth_a != depth_b) return depth_a < depth_b;
1287 if (a.size != b.size) return a.size > b.size;
1288
1289 const parent_a = a.job.parent_path orelse "";
1290 const parent_b = b.job.parent_path orelse "";
1291 const parent_order = std.mem.order(u8, parent_a, parent_b);
1292 if (parent_order != .eq) return parent_order == .lt;
1293 return std.mem.order(u8, a.job.name, b.job.name) == .lt;
1294 }
1295 }.lessThan);
1296
1297 var slow_link_count = std.atomic.Value(u32).init(0);
1298 var max_link_ms = std.atomic.Value(u64).init(0);
1299 var slow_link_names = std.ArrayListUnmanaged([]const u8){};
1300 defer slow_link_names.deinit(c.allocator);
1301 var slow_link_lock = std.Thread.Mutex{};
1302
1303 var depth_start: usize = 0;
1304 while (depth_start < link_jobs.items.len) {
1305 const depth = linkPathDepth(link_jobs.items[depth_start].job.parent_path);
1306 var depth_end = depth_start + 1;
1307 while (depth_end < link_jobs.items.len and
1308 linkPathDepth(link_jobs.items[depth_end].job.parent_path) == depth) : (depth_end += 1) {}
1309
1310 const depth_jobs = link_jobs.items[depth_start..depth_end];
1311 const num_threads = @min(8, depth_jobs.len);
1312 if (c.options.verbose and depth_jobs.len > 1) {
1313 debug.log(" linking depth {d} ({d} items)", .{ depth, depth_jobs.len });
1314 }
1315
1316 if (num_threads > 1 and depth_jobs.len > 4) {
1317 var threads: [8]?std.Thread = .{null} ** 8;
1318 const jobs_per_thread = (depth_jobs.len + num_threads - 1) / num_threads;
1319
1320 for (0..num_threads) |t| {
1321 const start_idx = t * jobs_per_thread;
1322 const end_idx = @min(start_idx + jobs_per_thread, depth_jobs.len);
1323 if (start_idx >= end_idx) break;
1324
1325 threads[t] = std.Thread.spawn(.{}, struct {
1326 fn work(lnk: *linker.Linker, jobs: []const LinkJobWithSize, pkg_ctx: *PkgContext, total: u32, counter: *std.atomic.Value(u32), slow_count: *std.atomic.Value(u32), max_ms: *std.atomic.Value(u64), names: *std.ArrayListUnmanaged([]const u8), lock: *std.Thread.Mutex, alloc: std.mem.Allocator) void {
1327 for (jobs) |job_with_size| {
1328 const job = job_with_size.job;
1329 const current = counter.fetchAdd(1, .monotonic) + 1;
1330 var msg_buf: [256]u8 = undefined;
1331 const msg_len = std.fmt.bufPrint(&msg_buf, "{s}", .{job.name}) catch continue;
1332 msg_buf[msg_len.len] = 0;
1333 pkg_ctx.reportProgress(.linking, current, total, msg_buf[0..msg_len.len :0]);
1334 const start = std.time.nanoTimestamp();
1335 lnk.linkPackage(job) catch {};
1336 const delta = std.time.nanoTimestamp() - start;
1337 const elapsed_ms: u64 = if (delta < 0) 0 else @intCast(@as(u128, @intCast(delta)) / 1_000_000);
1338 if (elapsed_ms > 100) {
1339 _ = slow_count.fetchAdd(1, .monotonic);
1340 lock.lock();
1341 const entry = std.fmt.allocPrint(alloc, "{s} {d}ms", .{ job.name, elapsed_ms }) catch null;
1342 if (entry) |val| {
1343 names.append(alloc, val) catch {};
1344 }
1345 lock.unlock();
1346 var current_max = max_ms.load(.monotonic);
1347 while (elapsed_ms > current_max) : (current_max = max_ms.load(.monotonic)) {
1348 if (max_ms.cmpxchgWeak(current_max, elapsed_ms, .monotonic, .monotonic) == null) break;
1349 }
1350 }
1351 }
1352 }
1353 }.work, .{ &pkg_linker, depth_jobs[start_idx..end_idx], c, total_jobs, &link_counter, &slow_link_count, &max_link_ms, &slow_link_names, &slow_link_lock, c.allocator }) catch null;
1354 }
1355
1356 for (&threads) |*t| {
1357 if (t.*) |thread| thread.join();
1358 }
1359 } else {
1360 for (depth_jobs) |job_with_size| {
1361 const job = job_with_size.job;
1362 const current = link_counter.fetchAdd(1, .monotonic) + 1;
1363 const msg = std.fmt.allocPrintSentinel(arena_alloc, "{s}", .{job.name}, 0) catch continue;
1364 c.reportProgress(.linking, current, total_jobs, msg);
1365 const start = std.time.nanoTimestamp();
1366 pkg_linker.linkPackage(job) catch {};
1367 const elapsed_ms: u64 = @intCast((@as(u64, @intCast(std.time.nanoTimestamp())) - @as(u64, @intCast(start))) / 1_000_000);
1368 if (elapsed_ms > 100 and c.options.verbose) {
1369 debug.log(" link slow: {s} {d}ms", .{ job.name, elapsed_ms });
1370 }
1371 }
1372 }
1373
1374 depth_start = depth_end;
1375 }
1376
1377 if (c.options.verbose) {
1378 debug.log(" link slow (>100ms): {d} max={d}ms", .{ slow_link_count.load(.monotonic), max_link_ms.load(.monotonic) });
1379 for (slow_link_names.items) |entry| {
1380 debug.log(" link slow: {s}", .{entry});
1381 }
1382 }
1383
1384 for (slow_link_names.items) |entry| c.allocator.free(entry);
1385 stage_start = debug.timer("link downloads (parallel)", stage_start);
1386
1387 debug.log(" downloaded: {d} success, {d} errors", .{ success_count, error_count });
1388 for (interleaved.extract_contexts.items) |ext_ctx| ext_ctx.ext.deinit();
1389
1390 db.sync();
1391 _ = debug.timer("cache sync", stage_start);
1392
1393 const link_stats = pkg_linker.getStats();
1394 c.last_install_result = .{
1395 .package_count = @intCast(res.resolved.count()),
1396 .cache_hits = @intCast(interleaved.cache_hits),
1397 .cache_misses = @intCast(interleaved.tarballs_queued),
1398 .files_linked = link_stats.files_linked,
1399 .files_copied = link_stats.files_copied,
1400 .packages_installed = link_stats.packages_installed,
1401 .packages_skipped = link_stats.packages_skipped,
1402 .elapsed_ms = timer.read() / 1_000_000,
1403 };
1404
1405 debug.log("total: {d} packages in {d}ms", .{ res.resolved.count(), c.last_install_result.elapsed_ms });
1406
1407 if (pkg_json.trusted_dependencies.count() > 0) {
1408 runTrustedPostinstall(c, &pkg_json.trusted_dependencies, std.mem.span(node_modules_path), arena_alloc);
1409 }
1410
1411 return .ok;
1412}
1413
1414const PostinstallJob = struct {
1415 pkg_name: []const u8,
1416 pkg_dir: []const u8,
1417 script: []const u8,
1418 child: ?std.process.Child = null,
1419 exit_code: ?u8 = null,
1420 stderr: ?[]const u8 = null,
1421 failed: bool = false,
1422};
1423
1424fn runTrustedPostinstall(
1425 ctx: *PkgContext,
1426 trusted: *std.StringHashMap(void),
1427 node_modules_path: []const u8,
1428 allocator: std.mem.Allocator,
1429) void {
1430 var env_map = std.process.getEnvMap(allocator) catch return;
1431 defer env_map.deinit();
1432
1433 const cwd = std.fs.cwd();
1434 const abs_nm_path = cwd.realpathAlloc(allocator, node_modules_path) catch return;
1435 defer allocator.free(abs_nm_path);
1436
1437 const bin_path = std.fmt.allocPrint(allocator, "{s}/.bin", .{abs_nm_path}) catch return;
1438 defer allocator.free(bin_path);
1439
1440 const current_path = env_map.get("PATH") orelse "";
1441 const new_path = if (builtin.os.tag == .windows)
1442 std.fmt.allocPrint(allocator, "{s};{s}", .{ bin_path, current_path }) catch return
1443 else
1444 std.fmt.allocPrint(allocator, "{s}:{s}", .{ bin_path, current_path }) catch return;
1445 defer allocator.free(new_path);
1446
1447 env_map.put("PATH", new_path) catch return;
1448
1449 var jobs = std.ArrayListUnmanaged(PostinstallJob){};
1450 defer {
1451 for (jobs.items) |*job| if (job.stderr) |s| allocator.free(s);
1452 jobs.deinit(allocator);
1453 }
1454
1455 var key_iter = trusted.keyIterator();
1456 while (key_iter.next()) |pkg_name_ptr| {
1457 const pkg_name = pkg_name_ptr.*;
1458
1459 const pkg_json_path = std.fmt.allocPrint(allocator, "{s}/{s}/package.json", .{
1460 node_modules_path, pkg_name,
1461 }) catch continue;
1462 defer allocator.free(pkg_json_path);
1463
1464 const file = std.fs.cwd().openFile(pkg_json_path, .{}) catch continue;
1465 defer file.close();
1466
1467 const content = file.readToEndAlloc(allocator, 1024 * 1024) catch continue;
1468 defer allocator.free(content);
1469
1470 var doc = json.JsonDoc.parse(content) catch continue;
1471 defer doc.deinit();
1472
1473 const root = doc.root();
1474
1475 if (root.getObject("scripts")) |scripts| {
1476 const script = scripts.getString("postinstall") orelse
1477 scripts.getString("install") orelse continue;
1478
1479 if (std.mem.eql(u8, pkg_name, "esbuild")) {
1480 debug.log("ignoring esbuild lifecycle scripts", .{});
1481 continue;
1482 }
1483
1484 const pkg_dir = std.fmt.allocPrint(allocator, "{s}/{s}", .{
1485 node_modules_path, pkg_name,
1486 }) catch continue;
1487
1488 const marker_path = std.fmt.allocPrint(allocator, "{s}/.postinstall", .{pkg_dir}) catch continue;
1489 defer allocator.free(marker_path);
1490 if (std.fs.cwd().access(marker_path, .{})) |_| {
1491 debug.log("postinstall already done: {s}", .{pkg_name});
1492 allocator.free(pkg_dir);
1493 continue;
1494 } else |_| {}
1495
1496 jobs.append(allocator, .{
1497 .pkg_name = pkg_name,
1498 .pkg_dir = pkg_dir,
1499 .script = allocator.dupe(u8, script) catch continue,
1500 }) catch continue;
1501 }
1502 }
1503
1504 if (jobs.items.len == 0) return;
1505 for (jobs.items) |job| debug.log("starting postinstall: {s}", .{job.pkg_name});
1506
1507 for (jobs.items, 0..) |*job, i| {
1508 const msg = std.fmt.allocPrintSentinel(allocator, "{s}", .{job.pkg_name}, 0) catch continue;
1509 ctx.reportProgress(.postinstall, @intCast(i), @intCast(jobs.items.len), msg);
1510 debug.log("running postinstall: {s}", .{job.pkg_name});
1511
1512 const shell_argv: []const []const u8 = if (builtin.os.tag == .windows)
1513 &[_][]const u8{ "cmd", "/c", job.script }
1514 else
1515 &[_][]const u8{ "sh", "-c", job.script };
1516
1517 var child = std.process.Child.init(shell_argv, allocator);
1518 child.cwd = job.pkg_dir;
1519 child.env_map = &env_map;
1520 child.stderr_behavior = .Pipe;
1521 child.stdout_behavior = .Pipe;
1522
1523 child.spawn() catch {
1524 job.failed = true;
1525 continue;
1526 };
1527 job.child = child;
1528 }
1529
1530 var scripts_run: u32 = 0;
1531 for (jobs.items) |*job| {
1532 if (job.child) |*child| {
1533 var stdout_buf: std.ArrayList(u8) = .empty;
1534 var stderr_buf: std.ArrayList(u8) = .empty;
1535
1536 child.collectOutput(allocator, &stdout_buf, &stderr_buf, 1024 * 1024) catch {};
1537
1538 const term = child.wait() catch {
1539 stdout_buf.deinit(allocator);
1540 stderr_buf.deinit(allocator);
1541 job.failed = true;
1542 continue;
1543 };
1544
1545 if (stdout_buf.items.len > 0) {
1546 var line_iter = std.mem.splitScalar(u8, stdout_buf.items, '\n');
1547 while (line_iter.next()) |line| {
1548 if (line.len > 0) debug.log(" {s}: {s}", .{ job.pkg_name, line });
1549 }
1550 } stdout_buf.deinit(allocator);
1551
1552 switch (term) {
1553 .Exited => |code| {
1554 if (code != 0) {
1555 job.exit_code = code;
1556 job.stderr = if (stderr_buf.items.len > 0) stderr_buf.toOwnedSlice(allocator) catch null else null;
1557 debug.log(" postinstall failed for {s}: exit code {d}", .{ job.pkg_name, code });
1558 if (job.stderr) |s| {
1559 if (s.len > 0) debug.log(" stderr: {s}", .{s});
1560 }
1561 } else {
1562 stderr_buf.deinit(allocator);
1563 scripts_run += 1;
1564 const marker_path = std.fmt.allocPrint(allocator, "{s}/.postinstall", .{job.pkg_dir}) catch continue;
1565 defer allocator.free(marker_path);
1566 if (std.fs.cwd().createFile(marker_path, .{})) |f| f.close() else |_| {}
1567 }
1568 },
1569 .Signal => |sig| {
1570 job.failed = true;
1571 debug.log(" postinstall killed by signal {d}: {s}", .{ sig, job.pkg_name });
1572 stderr_buf.deinit(allocator);
1573 },
1574 else => {
1575 job.failed = true;
1576 stderr_buf.deinit(allocator);
1577 },
1578 }
1579 }
1580 }
1581
1582 for (jobs.items) |job| {
1583 allocator.free(job.pkg_dir);
1584 allocator.free(job.script);
1585 }
1586
1587 if (scripts_run > 0) debug.log("ran {d} postinstall scripts", .{scripts_run});
1588}
1589
1590export fn pkg_add(
1591 ctx: ?*PkgContext,
1592 package_json_path: [*:0]const u8,
1593 package_spec: [*:0]const u8,
1594 dev: bool,
1595) PkgError {
1596 const c = ctx orelse return .invalid_argument;
1597 _ = c.arena_state.reset(.retain_capacity);
1598 const arena_alloc = c.arena_state.allocator();
1599
1600 const pkg_json_str = std.mem.span(package_json_path);
1601 const spec_str = std.mem.span(package_spec);
1602
1603 var pkg_name: []const u8 = spec_str;
1604 var version_constraint: []const u8 = "latest";
1605
1606 if (std.mem.indexOf(u8, spec_str, "@")) |at_idx| {
1607 if (at_idx == 0) {
1608 if (std.mem.indexOfPos(u8, spec_str, 1, "@")) |second_at| {
1609 pkg_name = spec_str[0..second_at];
1610 version_constraint = spec_str[second_at + 1 ..];
1611 }
1612 } else {
1613 pkg_name = spec_str[0..at_idx];
1614 version_constraint = spec_str[at_idx + 1 ..];
1615 }
1616 }
1617
1618 const http = c.http orelse return .network_error;
1619 var res = resolver.Resolver.init(
1620 arena_alloc,
1621 c.allocator,
1622 &c.string_pool,
1623 http,
1624 c.cache_db,
1625 if (c.options.registry_url) |url| std.mem.span(url) else "https://registry.npmjs.org",
1626 &c.metadata_cache,
1627 ); defer res.deinit();
1628
1629 const resolved_pkg = res.resolve(pkg_name, version_constraint, 0) catch |err| {
1630 setResolveError(c, pkg_name, err);
1631 return .resolve_error;
1632 };
1633
1634 const content = blk: {
1635 const file = std.fs.cwd().openFile(pkg_json_str, .{ .mode = .read_only }) catch |err| {
1636 if (err == error.FileNotFound) break :blk "{}";
1637 c.setError("Failed to open package.json");
1638 return .io_error;
1639 };
1640 defer file.close();
1641 break :blk file.readToEndAlloc(arena_alloc, 10 * 1024 * 1024) catch {
1642 c.setError("Failed to read package.json");
1643 return .io_error;
1644 };
1645 };
1646
1647 const parsed = std.json.parseFromSlice(std.json.Value, arena_alloc, content, .{}) catch {
1648 c.setError("Failed to parse package.json");
1649 return .invalid_argument;
1650 }; defer parsed.deinit();
1651
1652 if (parsed.value != .object) {
1653 c.setError("Invalid package.json format");
1654 return .invalid_argument;
1655 }
1656
1657 const version_str = resolved_pkg.version.format(arena_alloc) catch {
1658 return .out_of_memory;
1659 };
1660
1661 const version_with_caret = std.fmt.allocPrint(arena_alloc, "^{s}", .{version_str}) catch {
1662 return .out_of_memory;
1663 };
1664
1665 const target_key = if (dev) "devDependencies" else "dependencies";
1666
1667 var deps = if (parsed.value.object.get(target_key)) |d|
1668 if (d == .object) d.object else std.json.ObjectMap.init(arena_alloc)
1669 else std.json.ObjectMap.init(arena_alloc);
1670
1671 deps.put(pkg_name, .{ .string = version_with_caret }) catch {
1672 return .out_of_memory;
1673 };
1674
1675 var writer = json.JsonWriter.init() catch {
1676 return .out_of_memory;
1677 }; defer writer.deinit();
1678
1679 const root_obj = writer.createObject();
1680 writer.setRoot(root_obj);
1681
1682 var found_target = false;
1683 for (parsed.value.object.keys(), parsed.value.object.values()) |key, value| {
1684 if (std.mem.eql(u8, key, target_key)) {
1685 found_target = true;
1686 const deps_obj = writer.createObject();
1687 var dep_iter = deps.iterator();
1688 while (dep_iter.next()) |entry| {
1689 if (entry.value_ptr.* == .string) {
1690 writer.objectAdd(deps_obj, entry.key_ptr.*, writer.createString(entry.value_ptr.string));
1691 }
1692 } writer.objectAdd(root_obj, key, deps_obj);
1693 } else {
1694 const json_val = jsonValueToMut(&writer, value) catch continue;
1695 writer.objectAdd(root_obj, key, json_val);
1696 }
1697 }
1698
1699 if (!found_target) {
1700 const deps_obj = writer.createObject();
1701 writer.objectAdd(deps_obj, pkg_name, writer.createString(version_with_caret));
1702 writer.objectAdd(root_obj, target_key, deps_obj);
1703 }
1704
1705 const pkg_json_z = arena_alloc.dupeZ(u8, pkg_json_str) catch {
1706 return .out_of_memory;
1707 };
1708
1709 writer.writeToFile(pkg_json_z) catch {
1710 c.setError("Failed to write package.json");
1711 return .io_error;
1712 };
1713
1714 return .ok;
1715}
1716
1717export fn pkg_add_many(
1718 ctx: ?*PkgContext,
1719 package_json_path: [*:0]const u8,
1720 package_specs: [*]const [*:0]const u8,
1721 count: u32,
1722 dev: bool,
1723) PkgError {
1724 const c = ctx orelse return .invalid_argument;
1725 _ = c.arena_state.reset(.retain_capacity);
1726 const arena_alloc = c.arena_state.allocator();
1727
1728 const pkg_json_str = std.mem.span(package_json_path);
1729
1730 const http = c.http orelse return .network_error;
1731 var res = resolver.Resolver.init(
1732 arena_alloc,
1733 c.allocator,
1734 &c.string_pool,
1735 http,
1736 c.cache_db,
1737 if (c.options.registry_url) |url| std.mem.span(url) else "https://registry.npmjs.org",
1738 &c.metadata_cache,
1739 ); defer res.deinit();
1740
1741 const ResolvedEntry = struct {
1742 name: []const u8,
1743 version_str: []const u8,
1744 };
1745
1746 const resolved = arena_alloc.alloc(ResolvedEntry, count) catch return .out_of_memory;
1747
1748 for (0..count) |i| {
1749 const spec_str = std.mem.span(package_specs[i]);
1750 var pkg_name: []const u8 = spec_str;
1751 var version_constraint: []const u8 = "latest";
1752
1753 if (std.mem.indexOf(u8, spec_str, "@")) |at_idx| {
1754 if (at_idx == 0) {
1755 if (std.mem.indexOfPos(u8, spec_str, 1, "@")) |second_at| {
1756 pkg_name = spec_str[0..second_at];
1757 version_constraint = spec_str[second_at + 1 ..];
1758 }
1759 } else {
1760 pkg_name = spec_str[0..at_idx];
1761 version_constraint = spec_str[at_idx + 1 ..];
1762 }
1763 }
1764
1765 const resolved_pkg = res.resolve(pkg_name, version_constraint, 0) catch |err| {
1766 setResolveError(c, pkg_name, err);
1767 return .resolve_error;
1768 };
1769
1770 const version_str = resolved_pkg.version.format(arena_alloc) catch return .out_of_memory;
1771 resolved[i] = .{ .name = pkg_name, .version_str = version_str };
1772 }
1773
1774 const content = blk: {
1775 const file = std.fs.cwd().openFile(pkg_json_str, .{ .mode = .read_only }) catch |err| {
1776 if (err == error.FileNotFound) break :blk "{}";
1777 c.setError("Failed to open package.json");
1778 return .io_error;
1779 };
1780 defer file.close();
1781 break :blk file.readToEndAlloc(arena_alloc, 10 * 1024 * 1024) catch {
1782 c.setError("Failed to read package.json");
1783 return .io_error;
1784 };
1785 };
1786
1787 const parsed = std.json.parseFromSlice(std.json.Value, arena_alloc, content, .{}) catch {
1788 c.setError("Failed to parse package.json");
1789 return .invalid_argument;
1790 }; defer parsed.deinit();
1791
1792 if (parsed.value != .object) {
1793 c.setError("Invalid package.json format");
1794 return .invalid_argument;
1795 }
1796
1797 const target_key = if (dev) "devDependencies" else "dependencies";
1798
1799 var deps = if (parsed.value.object.get(target_key)) |d|
1800 if (d == .object) d.object else std.json.ObjectMap.init(arena_alloc)
1801 else std.json.ObjectMap.init(arena_alloc);
1802
1803 for (resolved) |entry| {
1804 const version_with_caret = std.fmt.allocPrint(arena_alloc, "^{s}", .{entry.version_str}) catch return .out_of_memory;
1805 deps.put(entry.name, .{ .string = version_with_caret }) catch return .out_of_memory;
1806 }
1807
1808 var writer = json.JsonWriter.init() catch return .out_of_memory;
1809 defer writer.deinit();
1810
1811 const root_obj = writer.createObject();
1812 writer.setRoot(root_obj);
1813
1814 var found_target = false;
1815 for (parsed.value.object.keys(), parsed.value.object.values()) |key, value| {
1816 if (std.mem.eql(u8, key, target_key)) {
1817 found_target = true;
1818 const deps_obj = writer.createObject();
1819 var dep_iter = deps.iterator();
1820 while (dep_iter.next()) |entry| {
1821 if (entry.value_ptr.* == .string) {
1822 writer.objectAdd(deps_obj, entry.key_ptr.*, writer.createString(entry.value_ptr.string));
1823 }
1824 } writer.objectAdd(root_obj, key, deps_obj);
1825 } else {
1826 const json_val = jsonValueToMut(&writer, value) catch continue;
1827 writer.objectAdd(root_obj, key, json_val);
1828 }
1829 }
1830
1831 if (!found_target) {
1832 const deps_obj = writer.createObject();
1833 for (resolved) |entry| {
1834 const version_with_caret = std.fmt.allocPrint(arena_alloc, "^{s}", .{entry.version_str}) catch return .out_of_memory;
1835 writer.objectAdd(deps_obj, entry.name, writer.createString(version_with_caret));
1836 }
1837 writer.objectAdd(root_obj, target_key, deps_obj);
1838 }
1839
1840 const pkg_json_z = arena_alloc.dupeZ(u8, pkg_json_str) catch return .out_of_memory;
1841
1842 writer.writeToFile(pkg_json_z) catch {
1843 c.setError("Failed to write package.json");
1844 return .io_error;
1845 };
1846
1847 return .ok;
1848}
1849
1850fn jsonValueToMut(writer: *json.JsonWriter, value: std.json.Value) !*json.yyjson.yyjson_mut_val {
1851 return switch (value) {
1852 .null => writer.createNull(),
1853 .bool => |b| writer.createBool(b),
1854 .integer => |i| writer.createInt(i),
1855 .float => |f| writer.createReal(f),
1856 .string => |s| writer.createString(s),
1857 .array => |arr| blk: {
1858 const json_arr = writer.createArray();
1859 for (arr.items) |item| {
1860 const item_val = try jsonValueToMut(writer, item);
1861 writer.arrayAppend(json_arr, item_val);
1862 }
1863 break :blk json_arr;
1864 },
1865 .object => |obj| blk: {
1866 const json_obj = writer.createObject();
1867 for (obj.keys(), obj.values()) |k, v| {
1868 const v_mut = try jsonValueToMut(writer, v);
1869 writer.objectAdd(json_obj, k, v_mut);
1870 }
1871 break :blk json_obj;
1872 },
1873 .number_string => |s| writer.createString(s),
1874 };
1875}
1876
1877export fn pkg_remove(
1878 ctx: ?*PkgContext,
1879 package_json_path: [*:0]const u8,
1880 package_name: [*:0]const u8,
1881) PkgError {
1882 const c = ctx orelse return .invalid_argument;
1883 _ = c.arena_state.reset(.retain_capacity);
1884 const arena_alloc = c.arena_state.allocator();
1885
1886 const pkg_json_str = std.mem.span(package_json_path);
1887 const name_str = std.mem.span(package_name);
1888
1889 const content = std.fs.cwd().readFileAlloc(arena_alloc, pkg_json_str, 10 * 1024 * 1024) catch {
1890 c.setError("Failed to read package.json");
1891 return .io_error;
1892 };
1893
1894 const parsed = std.json.parseFromSlice(std.json.Value, arena_alloc, content, .{}) catch {
1895 c.setError("Failed to parse package.json");
1896 return .invalid_argument;
1897 };
1898 defer parsed.deinit();
1899
1900 if (parsed.value != .object) {
1901 c.setError("Invalid package.json format");
1902 return .invalid_argument;
1903 }
1904
1905 const dep_keys = [_][]const u8{
1906 "dependencies",
1907 "devDependencies",
1908 "peerDependencies",
1909 "optionalDependencies"
1910 };
1911
1912 const found = found: {
1913 for (dep_keys) |dep_key| {
1914 const deps = parsed.value.object.get(dep_key) orelse continue;
1915 if (deps != .object) continue;
1916 if (deps.object.get(name_str) != null) break :found true;
1917 }
1918 break :found false;
1919 };
1920
1921 if (!found) {
1922 c.setErrorFmt("Package {s} not found in dependencies", .{name_str});
1923 return .not_found;
1924 }
1925
1926 var writer = json.JsonWriter.init() catch return .out_of_memory;
1927 defer writer.deinit();
1928
1929 const root_obj = writer.createObject();
1930 writer.setRoot(root_obj);
1931
1932 for (parsed.value.object.keys(), parsed.value.object.values()) |key, value| {
1933 const is_dep_obj = for (dep_keys) |dk| {
1934 if (std.mem.eql(u8, key, dk)) break true;
1935 } else false;
1936
1937 if (!is_dep_obj or value != .object) {
1938 const val_mut = jsonValueToMut(&writer, value) catch continue;
1939 writer.objectAdd(root_obj, key, val_mut);
1940 continue;
1941 }
1942
1943 const filtered_obj = writer.createObject();
1944 for (value.object.keys(), value.object.values()) |dk, dv| {
1945 if (std.mem.eql(u8, dk, name_str)) continue;
1946 const dv_mut = jsonValueToMut(&writer, dv) catch continue;
1947 writer.objectAdd(filtered_obj, dk, dv_mut);
1948 }
1949 writer.objectAdd(root_obj, key, filtered_obj);
1950 }
1951
1952 const pkg_json_z = arena_alloc.dupeZ(u8, pkg_json_str) catch return .out_of_memory;
1953 writer.writeToFile(pkg_json_z) catch {
1954 c.setError("Failed to write package.json");
1955 return .io_error;
1956 };
1957
1958 return .ok;
1959}
1960
1961export fn pkg_error_string(ctx: ?*const PkgContext) [*:0]const u8 {
1962 if (ctx) |c| if (c.last_error) |e| return e.ptr;
1963 return "Unknown error";
1964}
1965
1966export fn pkg_cache_sync(ctx: ?*PkgContext) void {
1967 if (ctx) |c| if (c.cache_db) |db| db.sync();
1968}
1969
1970export fn pkg_cache_stats(ctx: ?*PkgContext, out: *CacheStats) PkgError {
1971 const c = ctx orelse return .invalid_argument;
1972 const db = c.cache_db orelse return .cache_error;
1973
1974 const stats = db.stats() catch return .cache_error;
1975 out.* = .{
1976 .total_size = stats.cache_size,
1977 .db_size = stats.db_size,
1978 .package_count = @intCast(stats.entries),
1979 };
1980
1981 return .ok;
1982}
1983
1984export fn pkg_cache_prune(ctx: ?*PkgContext, max_age_days: u32) i32 {
1985 const c = ctx orelse return @intFromEnum(PkgError.invalid_argument);
1986 const db = c.cache_db orelse return @intFromEnum(PkgError.cache_error);
1987
1988 const pruned = db.prune(max_age_days) catch return @intFromEnum(PkgError.cache_error);
1989 return @intCast(pruned);
1990}
1991
1992export fn pkg_get_bin_path(
1993 node_modules_path: [*:0]const u8,
1994 bin_name: [*:0]const u8,
1995 out_path: [*]u8,
1996 out_path_len: usize,
1997) c_int {
1998 const nm_path = std.mem.span(node_modules_path);
1999 const full = std.mem.span(bin_name);
2000
2001 const start: usize = if (full.len > 0 and full[0] == '@')
2002 (std.mem.indexOfScalar(u8, full[1..], '/') orelse return -1) + 2
2003 else 0;
2004 const name, const constraint_str = if (std.mem.indexOfScalar(u8, full[start..], '@')) |i|
2005 .{ full[0..start + i], full[start + i + 1..] }
2006 else
2007 .{ full, @as([]const u8, "") };
2008
2009 var path_buf: [std.fs.max_path_bytes]u8 = undefined;
2010 const bin_path = std.fmt.bufPrint(&path_buf, "{s}/.bin/{s}", .{ nm_path, name }) catch return -1;
2011
2012 std.fs.cwd().access(bin_path, .{}) catch return -1;
2013
2014 if (constraint_str.len > 0) {
2015 const constraint = resolver.Constraint.parse(constraint_str) catch return -1;
2016 if (constraint.kind != .any) {
2017 var pkg_buf: [std.fs.max_path_bytes]u8 = undefined;
2018 const pkg_path = std.fmt.bufPrint(&pkg_buf, "{s}/{s}/package.json", .{ nm_path, name }) catch return -1;
2019 const pkg_path_z = pkg_buf[0..pkg_path.len :0];
2020 var doc = json.JsonDoc.parseFile(pkg_path_z) catch return -1;
2021 defer doc.deinit();
2022 const version_str = doc.root().getString("version") orelse return -1;
2023 const installed = resolver.Version.parse(version_str) catch return -1;
2024 if (!constraint.satisfies(installed)) return -1;
2025 }
2026 }
2027
2028 var real_path_buf: [std.fs.max_path_bytes]u8 = undefined;
2029 const real_path = std.fs.cwd().realpath(bin_path, &real_path_buf) catch return -1;
2030
2031 if (real_path.len >= out_path_len) return -1;
2032
2033 @memcpy(out_path[0..real_path.len], real_path);
2034 out_path[real_path.len] = 0;
2035
2036 return @intCast(real_path.len);
2037}
2038
2039export fn pkg_list_bins(
2040 node_modules_path: [*:0]const u8,
2041 callback: ?*const fn ([*:0]const u8, ?*anyopaque) callconv(.c) void,
2042 user_data: ?*anyopaque,
2043) c_int {
2044 const nm_path = std.mem.span(node_modules_path);
2045
2046 var path_buf: [std.fs.max_path_bytes]u8 = undefined;
2047 const bin_dir_path = std.fmt.bufPrint(&path_buf, "{s}/.bin", .{nm_path}) catch return -1;
2048
2049 var dir = std.fs.cwd().openDir(bin_dir_path, .{ .iterate = true }) catch return -1;
2050 defer dir.close();
2051
2052 var count: c_int = 0;
2053 var iter = dir.iterate();
2054 while (iter.next() catch null) |entry| {
2055 if (entry.kind == .sym_link or entry.kind == .file) {
2056 if (callback) |cb| {
2057 var name_buf: [256]u8 = undefined;
2058 if (entry.name.len < name_buf.len) {
2059 @memcpy(name_buf[0..entry.name.len], entry.name);
2060 name_buf[entry.name.len] = 0;
2061 cb(@ptrCast(&name_buf), user_data);
2062 }
2063 }
2064 count += 1;
2065 }
2066 }
2067
2068 return count;
2069}
2070
2071export fn pkg_list_package_bins(
2072 node_modules_path: [*:0]const u8,
2073 package_name: [*:0]const u8,
2074 callback: ?*const fn ([*:0]const u8, ?*anyopaque) callconv(.c) void,
2075 user_data: ?*anyopaque,
2076) c_int {
2077 const nm_path = std.mem.span(node_modules_path);
2078 const pkg_name = std.mem.span(package_name);
2079
2080 var path_buf: [std.fs.max_path_bytes]u8 = undefined;
2081 const pkg_json_path = std.fmt.bufPrint(&path_buf, "{s}/{s}/package.json", .{ nm_path, pkg_name }) catch return -1;
2082
2083 const file = std.fs.cwd().openFile(pkg_json_path, .{}) catch return -1;
2084 defer file.close();
2085
2086 const content = file.readToEndAlloc(global_allocator, 1024 * 1024) catch return -1;
2087 defer global_allocator.free(content);
2088
2089 var doc = json.JsonDoc.parse(content) catch return -1;
2090 defer doc.deinit();
2091
2092 const root_val = doc.root();
2093 var count: c_int = 0;
2094
2095 if (root_val.getObject("bin")) |bin_obj| {
2096 var iter = bin_obj.objectIterator() orelse return 0;
2097 while (iter.next()) |entry| {
2098 if (callback) |cb| {
2099 var name_buf: [256]u8 = undefined;
2100 if (entry.key.len < name_buf.len) {
2101 @memcpy(name_buf[0..entry.key.len], entry.key);
2102 name_buf[entry.key.len] = 0;
2103 cb(@ptrCast(&name_buf), user_data);
2104 }
2105 }
2106 count += 1;
2107 }
2108 } else if (root_val.getString("bin")) |_| {
2109 const simple_name = if (std.mem.indexOf(u8, pkg_name, "/")) |slash| pkg_name[slash + 1 ..]
2110 else pkg_name;
2111
2112 if (callback) |cb| {
2113 var name_buf: [256]u8 = undefined;
2114 if (simple_name.len < name_buf.len) {
2115 @memcpy(name_buf[0..simple_name.len], simple_name);
2116 name_buf[simple_name.len] = 0;
2117 cb(@ptrCast(&name_buf), user_data);
2118 }
2119 }
2120 count = 1;
2121 }
2122
2123 return count;
2124}
2125
2126export fn pkg_get_script(
2127 package_json_path: [*:0]const u8,
2128 script_name: [*:0]const u8,
2129 out_script: [*]u8,
2130 out_script_len: usize,
2131) c_int {
2132 const allocator = global_allocator;
2133 const name = std.mem.span(script_name);
2134
2135 var doc = json.JsonDoc.parseFile(std.mem.span(package_json_path)) catch return -1;
2136 defer doc.deinit();
2137 const root_val = doc.root();
2138
2139 if (root_val.getObject("scripts")) |scripts_obj| {
2140 if (scripts_obj.getString(std.mem.span(script_name))) |script| {
2141 if (script.len >= out_script_len) return -1;
2142 @memcpy(out_script[0..script.len], script);
2143 out_script[script.len] = 0;
2144 return @intCast(script.len);
2145 }
2146 }
2147
2148 if (std.mem.eql(u8, name, "start")) {
2149 if (root_val.getString("main")) |main_file| {
2150 const script = std.fmt.allocPrint(allocator, "ant {s}", .{main_file}) catch return -1;
2151 defer allocator.free(script);
2152 if (script.len >= out_script_len) return -1;
2153 @memcpy(out_script[0..script.len], script);
2154 out_script[script.len] = 0;
2155 return @intCast(script.len);
2156 }
2157
2158 if (std.fs.cwd().access("server.js", .{})) |_| {
2159 const script = "ant server.js";
2160 if (script.len >= out_script_len) return -1;
2161 @memcpy(out_script[0..script.len], script);
2162 out_script[script.len] = 0;
2163 return @intCast(script.len);
2164 } else |_| {}
2165 }
2166
2167 return -1;
2168}
2169
2170pub const ScriptResult = extern struct {
2171 exit_code: c_int,
2172 signal: c_int,
2173};
2174
2175fn runScriptCommand(
2176 allocator: std.mem.Allocator,
2177 script: []const u8,
2178 extra_args: ?[*:0]const u8,
2179 env_map: *std.process.EnvMap,
2180) !ScriptResult {
2181 const final_script = if (extra_args) |args| blk: {
2182 const args_str = std.mem.span(args);
2183 if (args_str.len > 0) {
2184 break :blk try std.fmt.allocPrint(allocator, "{s} {s}", .{ script, args_str });
2185 }
2186 break :blk try allocator.dupe(u8, script);
2187 } else try allocator.dupe(u8, script);
2188 defer allocator.free(final_script);
2189
2190 const script_z = try allocator.dupeZ(u8, final_script);
2191 defer allocator.free(script_z);
2192
2193 const shell_argv: []const []const u8 = if (builtin.os.tag == .windows)
2194 &[_][]const u8{ "cmd", "/c", script_z }
2195 else &[_][]const u8{ "sh", "-c", script_z };
2196
2197 var child = std.process.Child.init(shell_argv, allocator);
2198 child.env_map = env_map;
2199
2200 try child.spawn();
2201 const term = try child.wait();
2202
2203 return switch (term) {
2204 .Exited => |code| .{ .exit_code = code, .signal = 0 },
2205 .Signal => |sig| .{ .exit_code = -1, .signal = @intCast(sig) },
2206 else => .{ .exit_code = -1, .signal = 0 },
2207 };
2208}
2209
2210export fn pkg_run_script(
2211 package_json_path: [*:0]const u8,
2212 script_name: [*:0]const u8,
2213 node_modules_path: [*:0]const u8,
2214 extra_args: ?[*:0]const u8,
2215 result: ?*ScriptResult,
2216) PkgError {
2217 const allocator = global_allocator;
2218 const name = std.mem.span(script_name);
2219
2220 var doc = json.JsonDoc.parseFile(std.mem.span(package_json_path)) catch return .io_error;
2221 defer doc.deinit(); const root_val = doc.root();
2222
2223 var script_buf: [8192]u8 = undefined;
2224 const script_len = pkg_get_script(package_json_path, script_name, &script_buf, script_buf.len);
2225
2226 if (script_len < 0) return .not_found;
2227 const script = script_buf[0..@intCast(script_len)];
2228
2229 var pre_script: ?[]const u8 = null;
2230 var post_script: ?[]const u8 = null;
2231
2232 if (root_val.getObject("scripts")) |scripts_obj| {
2233 var pre_key_buf: [256]u8 = undefined;
2234 var post_key_buf: [256]u8 = undefined;
2235
2236 const pre_key = std.fmt.bufPrintZ(&pre_key_buf, "pre{s}", .{name}) catch null;
2237 const post_key = std.fmt.bufPrintZ(&post_key_buf, "post{s}", .{name}) catch null;
2238
2239 if (pre_key) |pk| pre_script = scripts_obj.getString(pk);
2240 if (post_key) |pk| post_script = scripts_obj.getString(pk);
2241 }
2242
2243 var env_map = std.process.getEnvMap(allocator) catch return .out_of_memory;
2244 defer env_map.deinit(); const nm_path = std.mem.span(node_modules_path);
2245
2246 const cwd = std.fs.cwd();
2247 const abs_nm_path = cwd.realpathAlloc(allocator, nm_path) catch nm_path;
2248 defer if (abs_nm_path.ptr != nm_path.ptr) allocator.free(abs_nm_path);
2249
2250 const bin_path = std.fmt.allocPrint(allocator, "{s}/.bin", .{abs_nm_path}) catch return .out_of_memory;
2251 defer allocator.free(bin_path);
2252
2253 const current_path = env_map.get("PATH") orelse "";
2254 const new_path = if (builtin.os.tag == .windows)
2255 std.fmt.allocPrint(allocator, "{s};{s}", .{ bin_path, current_path }) catch return .out_of_memory
2256 else
2257 std.fmt.allocPrint(allocator, "{s}:{s}", .{ bin_path, current_path }) catch return .out_of_memory;
2258 defer allocator.free(new_path);
2259
2260 env_map.put("PATH", new_path) catch return .out_of_memory;
2261 env_map.put("npm_lifecycle_event", name) catch {};
2262
2263 if (root_val.getObject("config")) |config_obj| {
2264 if (config_obj.objectIterator()) |*config_iter_ptr| {
2265 var config_iter = config_iter_ptr.*;
2266 while (config_iter.next()) |entry| {
2267 if (entry.value.asString()) |value| {
2268 const env_key = std.fmt.allocPrint(allocator, "npm_package_config_{s}", .{entry.key}) catch continue;
2269 defer allocator.free(env_key);
2270 env_map.put(env_key, value) catch {};
2271 }
2272 }
2273 }
2274 }
2275
2276 if (root_val.getString("name")) |pkg_name| env_map.put("npm_package_name", pkg_name) catch {};
2277 if (root_val.getString("version")) |pkg_version| env_map.put("npm_package_version", pkg_version) catch {};
2278
2279 if (pre_script) |pre| {
2280 const pre_event = std.fmt.allocPrint(allocator, "pre{s}", .{name}) catch name;
2281 defer if (pre_event.ptr != name.ptr) allocator.free(pre_event);
2282 env_map.put("npm_lifecycle_event", pre_event) catch {};
2283 const pre_result = runScriptCommand(allocator, pre, null, &env_map) catch return .io_error;
2284 if (pre_result.exit_code != 0) {
2285 if (result) |r| r.* = pre_result;
2286 return .ok;
2287 }
2288 }
2289
2290 env_map.put("npm_lifecycle_event", name) catch {};
2291 const main_result = runScriptCommand(allocator, script, extra_args, &env_map) catch return .io_error;
2292
2293 if (main_result.exit_code != 0) {
2294 if (result) |r| r.* = main_result;
2295 return .ok;
2296 }
2297
2298 if (post_script) |post| {
2299 const post_event = std.fmt.allocPrint(allocator, "post{s}", .{name}) catch name;
2300 defer if (post_event.ptr != name.ptr) allocator.free(post_event);
2301 env_map.put("npm_lifecycle_event", post_event) catch {};
2302 const post_result = runScriptCommand(allocator, post, null, &env_map) catch return .io_error;
2303 if (result) |r| r.* = post_result;
2304 return .ok;
2305 }
2306
2307 if (result) |r| r.* = main_result;
2308 return .ok;
2309}
2310
2311pub const DepType = packed struct(u8) {
2312 peer: bool = false,
2313 dev: bool = false,
2314 optional: bool = false,
2315 direct: bool = false,
2316 _reserved: u4 = 0,
2317};
2318
2319pub const DepCallback = ?*const fn (
2320 name: [*:0]const u8,
2321 version: [*:0]const u8,
2322 constraint: [*:0]const u8,
2323 dep_type: DepType,
2324 user_data: ?*anyopaque,
2325) callconv(.c) void;
2326
2327pub const WhyInfo = extern struct {
2328 target_version: [64]u8,
2329 found: bool,
2330 is_peer: bool,
2331 is_dev: bool,
2332 is_direct: bool,
2333};
2334
2335export fn pkg_why_info(
2336 lockfile_path: [*:0]const u8,
2337 package_name: [*:0]const u8,
2338 out: *WhyInfo,
2339) c_int {
2340 const lf = lockfile.Lockfile.open(std.mem.span(lockfile_path)) catch return -1;
2341 defer @constCast(&lf).close();
2342
2343 const target_name = std.mem.span(package_name);
2344 out.found = false;
2345 out.is_peer = false;
2346 out.is_dev = false;
2347 out.is_direct = false;
2348 @memset(&out.target_version, 0);
2349
2350 for (lf.packages) |*pkg| {
2351 const pkg_name = pkg.name.slice(lf.string_table);
2352 if (std.mem.eql(u8, pkg_name, target_name)) {
2353 const ver_str = pkg.versionString(global_allocator, lf.string_table) catch return -1;
2354 defer global_allocator.free(ver_str);
2355 if (ver_str.len < out.target_version.len) {
2356 @memcpy(out.target_version[0..ver_str.len], ver_str);
2357 out.target_version[ver_str.len] = 0;
2358 }
2359 out.found = true;
2360 out.is_dev = pkg.flags.dev;
2361 out.is_direct = pkg.flags.direct;
2362 }
2363
2364 const deps = lf.getPackageDeps(pkg);
2365 for (deps) |dep| {
2366 const dep_pkg = &lf.packages[dep.package_index];
2367 const dep_name = dep_pkg.name.slice(lf.string_table);
2368 if (std.mem.eql(u8, dep_name, target_name) and dep.flags.peer) {
2369 out.is_peer = true;
2370 }
2371 }
2372 }
2373 return 0;
2374}
2375
2376export fn pkg_why(
2377 lockfile_path: [*:0]const u8,
2378 package_name: [*:0]const u8,
2379 callback: DepCallback,
2380 user_data: ?*anyopaque,
2381) c_int {
2382 const lf = lockfile.Lockfile.open(std.mem.span(lockfile_path)) catch return -1;
2383 defer @constCast(&lf).close();
2384
2385 const target_name = std.mem.span(package_name);
2386 var count: c_int = 0;
2387
2388 var name_buf: [512]u8 = undefined;
2389 var ver_buf: [64]u8 = undefined;
2390 var constraint_buf: [128]u8 = undefined;
2391
2392 for (lf.packages) |*pkg| {
2393 const deps = lf.getPackageDeps(pkg);
2394 for (deps) |dep| {
2395 const dep_pkg = &lf.packages[dep.package_index];
2396 const dep_name = dep_pkg.name.slice(lf.string_table);
2397
2398 if (std.mem.eql(u8, dep_name, target_name)) {
2399 const pkg_name = pkg.name.slice(lf.string_table);
2400 const constraint = dep.constraint.slice(lf.string_table);
2401
2402 if (callback) |cb| {
2403 if (pkg_name.len < name_buf.len) {
2404 @memcpy(name_buf[0..pkg_name.len], pkg_name);
2405 name_buf[pkg_name.len] = 0;
2406
2407 const ver_str = pkg.versionString(global_allocator, lf.string_table) catch continue;
2408 defer global_allocator.free(ver_str);
2409 if (ver_str.len < ver_buf.len) {
2410 @memcpy(ver_buf[0..ver_str.len], ver_str);
2411 ver_buf[ver_str.len] = 0;
2412
2413 if (constraint.len < constraint_buf.len) {
2414 @memcpy(constraint_buf[0..constraint.len], constraint);
2415 constraint_buf[constraint.len] = 0;
2416
2417 const dep_type = DepType{
2418 .peer = dep.flags.peer,
2419 .dev = dep.flags.dev or pkg.flags.dev,
2420 .optional = dep.flags.optional,
2421 .direct = pkg.flags.direct,
2422 };
2423 cb(@ptrCast(&name_buf), @ptrCast(&ver_buf), @ptrCast(&constraint_buf), dep_type, user_data);
2424 }
2425 }
2426 }
2427 }
2428 count += 1;
2429 }
2430 }
2431 }
2432
2433 for (lf.packages) |*pkg| {
2434 const pkg_name = pkg.name.slice(lf.string_table);
2435 if (std.mem.eql(u8, pkg_name, target_name) and pkg.flags.direct) {
2436 if (callback) |cb| {
2437 const direct_str = "package.json";
2438 var direct_buf: [16]u8 = undefined;
2439 @memcpy(direct_buf[0..direct_str.len], direct_str);
2440 direct_buf[direct_str.len] = 0;
2441
2442 var empty_buf: [1]u8 = .{0};
2443 var dep_buf: [16]u8 = undefined;
2444 const constraint_str = "dependencies";
2445 @memcpy(dep_buf[0..constraint_str.len], constraint_str);
2446 dep_buf[constraint_str.len] = 0;
2447
2448 const dep_type = DepType{
2449 .peer = false,
2450 .dev = pkg.flags.dev,
2451 .optional = false,
2452 .direct = true,
2453 };
2454 cb(@ptrCast(&direct_buf), @ptrCast(&empty_buf), @ptrCast(&dep_buf), dep_type, user_data);
2455 }
2456 count += 1;
2457 }
2458 }
2459
2460 return count;
2461}
2462
2463export fn pkg_list_scripts(
2464 package_json_path: [*:0]const u8,
2465 callback: ?*const fn ([*:0]const u8, [*:0]const u8, ?*anyopaque) callconv(.c) void,
2466 user_data: ?*anyopaque,
2467) c_int {
2468 var doc = json.JsonDoc.parseFile(std.mem.span(package_json_path)) catch return -1;
2469 defer doc.deinit();
2470
2471 const root_val = doc.root();
2472 const scripts_obj = root_val.getObject("scripts") orelse return 0;
2473
2474 var iter = scripts_obj.objectIterator() orelse return 0;
2475 defer iter.deinit();
2476
2477 var count: c_int = 0;
2478 while (iter.next()) |entry| {
2479 if (callback) |cb| {
2480 var name_buf: [256]u8 = undefined;
2481 var cmd_buf: [4096]u8 = undefined;
2482
2483 if (entry.key.len < name_buf.len) {
2484 @memcpy(name_buf[0..entry.key.len], entry.key);
2485 name_buf[entry.key.len] = 0;
2486
2487 if (entry.value.asString()) |cmd| {
2488 if (cmd.len < cmd_buf.len) {
2489 @memcpy(cmd_buf[0..cmd.len], cmd);
2490 cmd_buf[cmd.len] = 0;
2491 cb(@ptrCast(&name_buf), @ptrCast(&cmd_buf), user_data);
2492 }
2493 }
2494 }
2495 }
2496 count += 1;
2497 }
2498
2499 return count;
2500}
2501
2502export fn pkg_info(
2503 ctx: ?*PkgContext,
2504 package_spec: [*:0]const u8,
2505 out: *PkgInfo,
2506) PkgError {
2507 const c = ctx orelse return .invalid_argument;
2508 c.clearInfo();
2509 _ = c.arena_state.reset(.retain_capacity);
2510 const arena = c.arena_state.allocator();
2511
2512 const http = c.http orelse return .network_error;
2513 const spec = std.mem.span(package_spec);
2514
2515 var name: []const u8 = spec;
2516 var requested_version: ?[]const u8 = null;
2517 if (std.mem.lastIndexOf(u8, spec, "@")) |at_idx| {
2518 if (at_idx > 0) {
2519 name = spec[0..at_idx];
2520 requested_version = spec[at_idx + 1..];
2521 }
2522 }
2523
2524 const data = http.fetchMetadataFull(name, true, c.allocator) catch |err| {
2525 c.setErrorFmt("Failed to fetch package info: {}", .{err});
2526 return .network_error;
2527 }; defer c.allocator.free(data);
2528
2529 var doc = json.JsonDoc.parse(data) catch {
2530 c.setError("Failed to parse package metadata");
2531 return .resolve_error;
2532 };
2533
2534 defer doc.deinit();
2535 const root = doc.root();
2536
2537 const versions_obj = root.getObject("versions") orelse {
2538 c.setError("No versions found");
2539 return .not_found;
2540 };
2541
2542 var versions_iter = versions_obj.objectIterator() orelse return .resolve_error;
2543 defer versions_iter.deinit();
2544 var version_count: u32 = 0;
2545 while (versions_iter.next()) |_| version_count += 1;
2546
2547 var version_str: []const u8 = "";
2548 if (requested_version) |rv| {
2549 version_str = rv;
2550 } else if (root.getObject("dist-tags")) |tags| {
2551 version_str = tags.getString("latest") orelse "";
2552 }
2553
2554 const version_z = arena.dupeZ(u8, version_str) catch return .out_of_memory;
2555
2556 const version_obj = versions_obj.getObject(version_z) orelse {
2557 c.setErrorFmt("Version {s} not found", .{version_str});
2558 return .not_found;
2559 };
2560
2561 var dep_count: u32 = 0;
2562 if (version_obj.getObject("dependencies")) |deps| {
2563 var deps_iter = deps.objectIterator() orelse return .resolve_error;
2564 defer deps_iter.deinit();
2565 while (deps_iter.next()) |entry| {
2566 dep_count += 1;
2567 if (entry.value.asString()) |ver| {
2568 c.info_dependencies.append(c.allocator, .{
2569 .name = c.storeInfoString(entry.key) catch continue,
2570 .version = c.storeInfoString(ver) catch continue,
2571 }) catch continue;
2572 }
2573 }
2574 }
2575
2576 const dist = version_obj.getObject("dist");
2577 const unpacked_size: u64 = if (dist) |d| @as(u64, @intCast(d.getInt("unpackedSize") orelse 0)) else 0;
2578
2579 var keywords_buf = std.ArrayListUnmanaged(u8){};
2580 defer keywords_buf.deinit(c.allocator);
2581 if (version_obj.getArray("keywords")) |kw_arr| {
2582 var kw_iter = kw_arr.arrayIterator() orelse return .resolve_error;
2583 defer kw_iter.deinit();
2584 var first = true;
2585 while (kw_iter.next()) |kw_val| {
2586 if (kw_val.asString()) |kw| {
2587 if (!first) keywords_buf.appendSlice(c.allocator, ", ") catch {};
2588 keywords_buf.appendSlice(c.allocator, kw) catch {};
2589 first = false;
2590 }
2591 }
2592 }
2593
2594 out.* = .{
2595 .name = c.storeInfoString(root.getString("name") orelse name) catch return .out_of_memory,
2596 .version = c.storeInfoString(version_str) catch return .out_of_memory,
2597 .description = c.storeInfoString(version_obj.getString("description") orelse "") catch return .out_of_memory,
2598 .license = c.storeInfoString(version_obj.getString("license") orelse "") catch return .out_of_memory,
2599 .homepage = c.storeInfoString(version_obj.getString("homepage") orelse "") catch return .out_of_memory,
2600 .tarball = c.storeInfoString(if (dist) |d| d.getString("tarball") orelse "" else "") catch return .out_of_memory,
2601 .shasum = c.storeInfoString(if (dist) |d| d.getString("shasum") orelse "" else "") catch return .out_of_memory,
2602 .integrity = c.storeInfoString(if (dist) |d| d.getString("integrity") orelse "" else "") catch return .out_of_memory,
2603 .keywords = c.storeInfoString(keywords_buf.items) catch return .out_of_memory,
2604 .published = c.storeInfoString(if (root.getObject("time")) |t| t.getString(version_z) orelse "" else "") catch return .out_of_memory,
2605 .dep_count = dep_count,
2606 .version_count = version_count,
2607 .unpacked_size = unpacked_size,
2608 };
2609
2610 if (root.getObject("dist-tags")) |tags| {
2611 var tags_iter = tags.objectIterator() orelse return .ok;
2612 defer tags_iter.deinit();
2613 while (tags_iter.next()) |entry| {
2614 if (entry.value.asString()) |ver| {
2615 c.info_dist_tags.append(c.allocator, .{
2616 .tag = c.storeInfoString(entry.key) catch continue,
2617 .version = c.storeInfoString(ver) catch continue,
2618 }) catch continue;
2619 }
2620 }
2621 }
2622
2623 if (root.getArray("maintainers")) |maint_arr| {
2624 var maint_iter = maint_arr.arrayIterator() orelse return .ok;
2625 defer maint_iter.deinit();
2626 while (maint_iter.next()) |maint_val| {
2627 const maint_name = maint_val.getString("name") orelse continue;
2628 const maint_email = maint_val.getString("email") orelse "";
2629 c.info_maintainers.append(c.allocator, .{
2630 .name = c.storeInfoString(maint_name) catch continue,
2631 .email = c.storeInfoString(maint_email) catch continue,
2632 }) catch continue;
2633 }
2634 }
2635
2636 return .ok;
2637}
2638
2639export fn pkg_info_dist_tag_count(ctx: ?*const PkgContext) u32 {
2640 const c = ctx orelse return 0;
2641 return @intCast(c.info_dist_tags.items.len);
2642}
2643
2644export fn pkg_info_get_dist_tag(ctx: ?*const PkgContext, index: u32, out: *DistTag) PkgError {
2645 const c = ctx orelse return .invalid_argument;
2646 if (index >= c.info_dist_tags.items.len) return .invalid_argument;
2647 out.* = c.info_dist_tags.items[index];
2648 return .ok;
2649}
2650
2651export fn pkg_info_maintainer_count(ctx: ?*const PkgContext) u32 {
2652 const c = ctx orelse return 0;
2653 return @intCast(c.info_maintainers.items.len);
2654}
2655
2656export fn pkg_info_get_maintainer(ctx: ?*const PkgContext, index: u32, out: *Maintainer) PkgError {
2657 const c = ctx orelse return .invalid_argument;
2658 if (index >= c.info_maintainers.items.len) return .invalid_argument;
2659 out.* = c.info_maintainers.items[index];
2660 return .ok;
2661}
2662
2663export fn pkg_info_dependency_count(ctx: ?*const PkgContext) u32 {
2664 const c = ctx orelse return 0;
2665 return @intCast(c.info_dependencies.items.len);
2666}
2667
2668export fn pkg_info_get_dependency(ctx: ?*const PkgContext, index: u32, out: *Dependency) PkgError {
2669 const c = ctx orelse return .invalid_argument;
2670 if (index >= c.info_dependencies.items.len) return .invalid_argument;
2671 out.* = c.info_dependencies.items[index];
2672 return .ok;
2673}
2674
2675const BinSelection = struct {
2676 err: PkgError,
2677 name: []const u8 = "",
2678};
2679
2680fn packageSimpleName(pkg_name: []const u8) []const u8 {
2681 if (std.mem.lastIndexOfScalar(u8, pkg_name, '/')) |slash| {
2682 return pkg_name[slash + 1 ..];
2683 }
2684 return pkg_name;
2685}
2686
2687fn normalizedBinTarget(path: []const u8) []const u8 {
2688 if (std.mem.startsWith(u8, path, "./")) return path[2..];
2689 return path;
2690}
2691
2692fn appendBinName(list: *std.ArrayList(u8), allocator: std.mem.Allocator, name: []const u8) !void {
2693 if (list.items.len > 0) try list.appendSlice(allocator, ", ");
2694 try list.appendSlice(allocator, name);
2695}
2696
2697fn selectPackageBinName(
2698 c: *PkgContext,
2699 allocator: std.mem.Allocator,
2700 node_modules_path: []const u8,
2701 pkg_name: []const u8,
2702) BinSelection {
2703 const simple_name = packageSimpleName(pkg_name);
2704
2705 const pkg_json_path = std.fmt.allocPrint(allocator, "{s}/{s}/package.json", .{
2706 node_modules_path,
2707 pkg_name,
2708 }) catch return .{ .err = .out_of_memory };
2709
2710 const file = std.fs.cwd().openFile(pkg_json_path, .{}) catch {
2711 c.setErrorFmt("Package '{s}' was installed, but its package.json could not be opened", .{pkg_name});
2712 return .{ .err = .io_error };
2713 };
2714 defer file.close();
2715
2716 const content = file.readToEndAlloc(allocator, 1024 * 1024) catch {
2717 c.setErrorFmt("Package '{s}' was installed, but its package.json could not be read", .{pkg_name});
2718 return .{ .err = .io_error };
2719 };
2720
2721 var doc = json.JsonDoc.parse(content) catch {
2722 c.setErrorFmt("Package '{s}' was installed, but its package.json could not be parsed", .{pkg_name});
2723 return .{ .err = .io_error };
2724 };
2725 defer doc.deinit();
2726
2727 const root_val = doc.root();
2728
2729 if (root_val.getString("bin")) |_| {
2730 const selected = allocator.dupe(u8, simple_name) catch return .{ .err = .out_of_memory };
2731 return .{ .err = .ok, .name = selected };
2732 }
2733
2734 const bin_obj = root_val.getObject("bin") orelse {
2735 c.setErrorFmt("Package '{s}' does not declare any binaries", .{pkg_name});
2736 return .{ .err = .not_found };
2737 };
2738
2739 var iter = bin_obj.objectIterator() orelse {
2740 c.setErrorFmt("Package '{s}' does not declare any usable binaries", .{pkg_name});
2741 return .{ .err = .not_found };
2742 };
2743
2744 var valid_count: usize = 0;
2745 var first_name: []const u8 = "";
2746 var first_target: []const u8 = "";
2747 var package_named_bin: ?[]const u8 = null;
2748 var all_targets_same = true;
2749 var names: std.ArrayList(u8) = .empty;
2750
2751 while (iter.next()) |entry| {
2752 const target = entry.value.asString() orelse continue;
2753
2754 if (valid_count == 0) {
2755 first_name = entry.key;
2756 first_target = normalizedBinTarget(target);
2757 } else if (!std.mem.eql(u8, normalizedBinTarget(target), first_target)) {
2758 all_targets_same = false;
2759 }
2760
2761 appendBinName(&names, allocator, entry.key) catch return .{ .err = .out_of_memory };
2762 valid_count += 1;
2763
2764 if (std.mem.eql(u8, entry.key, simple_name)) {
2765 package_named_bin = entry.key;
2766 }
2767 }
2768
2769 const selected = package_named_bin orelse if (valid_count == 1 or all_targets_same) first_name else "";
2770 if (selected.len > 0) {
2771 const selected_copy = allocator.dupe(u8, selected) catch return .{ .err = .out_of_memory };
2772 return .{ .err = .ok, .name = selected_copy };
2773 }
2774
2775 if (valid_count == 0) {
2776 c.setErrorFmt("Package '{s}' does not declare any usable binaries", .{pkg_name});
2777 } else {
2778 c.setErrorFmt("Package '{s}' exposes multiple binaries ({s}) and no default could be inferred", .{
2779 pkg_name,
2780 names.items,
2781 });
2782 }
2783
2784 return .{ .err = .not_found };
2785}
2786
2787export fn pkg_exec_temp(
2788 ctx: ?*PkgContext,
2789 package_spec: [*:0]const u8,
2790 out_bin_path: [*]u8,
2791 out_bin_path_len: usize,
2792) PkgError {
2793 const c = ctx orelse return .invalid_argument;
2794 _ = c.arena_state.reset(.retain_capacity);
2795 const arena_alloc = c.arena_state.allocator();
2796
2797 const spec_str = std.mem.span(package_spec);
2798
2799 var pkg_name: []const u8 = spec_str;
2800 var bin_name: []const u8 = spec_str;
2801 var version_constraint: []const u8 = "latest";
2802
2803 if (std.mem.indexOf(u8, spec_str, "@")) |at_idx| {
2804 if (at_idx == 0) {
2805 if (std.mem.indexOfPos(u8, spec_str, 1, "@")) |second_at| {
2806 pkg_name = spec_str[0..second_at];
2807 version_constraint = spec_str[second_at + 1 ..];
2808 }
2809 } else {
2810 pkg_name = spec_str[0..at_idx];
2811 version_constraint = spec_str[at_idx + 1 ..];
2812 }
2813 }
2814
2815 if (std.mem.lastIndexOfScalar(u8, pkg_name, '/')) |slash| {
2816 bin_name = pkg_name[slash + 1 ..];
2817 } else {
2818 bin_name = pkg_name;
2819 }
2820
2821 const exec_base = std.fmt.allocPrint(arena_alloc, "{s}/exec", .{c.cache_dir}) catch return .out_of_memory;
2822 const temp_nm_path = std.fmt.allocPrint(arena_alloc, "{s}/{s}", .{exec_base, pkg_name}) catch return .out_of_memory;
2823 const temp_pkg_json = std.fmt.allocPrint(arena_alloc, "{s}/package.json", .{temp_nm_path}) catch return .out_of_memory;
2824 const temp_nm_dir = std.fmt.allocPrint(arena_alloc, "{s}/node_modules", .{temp_nm_path}) catch return .out_of_memory;
2825 const temp_lockfile = std.fmt.allocPrint(arena_alloc, "{s}/ant.lockb", .{temp_nm_path}) catch return .out_of_memory;
2826
2827 if (std.fs.cwd().openDir(exec_base, .{ .iterate = true })) |dir| {
2828 var d = dir;
2829 defer d.close();
2830
2831 const stat = d.statFile(pkg_name) catch null;
2832 if (stat) |s| {
2833 const now: i128 = std.time.nanoTimestamp();
2834 const mtime: i128 = s.mtime;
2835 const age_ns = now - mtime;
2836 const hours_24_ns: i128 = 24 * 60 * 60 * 1_000_000_000;
2837
2838 if (age_ns > hours_24_ns) {
2839 debug.log("exec: cleaning stale cache for {s} (age: {d}h)", .{
2840 pkg_name, @divFloor(age_ns, 60 * 60 * 1_000_000_000),
2841 });
2842 d.deleteTree(pkg_name) catch {};
2843 }
2844 }
2845 } else |_| {}
2846
2847 std.fs.cwd().makePath(temp_nm_path) catch {};
2848
2849 const pkg_json_content = std.fmt.allocPrint(arena_alloc,
2850 \\{{"dependencies":{{"{s}":"{s}"}}}}
2851 , .{pkg_name, version_constraint}) catch return .out_of_memory;
2852
2853 const pkg_json_file = std.fs.cwd().createFile(temp_pkg_json, .{}) catch {
2854 c.setError("Failed to create temp package.json");
2855 return .io_error;
2856 };
2857 pkg_json_file.writeAll(pkg_json_content) catch {
2858 pkg_json_file.close();
2859 c.setError("Failed to write temp package.json");
2860 return .io_error;
2861 };
2862 pkg_json_file.close();
2863
2864 const http = c.http orelse return .network_error;
2865 const db = c.cache_db orelse return .cache_error;
2866
2867 var interleaved = InterleavedContext.init(c.allocator, arena_alloc, db, http, c);
2868 defer interleaved.deinit();
2869
2870 var res = resolver.Resolver.init(
2871 arena_alloc,
2872 c.allocator,
2873 &c.string_pool,
2874 http,
2875 db,
2876 if (c.options.registry_url) |url| std.mem.span(url) else "https://registry.npmjs.org",
2877 &c.metadata_cache,
2878 ); defer res.deinit();
2879
2880 res.setOnPackageResolved(InterleavedContext.onPackageResolved, &interleaved);
2881 res.resolveFromPackageJson(temp_pkg_json) catch |err| {
2882 setResolveError(c, pkg_name, err);
2883 return .resolve_error;
2884 };
2885
2886 debug.log("exec: resolved {d} packages, queued {d} tarballs", .{
2887 interleaved.callbacks_received, interleaved.tarballs_queued,
2888 });
2889
2890 http.run() catch {};
2891
2892 var pkg_linker = linker.Linker.init(c.allocator);
2893 defer pkg_linker.deinit();
2894
2895 pkg_linker.setNodeModulesPath(temp_nm_dir) catch |err| {
2896 c.setErrorFmt("Failed to set up exec directory: {}", .{err});
2897 return .io_error;
2898 };
2899
2900 for (interleaved.extract_contexts.items) |ectx| {
2901 defer ectx.ext.deinit();
2902 if (ectx.has_error) continue;
2903
2904 const stats = ectx.ext.stats();
2905 db.insert(&.{
2906 .integrity = ectx.integrity,
2907 .path = ectx.cache_path,
2908 .unpacked_size = stats.bytes,
2909 .file_count = stats.files,
2910 .cached_at = std.time.timestamp(),
2911 }, ectx.pkg_name, ectx.version_str) catch continue;
2912
2913 pkg_linker.linkPackage(.{
2914 .cache_path = ectx.cache_path,
2915 .node_modules_path = temp_nm_dir,
2916 .name = ectx.pkg_name,
2917 .parent_path = ectx.parent_path,
2918 .file_count = stats.files,
2919 .has_bin = ectx.has_bin,
2920 }) catch continue;
2921 }
2922
2923 var resolved_iter = res.resolved.valueIterator();
2924 while (resolved_iter.next()) |pkg_ptr| {
2925 const pkg = pkg_ptr.*;
2926 if (db.lookup(&pkg.integrity)) |cache_entry| {
2927 var entry = cache_entry;
2928 defer entry.deinit();
2929 const pkg_cache_path = arena_alloc.dupe(u8, entry.path) catch continue;
2930 pkg_linker.linkPackage(.{
2931 .cache_path = pkg_cache_path,
2932 .node_modules_path = temp_nm_dir,
2933 .name = pkg.name.slice(),
2934 .parent_path = pkg.parent_path,
2935 .file_count = entry.file_count,
2936 .has_bin = pkg.has_bin,
2937 }) catch continue;
2938 }
2939 }
2940
2941 res.writeLockfile(temp_lockfile) catch {};
2942
2943 var trusted = std.StringHashMap(void).init(arena_alloc);
2944 var resolved_iter2 = res.resolved.valueIterator();
2945 while (resolved_iter2.next()) |pkg_ptr| {
2946 trusted.put(pkg_ptr.*.name.slice(), {}) catch continue;
2947 }
2948 runTrustedPostinstall(c, &trusted, temp_nm_dir, arena_alloc);
2949
2950 const selected_bin = selectPackageBinName(c, arena_alloc, temp_nm_dir, pkg_name);
2951 if (selected_bin.err != .ok) return selected_bin.err;
2952 bin_name = selected_bin.name;
2953
2954 var bin_path_buf: [std.fs.max_path_bytes]u8 = undefined;
2955 const bin_link_path = std.fmt.bufPrint(&bin_path_buf, "{s}/.bin/{s}", .{ temp_nm_dir, bin_name }) catch return .io_error;
2956
2957 debug.log("exec: looking for bin at {s}", .{bin_link_path});
2958
2959 std.fs.cwd().access(bin_link_path, .{}) catch {
2960 c.setErrorFmt("Binary '{s}' not found in package", .{bin_name});
2961 return .not_found;
2962 };
2963
2964 var real_path_buf: [std.fs.max_path_bytes]u8 = undefined;
2965 const real_path = std.fs.cwd().realpath(bin_link_path, &real_path_buf) catch return .io_error;
2966
2967 if (real_path.len >= out_bin_path_len) return .io_error;
2968
2969 @memcpy(out_bin_path[0..real_path.len], real_path);
2970 out_bin_path[real_path.len] = 0;
2971
2972 return .ok;
2973}
2974
2975fn getGlobalDir(allocator: std.mem.Allocator) ![]const u8 {
2976 if (builtin.os.tag != .windows) {
2977 if (try getLegacyAntDirIfExists(allocator)) |dir| {
2978 defer allocator.free(dir);
2979 return std.fmt.allocPrint(allocator, "{s}/pkg/global", .{dir});
2980 }
2981 if (getAbsoluteEnv("XDG_DATA_HOME")) |base| {
2982 return std.fmt.allocPrint(allocator, "{s}/ant/pkg/global", .{base});
2983 }
2984 const home = try getHomeDir(allocator);
2985 defer allocator.free(home);
2986 return std.fmt.allocPrint(allocator, "{s}/.local/share/ant/pkg/global", .{home});
2987 }
2988
2989 const home = try getHomeDir(allocator);
2990 defer allocator.free(home);
2991 return std.fmt.allocPrint(allocator, "{s}/.ant/pkg/global", .{home});
2992}
2993
2994fn getGlobalBinDir(allocator: std.mem.Allocator) ![]const u8 {
2995 const home = try getHomeDir(allocator);
2996 defer allocator.free(home);
2997 if (builtin.os.tag != .windows) {
2998 if (try getLegacyAntDirIfExists(allocator)) |dir| {
2999 defer allocator.free(dir);
3000 return std.fmt.allocPrint(allocator, "{s}/bin", .{dir});
3001 }
3002 return std.fmt.allocPrint(allocator, "{s}/.local/bin", .{home});
3003 }
3004 return std.fmt.allocPrint(allocator, "{s}/.ant/bin", .{home});
3005}
3006
3007fn ensureGlobalPackageJson(allocator: std.mem.Allocator, global_dir: []const u8) !void {
3008 const pkg_json_path = try std.fmt.allocPrint(allocator, "{s}/package.json", .{global_dir});
3009 defer allocator.free(pkg_json_path);
3010
3011 std.fs.cwd().access(pkg_json_path, .{}) catch {
3012 std.fs.cwd().makePath(global_dir) catch {};
3013 const file = try std.fs.cwd().createFile(pkg_json_path, .{});
3014 defer file.close();
3015 try file.writeAll("{\"dependencies\":{}}\n");
3016 };
3017}
3018
3019fn linkGlobalBins(allocator: std.mem.Allocator, nm_path: []const u8, pkg_name: []const u8) void {
3020 const bin_dir = getGlobalBinDir(allocator) catch return;
3021 defer allocator.free(bin_dir);
3022
3023 std.fs.cwd().makePath(bin_dir) catch return;
3024
3025 const pkg_bin_dir = std.fmt.allocPrint(allocator, "{s}/{s}", .{nm_path, pkg_name}) catch return;
3026 defer allocator.free(pkg_bin_dir);
3027
3028 const pkg_json_path = std.fmt.allocPrint(allocator, "{s}/package.json", .{pkg_bin_dir}) catch return;
3029 defer allocator.free(pkg_json_path);
3030
3031 const content = std.fs.cwd().readFileAlloc(allocator, pkg_json_path, 1024 * 1024) catch return;
3032 defer allocator.free(content);
3033
3034 const parsed = std.json.parseFromSlice(std.json.Value, allocator, content, .{}) catch return;
3035 defer parsed.deinit();
3036
3037 const bin_val = parsed.value.object.get("bin") orelse return;
3038
3039 switch (bin_val) {
3040 .string => |s| {
3041 const base_name = if (std.mem.lastIndexOfScalar(u8, pkg_name, '/')) |idx|
3042 pkg_name[idx + 1..] else pkg_name;
3043 linkSingleBin(allocator, bin_dir, nm_path, pkg_name, base_name, s);
3044 },
3045 .object => |obj| {
3046 for (obj.keys(), obj.values()) |bin_name, path_val| {
3047 if (path_val == .string) linkSingleBin(allocator, bin_dir, nm_path, pkg_name, bin_name, path_val.string);
3048 }
3049 },
3050 else => {},
3051 }
3052}
3053
3054fn linkSingleBin(allocator: std.mem.Allocator, bin_dir: []const u8, nm_path: []const u8, pkg_name: []const u8, bin_name: []const u8, bin_rel_path: []const u8) void {
3055 const target = std.fmt.allocPrint(allocator, "{s}/{s}/{s}", .{nm_path, pkg_name, bin_rel_path}) catch return;
3056 defer allocator.free(target);
3057
3058 const link_path = std.fmt.allocPrint(allocator, "{s}/{s}", .{bin_dir, bin_name}) catch return;
3059 defer allocator.free(link_path);
3060
3061 std.fs.cwd().deleteFile(link_path) catch {};
3062 linker.createSymlinkAbsolute(target, link_path);
3063
3064 debug.log("linked global bin: {s} -> {s}", .{link_path, target});
3065}
3066
3067fn unlinkGlobalBins(allocator: std.mem.Allocator, pkg_name: []const u8) void {
3068 const bin_dir = getGlobalBinDir(allocator) catch return;
3069 defer allocator.free(bin_dir);
3070
3071 var dir = std.fs.cwd().openDir(bin_dir, .{ .iterate = true }) catch return;
3072 defer dir.close();
3073
3074 var iter = dir.iterate();
3075 while (iter.next() catch null) |entry| {
3076 if (entry.kind != .sym_link) continue;
3077
3078 var target_buf: [std.fs.max_path_bytes]u8 = undefined;
3079 const target = dir.readLink(entry.name, &target_buf) catch continue;
3080
3081 const pattern = std.fmt.allocPrint(allocator, "/{s}/", .{pkg_name}) catch continue;
3082 defer allocator.free(pattern);
3083 const pattern_end = std.fmt.allocPrint(allocator, "/{s}", .{pkg_name}) catch continue;
3084 defer allocator.free(pattern_end);
3085
3086 if (std.mem.indexOf(u8, target, pattern) != null or std.mem.endsWith(u8, target, pattern_end)) {
3087 dir.deleteFile(entry.name) catch continue;
3088 debug.log("unlinked global bin: {s}", .{entry.name});
3089 }
3090 }
3091}
3092
3093export fn pkg_add_global(
3094 ctx: ?*PkgContext,
3095 package_spec: [*:0]const u8,
3096) PkgError {
3097 const specs = [_][*:0]const u8{package_spec};
3098 return pkg_add_global_many(ctx, &specs, 1);
3099}
3100
3101export fn pkg_add_global_many(
3102 ctx: ?*PkgContext,
3103 package_specs: [*]const [*:0]const u8,
3104 count: u32,
3105) PkgError {
3106 const c = ctx orelse return .invalid_argument;
3107 const allocator = c.allocator;
3108
3109 const global_dir = getGlobalDir(allocator) catch {
3110 c.setError("HOME not set");
3111 return .invalid_argument;
3112 };
3113 defer allocator.free(global_dir);
3114
3115 ensureGlobalPackageJson(allocator, global_dir) catch {
3116 c.setError("Failed to create global package.json");
3117 return .io_error;
3118 };
3119
3120 const pkg_json_path = std.fmt.allocPrintSentinel(allocator, "{s}/package.json", .{global_dir}, 0) catch return .out_of_memory;
3121 defer allocator.free(pkg_json_path);
3122 const lockfile_path = std.fmt.allocPrintSentinel(allocator, "{s}/ant.lockb", .{global_dir}, 0) catch return .out_of_memory;
3123 defer allocator.free(lockfile_path);
3124 const nm_path = std.fmt.allocPrintSentinel(allocator, "{s}/node_modules", .{global_dir}, 0) catch return .out_of_memory;
3125 defer allocator.free(nm_path);
3126
3127 const add_result = pkg_add_many(c, pkg_json_path.ptr, package_specs, count, false);
3128 if (add_result != .ok) return add_result;
3129
3130 const install_result = pkg_resolve_and_install(c, pkg_json_path.ptr, lockfile_path.ptr, nm_path.ptr);
3131 if (install_result != .ok) return install_result;
3132
3133 for (0..count) |i| {
3134 const spec_str = std.mem.span(package_specs[i]);
3135 var pkg_name: []const u8 = spec_str;
3136
3137 if (std.mem.indexOf(u8, spec_str, "@")) |at_idx| {
3138 if (at_idx == 0) {
3139 if (std.mem.indexOfPos(u8, spec_str, 1, "@")) |second_at| pkg_name = spec_str[0..second_at];
3140 } else pkg_name = spec_str[0..at_idx];
3141 }
3142
3143 linkGlobalBins(allocator, nm_path, pkg_name);
3144 }
3145
3146 return .ok;
3147}
3148
3149export fn pkg_remove_global(
3150 ctx: ?*PkgContext,
3151 package_name: [*:0]const u8,
3152) PkgError {
3153 const c = ctx orelse return .invalid_argument;
3154 const allocator = c.allocator;
3155
3156 const global_dir = getGlobalDir(allocator) catch {
3157 c.setError("HOME not set");
3158 return .invalid_argument;
3159 };
3160 defer allocator.free(global_dir);
3161
3162 const pkg_json_path = std.fmt.allocPrintSentinel(allocator, "{s}/package.json", .{global_dir}, 0) catch return .out_of_memory;
3163 defer allocator.free(pkg_json_path);
3164 const lockfile_path = std.fmt.allocPrintSentinel(allocator, "{s}/ant.lockb", .{global_dir}, 0) catch return .out_of_memory;
3165 defer allocator.free(lockfile_path);
3166 const nm_path = std.fmt.allocPrintSentinel(allocator, "{s}/node_modules", .{global_dir}, 0) catch return .out_of_memory;
3167 defer allocator.free(nm_path);
3168
3169 const name_str = std.mem.span(package_name);
3170
3171 unlinkGlobalBins(allocator, name_str);
3172
3173 const remove_result = pkg_remove(c, pkg_json_path.ptr, package_name);
3174 if (remove_result != .ok and remove_result != .not_found) return remove_result;
3175 if (remove_result == .not_found) return .not_found;
3176
3177 const install_result = pkg_resolve_and_install(c, pkg_json_path.ptr, lockfile_path.ptr, nm_path.ptr);
3178 if (install_result != .ok) return install_result;
3179
3180 return .ok;
3181}
3182
3183export fn pkg_count_local(ctx: ?*PkgContext) u32 {
3184 var pd = cli.get_dependencies(ctx, null, true) orelse return 0;
3185 defer pd.deinit(); return pd.count();
3186}
3187
3188export fn pkg_count_global(ctx: ?*PkgContext) u32 {
3189 const global_dir = getGlobalDir(global_allocator) catch return 0;
3190 var pd = cli.get_dependencies(ctx, global_dir, false) orelse return 0;
3191 defer pd.deinit(); return pd.count();
3192}
3193
3194export fn pkg_list_local(
3195 ctx: ?*PkgContext,
3196 callback: ?*const fn (name: [*:0]const u8, version: [*:0]const u8, user_data: ?*anyopaque) callconv(.c) void,
3197 user_data: ?*anyopaque,
3198) PkgError {
3199 var pd = cli.get_dependencies(ctx, null, true) orelse return .ok;
3200 defer pd.deinit();
3201 if (callback) |cb| cli.list_dependencies(&pd, cb, user_data);
3202 return .ok;
3203}
3204
3205export fn pkg_list_global(
3206 ctx: ?*PkgContext,
3207 callback: ?*const fn (name: [*:0]const u8, version: [*:0]const u8, user_data: ?*anyopaque) callconv(.c) void,
3208 user_data: ?*anyopaque,
3209) PkgError {
3210 const global_dir = getGlobalDir(global_allocator) catch return .invalid_argument;
3211 var pd = cli.get_dependencies(ctx, global_dir, false) orelse return .ok;
3212 defer pd.deinit();
3213 if (callback) |cb| cli.list_dependencies(&pd, cb, user_data);
3214 return .ok;
3215}