MIRROR: javascript for 馃悳's, a tiny runtime with big ambitions
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at mir/inline-method 3160 lines 108 kB view raw
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}