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