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