const std = @import("std"); const builtin = @import("builtin"); pub const cli = @import("cli.zig"); pub const lockfile = @import("lockfile.zig"); pub const cache = @import("cache.zig"); pub const fetcher = @import("fetcher.zig"); pub const extractor = @import("extractor.zig"); pub const linker = @import("linker.zig"); pub const resolver = @import("resolver.zig"); pub const intern = @import("intern.zig"); pub const json = @import("json.zig"); pub const debug = @import("debug.zig"); const global_allocator: std.mem.Allocator = std.heap.c_allocator; fn getHomeDir(allocator: std.mem.Allocator) ![]const u8 { if (builtin.os.tag == .windows) { const home_w = std.process.getenvW( std.unicode.utf8ToUtf16LeStringLiteral("USERPROFILE") ) orelse return error.NoHomeDir; return std.unicode.utf16LeToUtf8Alloc(allocator, home_w) catch error.NoHomeDir; } const home = std.posix.getenv("HOME") orelse return error.NoHomeDir; return allocator.dupe(u8, home); } fn getAbsoluteEnv(name: [:0]const u8) ?[]const u8 { if (builtin.os.tag == .windows) return null; const value = std.posix.getenv(name) orelse return null; if (value.len == 0 or !std.fs.path.isAbsolute(value)) return null; return value; } fn getLegacyAntDirIfExists(allocator: std.mem.Allocator) !?[]const u8 { const home = try getHomeDir(allocator); defer allocator.free(home); const dir = try std.fmt.allocPrint(allocator, "{s}/.ant", .{home}); std.fs.cwd().access(dir, .{}) catch { allocator.free(dir); return null; }; return dir; } pub const PkgError = enum(c_int) { ok = 0, out_of_memory = -1, invalid_lockfile = -2, io_error = -3, network_error = -4, cache_error = -5, extract_error = -6, resolve_error = -7, invalid_argument = -8, not_found = -9, integrity_mismatch = -10, }; pub const ProgressCallback = ?*const fn ( user_data: ?*anyopaque, phase: Phase, current: u32, total: u32, message: [*:0]const u8, ) callconv(.c) void; pub const Phase = enum(c_int) { resolving = 0, fetching = 1, extracting = 2, linking = 3, caching = 4, postinstall = 5, }; pub const PkgOptions = extern struct { cache_dir: ?[*:0]const u8 = null, registry_url: ?[*:0]const u8 = null, max_connections: u32 = 6, progress_callback: ProgressCallback = null, user_data: ?*anyopaque = null, verbose: bool = false, }; pub const CacheStats = extern struct { total_size: u64, db_size: u64, package_count: u32, }; pub const AddedPackage = extern struct { name: [*:0]const u8, version: [*:0]const u8, direct: bool, }; pub const InstallResult = extern struct { package_count: u32, cache_hits: u32, cache_misses: u32, files_linked: u32, files_copied: u32, packages_installed: u32, packages_skipped: u32, elapsed_ms: u64, }; pub const LifecycleScript = extern struct { name: [*:0]const u8, script: [*:0]const u8, }; const PkgInfo = extern struct { name: [*:0]const u8, version: [*:0]const u8, description: [*:0]const u8, license: [*:0]const u8, homepage: [*:0]const u8, tarball: [*:0]const u8, shasum: [*:0]const u8, integrity: [*:0]const u8, keywords: [*:0]const u8, published: [*:0]const u8, dep_count: u32, version_count: u32, unpacked_size: u64, }; const DistTag = extern struct { tag: [*:0]const u8, version: [*:0]const u8, }; const Maintainer = extern struct { name: [*:0]const u8, email: [*:0]const u8, }; const Dependency = extern struct { name: [*:0]const u8, version: [*:0]const u8, }; pub const PkgContext = struct { allocator: std.mem.Allocator, arena_state: std.heap.ArenaAllocator, string_pool: intern.StringPool, cache_db: ?*cache.CacheDB, http: ?*fetcher.Fetcher, options: PkgOptions, last_error: ?[:0]u8, cache_dir: []const u8, metadata_cache: std.StringHashMap(resolver.PackageMetadata), last_install_result: InstallResult, added_packages: std.ArrayListUnmanaged(AddedPackage), added_packages_storage: std.ArrayListUnmanaged([:0]u8), lifecycle_scripts: std.ArrayListUnmanaged(LifecycleScript), lifecycle_scripts_storage: std.ArrayListUnmanaged([:0]u8), info_dist_tags: std.ArrayListUnmanaged(DistTag), info_maintainers: std.ArrayListUnmanaged(Maintainer), info_dependencies: std.ArrayListUnmanaged(Dependency), info_storage: std.ArrayListUnmanaged([:0]u8), pub fn init(allocator: std.mem.Allocator, options: PkgOptions) !*PkgContext { const ctx = try allocator.create(PkgContext); errdefer allocator.destroy(ctx); const default_cache_path = if (options.cache_dir == null) try getDefaultCacheDir(allocator) else null; defer { if (default_cache_path) |path| allocator.free(path); } const cache_path = if (options.cache_dir) |dir| std.mem.span(dir) else default_cache_path.?; ctx.* = .{ .allocator = allocator, .arena_state = std.heap.ArenaAllocator.init(allocator), .string_pool = intern.StringPool.init(allocator), .cache_db = null, .http = null, .options = options, .last_error = null, .cache_dir = try allocator.dupe(u8, cache_path), .metadata_cache = std.StringHashMap(resolver.PackageMetadata).init(allocator), .last_install_result = .{ .package_count = 0, .cache_hits = 0, .cache_misses = 0, .files_linked = 0, .files_copied = 0, .packages_installed = 0, .packages_skipped = 0, .elapsed_ms = 0 }, .added_packages = .{}, .added_packages_storage = .{}, .lifecycle_scripts = .{}, .lifecycle_scripts_storage = .{}, .info_dist_tags = .{}, .info_maintainers = .{}, .info_dependencies = .{}, .info_storage = .{}, }; debug.enabled = options.verbose; debug.log("init: cache_dir={s}", .{ctx.cache_dir}); ctx.cache_db = cache.CacheDB.open(ctx.cache_dir) catch |err| { ctx.setErrorFmt("Failed to open cache database: {}", .{err}); return error.CacheError; }; debug.log("init: cache database opened", .{}); const registry = if (options.registry_url) |url| std.mem.span(url) else "registry.npmjs.org"; ctx.http = fetcher.Fetcher.init(allocator, registry) catch |err| { ctx.setErrorFmt("Failed to initialize fetcher: {}", .{err}); return error.NetworkError; }; debug.log("init: http fetcher ready, registry={s}", .{registry}); return ctx; } pub fn deinit(self: *PkgContext) void { if (self.http) |h| h.deinit(); if (self.cache_db) |db| db.close(); var meta_iter = self.metadata_cache.valueIterator(); while (meta_iter.next()) |meta| meta.deinit(); self.metadata_cache.deinit(); self.string_pool.deinit(); self.arena_state.deinit(); if (self.last_error) |e| self.allocator.free(e); for (self.added_packages_storage.items) |s| self.allocator.free(s); self.added_packages_storage.deinit(self.allocator); self.added_packages.deinit(self.allocator); for (self.lifecycle_scripts_storage.items) |s| self.allocator.free(s); self.lifecycle_scripts_storage.deinit(self.allocator); self.lifecycle_scripts.deinit(self.allocator); for (self.info_storage.items) |s| self.allocator.free(s); self.info_storage.deinit(self.allocator); self.info_dist_tags.deinit(self.allocator); self.info_maintainers.deinit(self.allocator); self.info_dependencies.deinit(self.allocator); self.allocator.free(self.cache_dir); self.allocator.destroy(self); } pub fn setErrorFmt(self: *PkgContext, comptime fmt: []const u8, args: anytype) void { if (self.last_error) |e| self.allocator.free(e); self.last_error = std.fmt.allocPrintSentinel(self.allocator, fmt, args, 0) catch null; } pub fn setError(self: *PkgContext, msg: []const u8) void { if (self.last_error) |e| self.allocator.free(e); self.last_error = self.allocator.dupeZ(u8, msg) catch null; } fn getDefaultCacheDir(allocator: std.mem.Allocator) ![]const u8 { if (builtin.os.tag != .windows) { if (try getLegacyAntDirIfExists(allocator)) |dir| { defer allocator.free(dir); return std.fmt.allocPrint(allocator, "{s}/pkg", .{dir}); } if (getAbsoluteEnv("XDG_CACHE_HOME")) |base| { return std.fmt.allocPrint(allocator, "{s}/ant/pkg", .{base}); } const home = try getHomeDir(allocator); defer allocator.free(home); return std.fmt.allocPrint(allocator, "{s}/.cache/ant/pkg", .{home}); } const home = try getHomeDir(allocator); defer allocator.free(home); return std.fmt.allocPrint(allocator, "{s}/.ant/pkg", .{home}); } fn reportProgress(self: *PkgContext, phase: Phase, current: u32, total: u32, message: [:0]const u8) void { if (self.options.progress_callback) |cb| { cb(self.options.user_data, phase, current, total, message.ptr); } } fn clearAddedPackages(self: *PkgContext) void { for (self.added_packages_storage.items) |s| self.allocator.free(s); self.added_packages_storage.clearRetainingCapacity(); self.added_packages.clearRetainingCapacity(); } fn clearLifecycleScripts(self: *PkgContext) void { for (self.lifecycle_scripts_storage.items) |s| self.allocator.free(s); self.lifecycle_scripts_storage.clearRetainingCapacity(); self.lifecycle_scripts.clearRetainingCapacity(); } fn clearInfo(self: *PkgContext) void { for (self.info_storage.items) |s| self.allocator.free(s); self.info_storage.clearRetainingCapacity(); self.info_dist_tags.clearRetainingCapacity(); self.info_maintainers.clearRetainingCapacity(); self.info_dependencies.clearRetainingCapacity(); } fn storeInfoString(self: *PkgContext, str: []const u8) ![*:0]const u8 { const z = try self.allocator.dupeZ(u8, str); try self.info_storage.append(self.allocator, z); return z.ptr; } fn addLifecycleScript(self: *PkgContext, name: []const u8, script: []const u8) !void { const name_z = try self.allocator.dupeZ(u8, name); errdefer self.allocator.free(name_z); const script_z = try self.allocator.dupeZ(u8, script); errdefer self.allocator.free(script_z); try self.lifecycle_scripts_storage.append(self.allocator, name_z); try self.lifecycle_scripts_storage.append(self.allocator, script_z); try self.lifecycle_scripts.append(self.allocator, .{ .name = name_z.ptr, .script = script_z.ptr, }); } fn addPackageToResults(self: *PkgContext, name: []const u8, version: []const u8, direct: bool) !void { for (self.added_packages.items) |pkg| { if (std.mem.eql(u8, std.mem.span(pkg.name), name)) return; } const name_z = try self.allocator.dupeZ(u8, name); errdefer self.allocator.free(name_z); const version_z = try self.allocator.dupeZ(u8, version); errdefer self.allocator.free(version_z); try self.added_packages_storage.append(self.allocator, name_z); try self.added_packages_storage.append(self.allocator, version_z); try self.added_packages.append(self.allocator, .{ .name = name_z.ptr, .version = version_z.ptr, .direct = direct, }); } pub fn install(self: *PkgContext, lockfile_path: []const u8, node_modules_path: []const u8) !void { _ = self.arena_state.reset(.retain_capacity); const arena_alloc = self.arena_state.allocator(); self.clearAddedPackages(); var timer = std.time.Timer.start() catch return error.OutOfMemory; var stage_start: u64 = @intCast(std.time.nanoTimestamp()); debug.log("install start: lockfile={s} node_modules={s}", .{ lockfile_path, node_modules_path }); var lf = lockfile.Lockfile.open(lockfile_path) catch { self.setError("Failed to open lockfile"); return error.InvalidLockfile; }; defer lf.close(); const pkg_count = lf.header.package_count; stage_start = debug.timer("lockfile open", stage_start); debug.log(" packages in lockfile: {d}", .{pkg_count}); var integrities = try arena_alloc.alloc([64]u8, pkg_count); for (lf.packages, 0..) |pkg, i| { integrities[i] = pkg.integrity; } const db = self.cache_db orelse return error.CacheError; var cache_hits = try db.batchLookup(integrities, arena_alloc); defer cache_hits.deinit(); stage_start = debug.timer("cache lookup", stage_start); var hit_set = std.AutoHashMap(u32, u32).init(arena_alloc); for (cache_hits.items) |hit| { try hit_set.put(hit.index, hit.file_count); } var misses = std.ArrayListUnmanaged(u32){}; for (0..pkg_count) |i| { if (!hit_set.contains(@intCast(i))) { try misses.append(arena_alloc, @intCast(i)); } } debug.log(" cache hits: {d}, misses: {d}", .{ cache_hits.items.len, misses.items.len }); std.sort.heap(cache.CacheDB.BatchHit, cache_hits.items, &lf, batchHitLessThanParentFirst); var pkg_linker = linker.Linker.init(self.allocator); defer pkg_linker.deinit(); try pkg_linker.setNodeModulesPath(node_modules_path); for (cache_hits.items, 0..) |hit, i| { const pkg = &lf.packages[hit.index]; const pkg_name = pkg.name.slice(lf.string_table); const cache_path = try db.getPackagePath(&pkg.integrity, arena_alloc); const parent_path = pkg.parent_path.slice(lf.string_table); const msg = std.fmt.allocPrintSentinel(arena_alloc, "{s}", .{pkg_name}, 0) catch continue; self.reportProgress(.linking, @intCast(i), @intCast(cache_hits.items.len), msg); try pkg_linker.linkPackage(.{ .cache_path = cache_path, .node_modules_path = node_modules_path, .name = pkg_name, .parent_path = if (parent_path.len > 0) parent_path else null, .file_count = hit.file_count, .has_bin = pkg.flags.has_bin, }); if (pkg.flags.direct) { const version_str = pkg.versionString(arena_alloc, lf.string_table) catch continue; self.addPackageToResults(pkg_name, version_str, true) catch {}; } } stage_start = debug.timer("link cache hits", stage_start); if (misses.items.len > 0) { const http = self.http orelse return error.NetworkError; const PkgExtractCtx = struct { ext: *extractor.Extractor, pkg_idx: u32, integrity: [64]u8, cache_path: []const u8, pkg_name: []const u8, version_str: []const u8, direct: bool, parent_path: ?[]const u8, has_bin: bool, completed: bool, has_error: bool, }; var extract_contexts = try arena_alloc.alloc(PkgExtractCtx, misses.items.len); var valid_count: usize = 0; debug.log("queuing {d} tarball fetches...", .{misses.items.len}); for (misses.items, 0..) |pkg_idx, i| { const pkg = &lf.packages[pkg_idx]; const pkg_name = pkg.name.slice(lf.string_table); const tarball_url = pkg.tarball_url.slice(lf.string_table); const version_str = pkg.versionString(arena_alloc, lf.string_table) catch continue; const cache_path = db.getPackagePath(&pkg.integrity, arena_alloc) catch continue; const msg = std.fmt.allocPrintSentinel(arena_alloc, "{s}", .{pkg_name}, 0) catch continue; self.reportProgress(.fetching, @intCast(i), @intCast(misses.items.len), msg); if (self.options.verbose) { debug.log(" queue: {s}@{s}", .{ pkg_name, version_str }); } const ext = extractor.Extractor.init(self.allocator, cache_path) catch continue; const parent_path_str = pkg.parent_path.slice(lf.string_table); extract_contexts[valid_count] = .{ .ext = ext, .pkg_idx = pkg_idx, .integrity = pkg.integrity, .cache_path = cache_path, .pkg_name = pkg_name, .version_str = version_str, .direct = pkg.flags.direct, .parent_path = if (parent_path_str.len > 0) parent_path_str else null, .has_bin = pkg.flags.has_bin, .completed = false, .has_error = false, }; http.fetchTarball(tarball_url, fetcher.StreamHandler.init( struct { fn onData(data: []const u8, user_data: ?*anyopaque) void { const ctx: *PkgExtractCtx = @ptrCast(@alignCast(user_data)); ctx.ext.feedCompressed(data) catch { ctx.has_error = true; }; } }.onData, struct { fn onComplete(_: u16, user_data: ?*anyopaque) void { const ctx: *PkgExtractCtx = @ptrCast(@alignCast(user_data)); ctx.completed = true; } }.onComplete, struct { fn onError(_: fetcher.FetchError, user_data: ?*anyopaque) void { const ctx: *PkgExtractCtx = @ptrCast(@alignCast(user_data)); ctx.has_error = true; ctx.completed = true; } }.onError, &extract_contexts[valid_count], )) catch continue; valid_count += 1; } stage_start = debug.timer("queue fetches", stage_start); debug.log("running event loop for {d} fetches...", .{valid_count}); http.run() catch {}; stage_start = debug.timer("fetch + extract", stage_start); var success_count: usize = 0; var error_count: usize = 0; std.sort.heap(PkgExtractCtx, extract_contexts[0..valid_count], {}, struct { fn lessThan(_: void, a: PkgExtractCtx, b: PkgExtractCtx) bool { const depth_a = linkPathDepth(a.parent_path); const depth_b = linkPathDepth(b.parent_path); if (depth_a != depth_b) return depth_a < depth_b; const parent_a = a.parent_path orelse ""; const parent_b = b.parent_path orelse ""; const parent_order = std.mem.order(u8, parent_a, parent_b); if (parent_order != .eq) return parent_order == .lt; return std.mem.order(u8, a.pkg_name, b.pkg_name) == .lt; } }.lessThan); for (extract_contexts[0..valid_count], 0..) |*ctx, i| { defer ctx.ext.deinit(); if (ctx.has_error) { error_count += 1; debug.log(" error: {s}", .{ctx.pkg_name}); continue; } success_count += 1; const msg = std.fmt.allocPrintSentinel(arena_alloc, "{s}", .{ctx.pkg_name}, 0) catch continue; self.reportProgress(.linking, @intCast(i), @intCast(valid_count), msg); const stats = ctx.ext.stats(); if (stats.files >= 100) { debug.log(" extracted {s}: {d} files, {d} bytes", .{ ctx.pkg_name, stats.files, stats.bytes }); } db.insert(&.{ .integrity = ctx.integrity, .path = ctx.cache_path, .unpacked_size = stats.bytes, .file_count = stats.files, .cached_at = std.time.timestamp(), }, ctx.pkg_name, ctx.version_str) catch continue; self.addPackageToResults(ctx.pkg_name, ctx.version_str, ctx.direct) catch {}; pkg_linker.linkPackage(.{ .cache_path = ctx.cache_path, .node_modules_path = node_modules_path, .name = ctx.pkg_name, .parent_path = ctx.parent_path, .file_count = stats.files, .has_bin = ctx.has_bin, }) catch {}; } stage_start = debug.timer("cache insert + link misses", stage_start); debug.log(" fetched: {d} success, {d} errors", .{ success_count, error_count }); } db.sync(); stage_start = debug.timer("cache sync", stage_start); const link_stats = pkg_linker.getStats(); self.last_install_result = .{ .package_count = pkg_count, .cache_hits = @intCast(cache_hits.items.len), .cache_misses = @intCast(misses.items.len), .files_linked = link_stats.files_linked, .files_copied = link_stats.files_copied, .packages_installed = link_stats.packages_installed, .packages_skipped = link_stats.packages_skipped, .elapsed_ms = timer.read() / 1_000_000, }; } }; fn trySetHttpResolveError(c: *PkgContext, err: anyerror) bool { if (err != error.ResponseError) return false; const http = c.http orelse return false; const info = http.getLastHttpError() orelse return false; c.setErrorFmt("error: GET {s} - {d}", .{ info.url, info.status }); return true; } fn setResolveError(c: *PkgContext, target: ?[]const u8, err: anyerror) void { if (trySetHttpResolveError(c, err)) return; if (target) |name| c.setErrorFmt("Failed to resolve {s}: {}", .{ name, err }) else c.setErrorFmt("Failed to resolve dependencies: {}", .{ err }); } fn linkPathDepth(parent_path: ?[]const u8) u32 { const parent = parent_path orelse return 0; if (parent.len == 0) return 0; var depth: u32 = 1; var it: usize = 0; while (std.mem.indexOfPos(u8, parent, it, "/node_modules/")) |idx| { depth += 1; it = idx + "/node_modules/".len; } return depth; } fn packageLinkLessThanParentFirst(_: void, a: linker.PackageLink, b: linker.PackageLink) bool { const a_depth = linkPathDepth(a.parent_path); const b_depth = linkPathDepth(b.parent_path); if (a_depth != b_depth) return a_depth < b_depth; const a_parent = a.parent_path orelse ""; const b_parent = b.parent_path orelse ""; const parent_order = std.mem.order(u8, a_parent, b_parent); if (parent_order != .eq) return parent_order == .lt; return std.mem.order(u8, a.name, b.name) == .lt; } fn batchHitLessThanParentFirst(lf: *const lockfile.Lockfile, a: cache.CacheDB.BatchHit, b: cache.CacheDB.BatchHit) bool { const pkg_a = &lf.packages[a.index]; const pkg_b = &lf.packages[b.index]; const parent_a = pkg_a.parent_path.slice(lf.string_table); const parent_b = pkg_b.parent_path.slice(lf.string_table); const depth_a = linkPathDepth(if (parent_a.len > 0) parent_a else null); const depth_b = linkPathDepth(if (parent_b.len > 0) parent_b else null); if (depth_a != depth_b) return depth_a < depth_b; const name_a = pkg_a.name.slice(lf.string_table); const name_b = pkg_b.name.slice(lf.string_table); const parent_order = std.mem.order(u8, parent_a, parent_b); if (parent_order != .eq) return parent_order == .lt; return std.mem.order(u8, name_a, name_b) == .lt; } export fn pkg_init(options: *const PkgOptions) ?*PkgContext { return PkgContext.init(global_allocator, options.*) catch null; } export fn pkg_free(ctx: ?*PkgContext) void { if (ctx) |c| c.deinit(); } export fn pkg_install( ctx: ?*PkgContext, package_json_path: [*:0]const u8, lockfile_path: [*:0]const u8, node_modules_path: [*:0]const u8, ) PkgError { const c = ctx orelse return .invalid_argument; _ = c.arena_state.reset(.retain_capacity); const arena_alloc = c.arena_state.allocator(); c.install( std.mem.span(lockfile_path), std.mem.span(node_modules_path), ) catch |err| { return switch (err) { error.InvalidLockfile => .invalid_lockfile, error.CacheError => .cache_error, error.NetworkError => .network_error, error.OutOfMemory => .out_of_memory, else => .io_error, }; }; var pkg_json = json.PackageJson.parse(arena_alloc, std.mem.span(package_json_path)) catch return .ok; defer pkg_json.deinit(arena_alloc); if (pkg_json.trusted_dependencies.count() > 0) { runTrustedPostinstall(c, &pkg_json.trusted_dependencies, std.mem.span(node_modules_path), arena_alloc); } return .ok; } export fn pkg_get_install_result(ctx: ?*PkgContext, out: *InstallResult) PkgError { const c = ctx orelse return .invalid_argument; out.* = c.last_install_result; return .ok; } export fn pkg_get_added_count(ctx: ?*const PkgContext) u32 { const c = ctx orelse return 0; return @intCast(c.added_packages.items.len); } export fn pkg_get_added_package(ctx: ?*const PkgContext, index: u32, out: *AddedPackage) PkgError { const c = ctx orelse return .invalid_argument; if (index >= c.added_packages.items.len) return .invalid_argument; out.* = c.added_packages.items[index]; return .ok; } export fn pkg_get_latest_available_version( ctx: ?*PkgContext, package_name: [*:0]const u8, installed_version: [*:0]const u8, out_version: [*]u8, out_version_len: usize, ) c_int { const c = ctx orelse return 0; if (out_version_len == 0) return 0; out_version[0] = 0; const name = std.mem.span(package_name); const installed_str = std.mem.span(installed_version); const installed = resolver.Version.parse(installed_str) catch return 0; const meta = c.metadata_cache.get(name) orelse return 0; var best: ?*const resolver.VersionInfo = null; for (meta.versions.items) |*v| { if (v.version.prerelease != null) continue; if (!v.matchesPlatform()) continue; if (best == null or v.version.order(best.?.version) == .gt) best = v; } if (best == null) { for (meta.versions.items) |*v| { if (!v.matchesPlatform()) continue; if (best == null or v.version.order(best.?.version) == .gt) best = v; } } const latest = best orelse return 0; if (latest.version.order(installed) != .gt) return 0; if (latest.version_str.len + 1 > out_version_len) return 0; @memcpy(out_version[0..latest.version_str.len], latest.version_str); out_version[latest.version_str.len] = 0; return 1; } export fn pkg_count_installed(node_modules_path: [*:0]const u8) u32 { const nm_path = std.mem.span(node_modules_path); if (!std.mem.endsWith(u8, nm_path, "node_modules")) return 0; const base = nm_path[0 .. nm_path.len - "node_modules".len]; var buf: [std.fs.max_path_bytes]u8 = undefined; const lp = std.fmt.bufPrint(&buf, "{s}ant.lockb", .{base}) catch return 0; var lf = lockfile.Lockfile.open(lp) catch return 0; defer lf.close(); return lf.header.package_count; } export fn pkg_discover_lifecycle_scripts( ctx: ?*PkgContext, node_modules_path: [*:0]const u8, ) PkgError { const c = ctx orelse return .invalid_argument; c.clearLifecycleScripts(); const nm_path = std.mem.span(node_modules_path); var nm_dir = std.fs.cwd().openDir(nm_path, .{ .iterate = true }) catch return .io_error; defer nm_dir.close(); var iter = nm_dir.iterate(); while (iter.next() catch null) |entry| { if (entry.kind != .directory) continue; if (entry.name[0] == '@') { var scope_dir = nm_dir.openDir(entry.name, .{ .iterate = true }) catch continue; defer scope_dir.close(); var scope_iter = scope_dir.iterate(); while (scope_iter.next() catch null) |scoped_entry| { if (scoped_entry.kind != .directory) continue; const full_name = std.fmt.allocPrint(c.allocator, "@{s}/{s}", .{ entry.name[1..], scoped_entry.name }) catch continue; defer c.allocator.free(full_name); discoverPackageScript(c, nm_path, full_name, scope_dir, scoped_entry.name); } } else { discoverPackageScript(c, nm_path, entry.name, nm_dir, entry.name); } } return .ok; } fn discoverPackageScript(ctx: *PkgContext, nm_path: []const u8, pkg_name: []const u8, parent_dir: std.fs.Dir, dir_name: []const u8) void { var pkg_dir = parent_dir.openDir(dir_name, .{}) catch return; defer pkg_dir.close(); pkg_dir.access(".postinstall", .{}) catch |err| { if (err != error.FileNotFound) return; const pkg_json = pkg_dir.openFile("package.json", .{}) catch return; defer pkg_json.close(); const content = pkg_json.readToEndAlloc(ctx.allocator, 1024 * 1024) catch return; defer ctx.allocator.free(content); var doc = json.JsonDoc.parse(content) catch return; defer doc.deinit(); const root = doc.root(); if (root.getObject("scripts")) |scripts| { const script = scripts.getString("postinstall") orelse scripts.getString("install") orelse return; if (std.mem.eql(u8, pkg_name, "esbuild")) return; ctx.addLifecycleScript(pkg_name, script) catch return; } return; }; _ = nm_path; } export fn pkg_get_lifecycle_script_count(ctx: ?*const PkgContext) u32 { const c = ctx orelse return 0; return @intCast(c.lifecycle_scripts.items.len); } export fn pkg_get_lifecycle_script(ctx: ?*const PkgContext, index: u32, out: *LifecycleScript) PkgError { const c = ctx orelse return .invalid_argument; if (index >= c.lifecycle_scripts.items.len) return .invalid_argument; out.* = c.lifecycle_scripts.items[index]; return .ok; } export fn pkg_run_postinstall( ctx: ?*PkgContext, node_modules_path: [*:0]const u8, package_names: [*]const [*:0]const u8, count: u32, ) PkgError { const c = ctx orelse return .invalid_argument; _ = c.arena_state.reset(.retain_capacity); const arena_alloc = c.arena_state.allocator(); var trusted = std.StringHashMap(void).init(arena_alloc); for (0..count) |i| { trusted.put(std.mem.span(package_names[i]), {}) catch continue; } runTrustedPostinstall(c, &trusted, std.mem.span(node_modules_path), arena_alloc); return .ok; } export fn pkg_add_trusted_dependencies( package_json_path: [*:0]const u8, package_names: [*]const [*:0]const u8, count: u32, ) PkgError { const allocator = std.heap.c_allocator; const path = std.mem.span(package_json_path); const path_z = package_json_path; debug.log("[trust] pkg_add_trusted_dependencies: path={s} count={d}", .{ path, count }); const file = std.fs.cwd().openFile(path, .{ .mode = .read_write }) catch |err| { debug.log("[trust] failed to open file: {}", .{err}); return .io_error; }; defer file.close(); const content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch |err| { debug.log("[trust] failed to read file: {}", .{err}); return .io_error; }; defer allocator.free(content); debug.log("[trust] read {d} bytes from package.json", .{content.len}); const doc = json.yyjson.yyjson_read(content.ptr, content.len, 0); if (doc == null) { debug.log("[trust] failed to parse JSON", .{}); return .io_error; } defer json.yyjson.yyjson_doc_free(doc); const mdoc = json.yyjson.yyjson_doc_mut_copy(doc, null); if (mdoc == null) { debug.log("[trust] failed to create mutable doc", .{}); return .out_of_memory; } defer json.yyjson.yyjson_mut_doc_free(mdoc); const root = json.yyjson.yyjson_mut_doc_get_root(mdoc); if (root == null) { debug.log("[trust] failed to get root", .{}); return .io_error; } var trusted_arr = json.yyjson.yyjson_mut_obj_get(root, "trustedDependencies"); if (trusted_arr == null) { debug.log("[trust] creating new trustedDependencies array", .{}); trusted_arr = json.yyjson.yyjson_mut_arr(mdoc); if (trusted_arr == null) { debug.log("[trust] failed to create array", .{}); return .out_of_memory; } _ = json.yyjson.yyjson_mut_obj_add_val(mdoc, root, "trustedDependencies", trusted_arr); } else { debug.log("[trust] trustedDependencies array already exists", .{}); } var string_copies = std.ArrayListUnmanaged([:0]u8){}; defer { for (string_copies.items) |s| allocator.free(s); string_copies.deinit(allocator); } var added: u32 = 0; for (0..count) |i| { const pkg_name = std.mem.span(package_names[i]); var exists = false; var iter = json.yyjson.yyjson_mut_arr_iter{}; _ = json.yyjson.yyjson_mut_arr_iter_init(trusted_arr, &iter); while (json.yyjson.yyjson_mut_arr_iter_next(&iter)) |val| { if (json.yyjson.yyjson_mut_is_str(val)) { const existing = json.yyjson.yyjson_mut_get_str(val); if (existing != null and std.mem.eql(u8, std.mem.span(existing.?), pkg_name)) { exists = true; break; } } } if (!exists) { const name_copy = allocator.dupeZ(u8, pkg_name) catch continue; string_copies.append(allocator, name_copy) catch { allocator.free(name_copy); continue; }; const val = json.yyjson.yyjson_mut_str(mdoc, name_copy.ptr); if (val != null) { _ = json.yyjson.yyjson_mut_arr_append(trusted_arr, val); added += 1; debug.log("[trust] added {s}", .{pkg_name}); } } else { debug.log("[trust] {s} already in trustedDependencies", .{pkg_name}); } } debug.log("[trust] added {d} packages, writing file", .{added}); var write_err: json.yyjson.yyjson_write_err = undefined; const flags = json.yyjson.YYJSON_WRITE_PRETTY_TWO_SPACES | json.yyjson.YYJSON_WRITE_ESCAPE_UNICODE; const written = json.yyjson.yyjson_mut_write_file(path_z, mdoc, flags, null, &write_err); if (!written) { const msg = if (write_err.msg) |m| std.mem.span(m) else "unknown"; debug.log("[trust] failed to write file: code={d} msg={s}", .{ write_err.code, msg }); return .io_error; } debug.log("[trust] successfully wrote package.json", .{}); return .ok; } const InterleavedExtractCtx = struct { ext: *extractor.Extractor, integrity: [64]u8, cache_path: []const u8, pkg_name: []const u8, version_str: []const u8, direct: bool, parent_path: ?[]const u8, has_bin: bool, completed: bool, has_error: bool, queued: bool, parent: *InterleavedContext, }; const InterleavedContext = struct { allocator: std.mem.Allocator, arena_alloc: std.mem.Allocator, db: *cache.CacheDB, http: *fetcher.Fetcher, pkg_ctx: *PkgContext, extract_contexts: std.ArrayListUnmanaged(*InterleavedExtractCtx), queued_integrities: std.AutoHashMap([64]u8, void), callbacks_received: usize, integrity_duplicates: usize, cache_hits: usize, tarballs_queued: usize, tarballs_completed: std.atomic.Value(u32), fn init(allocator: std.mem.Allocator, arena_alloc: std.mem.Allocator, db: *cache.CacheDB, http: *fetcher.Fetcher, pkg_ctx: *PkgContext) InterleavedContext { return .{ .allocator = allocator, .arena_alloc = arena_alloc, .db = db, .http = http, .pkg_ctx = pkg_ctx, .extract_contexts = .{}, .queued_integrities = std.AutoHashMap([64]u8, void).init(arena_alloc), .callbacks_received = 0, .integrity_duplicates = 0, .cache_hits = 0, .tarballs_queued = 0, .tarballs_completed = std.atomic.Value(u32).init(0), }; } fn deinit(self: *InterleavedContext) void { self.extract_contexts.deinit(self.arena_alloc); self.queued_integrities.deinit(); } fn onPackageResolved(pkg: *const resolver.ResolvedPackage, user_data: ?*anyopaque) void { const self: *InterleavedContext = @ptrCast(@alignCast(user_data)); self.callbacks_received += 1; const pkg_name = pkg.name.slice(); const current: u32 = @intCast(self.callbacks_received); const msg = std.fmt.allocPrintSentinel(self.arena_alloc, "{s}", .{pkg_name}, 0) catch return; self.pkg_ctx.reportProgress(.resolving, current, current, msg); if (self.queued_integrities.contains(pkg.integrity)) { self.integrity_duplicates += 1; return; } self.queued_integrities.put(pkg.integrity, {}) catch return; if (self.db.hasIntegrity(&pkg.integrity)) { self.cache_hits += 1; return; } self.tarballs_queued += 1; const cache_path = self.db.getPackagePath(&pkg.integrity, self.arena_alloc) catch return; const version_str = pkg.version.format(self.arena_alloc) catch return; const ext = extractor.Extractor.init(self.allocator, cache_path) catch return; const ctx = self.arena_alloc.create(InterleavedExtractCtx) catch { ext.deinit(); return; }; ctx.* = .{ .ext = ext, .integrity = pkg.integrity, .cache_path = cache_path, .pkg_name = pkg.name.slice(), .version_str = version_str, .direct = pkg.direct, .parent_path = pkg.parent_path, .has_bin = pkg.has_bin, .completed = false, .has_error = false, .queued = false, .parent = self, }; self.http.fetchTarball(pkg.tarball_url, fetcher.StreamHandler.init( struct { fn onData(data: []const u8, ud: ?*anyopaque) void { const c: *InterleavedExtractCtx = @ptrCast(@alignCast(ud)); if (c.has_error) return; c.ext.feedCompressed(data) catch { c.has_error = true; }; } }.onData, struct { fn onComplete(_: u16, ud: ?*anyopaque) void { const c: *InterleavedExtractCtx = @ptrCast(@alignCast(ud)); c.completed = true; const completed = c.parent.tarballs_completed.fetchAdd(1, .monotonic) + 1; const total: u32 = @intCast(c.parent.tarballs_queued); var msg_buf: [256]u8 = undefined; const msg_len = std.fmt.bufPrint(&msg_buf, "{s}", .{c.pkg_name}) catch return; msg_buf[msg_len.len] = 0; c.parent.pkg_ctx.reportProgress(.fetching, completed, total, msg_buf[0..msg_len.len :0]); } }.onComplete, struct { fn onError(_: fetcher.FetchError, ud: ?*anyopaque) void { const c: *InterleavedExtractCtx = @ptrCast(@alignCast(ud)); c.has_error = true; c.completed = true; } }.onError, ctx, )) catch { ext.deinit(); self.arena_alloc.destroy(ctx); return; }; ctx.queued = true; self.extract_contexts.append(self.arena_alloc, ctx) catch return; debug.log(" queued tarball: {s}@{s}", .{ pkg.name.slice(), version_str }); } }; export fn pkg_resolve_and_install( ctx: ?*PkgContext, package_json_path: [*:0]const u8, lockfile_path: [*:0]const u8, node_modules_path: [*:0]const u8, ) PkgError { const c = ctx orelse return .invalid_argument; _ = c.arena_state.reset(.retain_capacity); const arena_alloc = c.arena_state.allocator(); var timer = std.time.Timer.start() catch return .out_of_memory; var stage_start: u64 = @intCast(std.time.nanoTimestamp()); debug.log("resolve+install (interleaved): package_json={s} lockfile={s} node_modules={s}", .{ std.mem.span(package_json_path), std.mem.span(lockfile_path), std.mem.span(node_modules_path), }); const http = c.http orelse return .network_error; http.resetMetaClients(); const db = c.cache_db orelse return .cache_error; const pkg_json_path_z = arena_alloc.dupeZ(u8, std.mem.span(package_json_path)) catch return .out_of_memory; var pkg_json = json.PackageJson.parse(arena_alloc, pkg_json_path_z) catch { c.setError("Failed to parse package.json"); return .io_error; }; defer pkg_json.deinit(arena_alloc); if (pkg_json.trusted_dependencies.count() > 0) { debug.log(" trusted dependencies: {d}", .{pkg_json.trusted_dependencies.count()}); } var interleaved = InterleavedContext.init(c.allocator, arena_alloc, db, http, c); defer interleaved.deinit(); var res = resolver.Resolver.init( arena_alloc, c.allocator, &c.string_pool, http, db, if (c.options.registry_url) |url| std.mem.span(url) else "https://registry.npmjs.org", &c.metadata_cache, ); defer res.deinit(); res.setOnPackageResolved(InterleavedContext.onPackageResolved, &interleaved); res.resolveFromPackageJson(std.mem.span(package_json_path)) catch |err| { setResolveError(c, null, err); return .resolve_error; }; stage_start = debug.timer("resolve + queue tarballs", stage_start); debug.log(" resolved {d} packages, callbacks={d} (dupes={d}), cache hits={d}, queued={d}", .{ res.resolved.count(), interleaved.callbacks_received, interleaved.integrity_duplicates, interleaved.cache_hits, interleaved.tarballs_queued, }); var direct_iter = res.resolved.valueIterator(); while (direct_iter.next()) |pkg_ptr| { const pkg = pkg_ptr.*; if (pkg.direct) { const version_str = pkg.version.format(arena_alloc) catch continue; c.addPackageToResults(pkg.name.slice(), version_str, true) catch {}; } } var pkg_linker = linker.Linker.init(c.allocator); defer pkg_linker.deinit(); pkg_linker.setNodeModulesPath(std.mem.span(node_modules_path)) catch return .io_error; var cache_hit_jobs = std.ArrayListUnmanaged(linker.PackageLink){}; var pkg_iter = res.resolved.valueIterator(); while (pkg_iter.next()) |pkg_ptr| { const pkg = pkg_ptr.*; if (interleaved.queued_integrities.contains(pkg.integrity)) { var is_download = false; for (interleaved.extract_contexts.items) |ext_ctx| { if (std.mem.eql(u8, &ext_ctx.integrity, &pkg.integrity)) { is_download = true; break; } } if (is_download) continue; } var cache_entry = db.lookup(&pkg.integrity) orelse continue; defer cache_entry.deinit(); const cache_path = arena_alloc.dupe(u8, cache_entry.path) catch continue; cache_hit_jobs.append(arena_alloc, .{ .cache_path = cache_path, .node_modules_path = std.mem.span(node_modules_path), .name = pkg.name.slice(), .parent_path = pkg.parent_path, .file_count = cache_entry.file_count, .has_bin = pkg.has_bin, }) catch continue; } var tarball_thread: ?std.Thread = null; if (interleaved.tarballs_queued > 0) { debug.log("finishing {d} tarball downloads (pending={d})...", .{ interleaved.tarballs_queued, http.pending.items.len, }); tarball_thread = std.Thread.spawn(.{}, struct { fn work(h: *fetcher.Fetcher) void { h.run() catch {}; } }.work, .{http}) catch |err| blk: { debug.log("warning: failed to spawn tarball thread: {}, running synchronously", .{err}); http.run() catch {}; break :blk null; }; } std.sort.heap(linker.PackageLink, cache_hit_jobs.items, {}, packageLinkLessThanParentFirst); var linked_count: usize = 0; for (cache_hit_jobs.items, 0..) |job, i| { const msg = std.fmt.allocPrintSentinel(arena_alloc, "{s}", .{job.name}, 0) catch continue; c.reportProgress(.linking, @intCast(i), @intCast(cache_hit_jobs.items.len), msg); pkg_linker.linkPackage(job) catch continue; linked_count += 1; } if (tarball_thread) |t| { t.join(); stage_start = debug.timer("finish tarballs + link cache hits", stage_start); } else stage_start = debug.timer("link cache hits", stage_start); debug.log(" linked {d} from cache", .{linked_count}); res.writeLockfile(std.mem.span(lockfile_path)) catch |err| { c.setErrorFmt("Failed to write lockfile: {}", .{err}); return .io_error; }; stage_start = debug.timer("write lockfile", stage_start); var success_count: usize = 0; var error_count: usize = 0; const LinkJobWithSize = struct { job: linker.PackageLink, size: u64, }; var cache_entries = std.ArrayListUnmanaged(cache.CacheDB.NamedCacheEntry){}; var link_jobs = std.ArrayListUnmanaged(LinkJobWithSize){}; const current_time = std.time.timestamp(); const nm_path = std.mem.span(node_modules_path); for (interleaved.extract_contexts.items) |ext_ctx| { if (ext_ctx.has_error or !ext_ctx.completed) { error_count += 1; debug.log(" error: {s}", .{ext_ctx.pkg_name}); continue; } success_count += 1; const stats = ext_ctx.ext.stats(); cache_entries.append(arena_alloc, .{ .entry = .{ .integrity = ext_ctx.integrity, .path = ext_ctx.cache_path, .unpacked_size = stats.bytes, .file_count = stats.files, .cached_at = current_time, }, .name = ext_ctx.pkg_name, .version = ext_ctx.version_str, }) catch continue; link_jobs.append(arena_alloc, .{ .job = .{ .cache_path = ext_ctx.cache_path, .node_modules_path = nm_path, .name = ext_ctx.pkg_name, .parent_path = ext_ctx.parent_path, .file_count = stats.files, .has_bin = ext_ctx.has_bin, }, .size = stats.bytes, }) catch continue; c.addPackageToResults(ext_ctx.pkg_name, ext_ctx.version_str, ext_ctx.direct) catch {}; } for (cache_entries.items, 0..) |entry, i| { const msg = std.fmt.allocPrintSentinel(arena_alloc, "{s}", .{entry.name}, 0) catch continue; c.reportProgress(.caching, @intCast(i), @intCast(cache_entries.items.len), msg); } db.batchInsertNamed(cache_entries.items) catch {}; stage_start = debug.timer("cache insert (batch)", stage_start); const total_jobs: u32 = @intCast(link_jobs.items.len); var link_counter = std.atomic.Value(u32).init(0); std.sort.heap(LinkJobWithSize, link_jobs.items, {}, struct { fn lessThan(_: void, a: LinkJobWithSize, b: LinkJobWithSize) bool { const depth_a = linkPathDepth(a.job.parent_path); const depth_b = linkPathDepth(b.job.parent_path); if (depth_a != depth_b) return depth_a < depth_b; if (a.size != b.size) return a.size > b.size; const parent_a = a.job.parent_path orelse ""; const parent_b = b.job.parent_path orelse ""; const parent_order = std.mem.order(u8, parent_a, parent_b); if (parent_order != .eq) return parent_order == .lt; return std.mem.order(u8, a.job.name, b.job.name) == .lt; } }.lessThan); var slow_link_count = std.atomic.Value(u32).init(0); var max_link_ms = std.atomic.Value(u64).init(0); var slow_link_names = std.ArrayListUnmanaged([]const u8){}; defer slow_link_names.deinit(c.allocator); var slow_link_lock = std.Thread.Mutex{}; var depth_start: usize = 0; while (depth_start < link_jobs.items.len) { const depth = linkPathDepth(link_jobs.items[depth_start].job.parent_path); var depth_end = depth_start + 1; while (depth_end < link_jobs.items.len and linkPathDepth(link_jobs.items[depth_end].job.parent_path) == depth) : (depth_end += 1) {} const depth_jobs = link_jobs.items[depth_start..depth_end]; const num_threads = @min(8, depth_jobs.len); if (c.options.verbose and depth_jobs.len > 1) { debug.log(" linking depth {d} ({d} items)", .{ depth, depth_jobs.len }); } if (num_threads > 1 and depth_jobs.len > 4) { var threads: [8]?std.Thread = .{null} ** 8; const jobs_per_thread = (depth_jobs.len + num_threads - 1) / num_threads; for (0..num_threads) |t| { const start_idx = t * jobs_per_thread; const end_idx = @min(start_idx + jobs_per_thread, depth_jobs.len); if (start_idx >= end_idx) break; threads[t] = std.Thread.spawn(.{}, struct { 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 { for (jobs) |job_with_size| { const job = job_with_size.job; const current = counter.fetchAdd(1, .monotonic) + 1; var msg_buf: [256]u8 = undefined; const msg_len = std.fmt.bufPrint(&msg_buf, "{s}", .{job.name}) catch continue; msg_buf[msg_len.len] = 0; pkg_ctx.reportProgress(.linking, current, total, msg_buf[0..msg_len.len :0]); const start = std.time.nanoTimestamp(); lnk.linkPackage(job) catch {}; const delta = std.time.nanoTimestamp() - start; const elapsed_ms: u64 = if (delta < 0) 0 else @intCast(@as(u128, @intCast(delta)) / 1_000_000); if (elapsed_ms > 100) { _ = slow_count.fetchAdd(1, .monotonic); lock.lock(); const entry = std.fmt.allocPrint(alloc, "{s} {d}ms", .{ job.name, elapsed_ms }) catch null; if (entry) |val| { names.append(alloc, val) catch {}; } lock.unlock(); var current_max = max_ms.load(.monotonic); while (elapsed_ms > current_max) : (current_max = max_ms.load(.monotonic)) { if (max_ms.cmpxchgWeak(current_max, elapsed_ms, .monotonic, .monotonic) == null) break; } } } } }.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; } for (&threads) |*t| { if (t.*) |thread| thread.join(); } } else { for (depth_jobs) |job_with_size| { const job = job_with_size.job; const current = link_counter.fetchAdd(1, .monotonic) + 1; const msg = std.fmt.allocPrintSentinel(arena_alloc, "{s}", .{job.name}, 0) catch continue; c.reportProgress(.linking, current, total_jobs, msg); const start = std.time.nanoTimestamp(); pkg_linker.linkPackage(job) catch {}; const elapsed_ms: u64 = @intCast((@as(u64, @intCast(std.time.nanoTimestamp())) - @as(u64, @intCast(start))) / 1_000_000); if (elapsed_ms > 100 and c.options.verbose) { debug.log(" link slow: {s} {d}ms", .{ job.name, elapsed_ms }); } } } depth_start = depth_end; } if (c.options.verbose) { debug.log(" link slow (>100ms): {d} max={d}ms", .{ slow_link_count.load(.monotonic), max_link_ms.load(.monotonic) }); for (slow_link_names.items) |entry| { debug.log(" link slow: {s}", .{entry}); } } for (slow_link_names.items) |entry| c.allocator.free(entry); stage_start = debug.timer("link downloads (parallel)", stage_start); debug.log(" downloaded: {d} success, {d} errors", .{ success_count, error_count }); for (interleaved.extract_contexts.items) |ext_ctx| ext_ctx.ext.deinit(); db.sync(); _ = debug.timer("cache sync", stage_start); const link_stats = pkg_linker.getStats(); c.last_install_result = .{ .package_count = @intCast(res.resolved.count()), .cache_hits = @intCast(interleaved.cache_hits), .cache_misses = @intCast(interleaved.tarballs_queued), .files_linked = link_stats.files_linked, .files_copied = link_stats.files_copied, .packages_installed = link_stats.packages_installed, .packages_skipped = link_stats.packages_skipped, .elapsed_ms = timer.read() / 1_000_000, }; debug.log("total: {d} packages in {d}ms", .{ res.resolved.count(), c.last_install_result.elapsed_ms }); if (pkg_json.trusted_dependencies.count() > 0) { runTrustedPostinstall(c, &pkg_json.trusted_dependencies, std.mem.span(node_modules_path), arena_alloc); } return .ok; } const PostinstallJob = struct { pkg_name: []const u8, pkg_dir: []const u8, script: []const u8, child: ?std.process.Child = null, exit_code: ?u8 = null, stderr: ?[]const u8 = null, failed: bool = false, }; fn runTrustedPostinstall( ctx: *PkgContext, trusted: *std.StringHashMap(void), node_modules_path: []const u8, allocator: std.mem.Allocator, ) void { var env_map = std.process.getEnvMap(allocator) catch return; defer env_map.deinit(); const cwd = std.fs.cwd(); const abs_nm_path = cwd.realpathAlloc(allocator, node_modules_path) catch return; defer allocator.free(abs_nm_path); const bin_path = std.fmt.allocPrint(allocator, "{s}/.bin", .{abs_nm_path}) catch return; defer allocator.free(bin_path); const current_path = env_map.get("PATH") orelse ""; const new_path = if (builtin.os.tag == .windows) std.fmt.allocPrint(allocator, "{s};{s}", .{ bin_path, current_path }) catch return else std.fmt.allocPrint(allocator, "{s}:{s}", .{ bin_path, current_path }) catch return; defer allocator.free(new_path); env_map.put("PATH", new_path) catch return; var jobs = std.ArrayListUnmanaged(PostinstallJob){}; defer { for (jobs.items) |*job| if (job.stderr) |s| allocator.free(s); jobs.deinit(allocator); } var key_iter = trusted.keyIterator(); while (key_iter.next()) |pkg_name_ptr| { const pkg_name = pkg_name_ptr.*; const pkg_json_path = std.fmt.allocPrint(allocator, "{s}/{s}/package.json", .{ node_modules_path, pkg_name, }) catch continue; defer allocator.free(pkg_json_path); const file = std.fs.cwd().openFile(pkg_json_path, .{}) catch continue; defer file.close(); const content = file.readToEndAlloc(allocator, 1024 * 1024) catch continue; defer allocator.free(content); var doc = json.JsonDoc.parse(content) catch continue; defer doc.deinit(); const root = doc.root(); if (root.getObject("scripts")) |scripts| { const script = scripts.getString("postinstall") orelse scripts.getString("install") orelse continue; if (std.mem.eql(u8, pkg_name, "esbuild")) { debug.log("ignoring esbuild lifecycle scripts", .{}); continue; } const pkg_dir = std.fmt.allocPrint(allocator, "{s}/{s}", .{ node_modules_path, pkg_name, }) catch continue; const marker_path = std.fmt.allocPrint(allocator, "{s}/.postinstall", .{pkg_dir}) catch continue; defer allocator.free(marker_path); if (std.fs.cwd().access(marker_path, .{})) |_| { debug.log("postinstall already done: {s}", .{pkg_name}); allocator.free(pkg_dir); continue; } else |_| {} jobs.append(allocator, .{ .pkg_name = pkg_name, .pkg_dir = pkg_dir, .script = allocator.dupe(u8, script) catch continue, }) catch continue; } } if (jobs.items.len == 0) return; for (jobs.items) |job| debug.log("starting postinstall: {s}", .{job.pkg_name}); for (jobs.items, 0..) |*job, i| { const msg = std.fmt.allocPrintSentinel(allocator, "{s}", .{job.pkg_name}, 0) catch continue; ctx.reportProgress(.postinstall, @intCast(i), @intCast(jobs.items.len), msg); debug.log("running postinstall: {s}", .{job.pkg_name}); const shell_argv: []const []const u8 = if (builtin.os.tag == .windows) &[_][]const u8{ "cmd", "/c", job.script } else &[_][]const u8{ "sh", "-c", job.script }; var child = std.process.Child.init(shell_argv, allocator); child.cwd = job.pkg_dir; child.env_map = &env_map; child.stderr_behavior = .Pipe; child.stdout_behavior = .Pipe; child.spawn() catch { job.failed = true; continue; }; job.child = child; } var scripts_run: u32 = 0; for (jobs.items) |*job| { if (job.child) |*child| { var stdout_buf: std.ArrayList(u8) = .empty; var stderr_buf: std.ArrayList(u8) = .empty; child.collectOutput(allocator, &stdout_buf, &stderr_buf, 1024 * 1024) catch {}; const term = child.wait() catch { stdout_buf.deinit(allocator); stderr_buf.deinit(allocator); job.failed = true; continue; }; if (stdout_buf.items.len > 0) { var line_iter = std.mem.splitScalar(u8, stdout_buf.items, '\n'); while (line_iter.next()) |line| { if (line.len > 0) debug.log(" {s}: {s}", .{ job.pkg_name, line }); } } stdout_buf.deinit(allocator); switch (term) { .Exited => |code| { if (code != 0) { job.exit_code = code; job.stderr = if (stderr_buf.items.len > 0) stderr_buf.toOwnedSlice(allocator) catch null else null; debug.log(" postinstall failed for {s}: exit code {d}", .{ job.pkg_name, code }); if (job.stderr) |s| { if (s.len > 0) debug.log(" stderr: {s}", .{s}); } } else { stderr_buf.deinit(allocator); scripts_run += 1; const marker_path = std.fmt.allocPrint(allocator, "{s}/.postinstall", .{job.pkg_dir}) catch continue; defer allocator.free(marker_path); if (std.fs.cwd().createFile(marker_path, .{})) |f| f.close() else |_| {} } }, .Signal => |sig| { job.failed = true; debug.log(" postinstall killed by signal {d}: {s}", .{ sig, job.pkg_name }); stderr_buf.deinit(allocator); }, else => { job.failed = true; stderr_buf.deinit(allocator); }, } } } for (jobs.items) |job| { allocator.free(job.pkg_dir); allocator.free(job.script); } if (scripts_run > 0) debug.log("ran {d} postinstall scripts", .{scripts_run}); } export fn pkg_add( ctx: ?*PkgContext, package_json_path: [*:0]const u8, package_spec: [*:0]const u8, dev: bool, ) PkgError { const c = ctx orelse return .invalid_argument; _ = c.arena_state.reset(.retain_capacity); const arena_alloc = c.arena_state.allocator(); const pkg_json_str = std.mem.span(package_json_path); const spec_str = std.mem.span(package_spec); var pkg_name: []const u8 = spec_str; var version_constraint: []const u8 = "latest"; if (std.mem.indexOf(u8, spec_str, "@")) |at_idx| { if (at_idx == 0) { if (std.mem.indexOfPos(u8, spec_str, 1, "@")) |second_at| { pkg_name = spec_str[0..second_at]; version_constraint = spec_str[second_at + 1 ..]; } } else { pkg_name = spec_str[0..at_idx]; version_constraint = spec_str[at_idx + 1 ..]; } } const http = c.http orelse return .network_error; var res = resolver.Resolver.init( arena_alloc, c.allocator, &c.string_pool, http, c.cache_db, if (c.options.registry_url) |url| std.mem.span(url) else "https://registry.npmjs.org", &c.metadata_cache, ); defer res.deinit(); const resolved_pkg = res.resolve(pkg_name, version_constraint, 0) catch |err| { setResolveError(c, pkg_name, err); return .resolve_error; }; const content = blk: { const file = std.fs.cwd().openFile(pkg_json_str, .{ .mode = .read_only }) catch |err| { if (err == error.FileNotFound) break :blk "{}"; c.setError("Failed to open package.json"); return .io_error; }; defer file.close(); break :blk file.readToEndAlloc(arena_alloc, 10 * 1024 * 1024) catch { c.setError("Failed to read package.json"); return .io_error; }; }; const parsed = std.json.parseFromSlice(std.json.Value, arena_alloc, content, .{}) catch { c.setError("Failed to parse package.json"); return .invalid_argument; }; defer parsed.deinit(); if (parsed.value != .object) { c.setError("Invalid package.json format"); return .invalid_argument; } const version_str = resolved_pkg.version.format(arena_alloc) catch { return .out_of_memory; }; const version_with_caret = std.fmt.allocPrint(arena_alloc, "^{s}", .{version_str}) catch { return .out_of_memory; }; const target_key = if (dev) "devDependencies" else "dependencies"; var deps = if (parsed.value.object.get(target_key)) |d| if (d == .object) d.object else std.json.ObjectMap.init(arena_alloc) else std.json.ObjectMap.init(arena_alloc); deps.put(pkg_name, .{ .string = version_with_caret }) catch { return .out_of_memory; }; var writer = json.JsonWriter.init() catch { return .out_of_memory; }; defer writer.deinit(); const root_obj = writer.createObject(); writer.setRoot(root_obj); var found_target = false; for (parsed.value.object.keys(), parsed.value.object.values()) |key, value| { if (std.mem.eql(u8, key, target_key)) { found_target = true; const deps_obj = writer.createObject(); var dep_iter = deps.iterator(); while (dep_iter.next()) |entry| { if (entry.value_ptr.* == .string) { writer.objectAdd(deps_obj, entry.key_ptr.*, writer.createString(entry.value_ptr.string)); } } writer.objectAdd(root_obj, key, deps_obj); } else { const json_val = jsonValueToMut(&writer, value) catch continue; writer.objectAdd(root_obj, key, json_val); } } if (!found_target) { const deps_obj = writer.createObject(); writer.objectAdd(deps_obj, pkg_name, writer.createString(version_with_caret)); writer.objectAdd(root_obj, target_key, deps_obj); } const pkg_json_z = arena_alloc.dupeZ(u8, pkg_json_str) catch { return .out_of_memory; }; writer.writeToFile(pkg_json_z) catch { c.setError("Failed to write package.json"); return .io_error; }; return .ok; } export fn pkg_add_many( ctx: ?*PkgContext, package_json_path: [*:0]const u8, package_specs: [*]const [*:0]const u8, count: u32, dev: bool, ) PkgError { const c = ctx orelse return .invalid_argument; _ = c.arena_state.reset(.retain_capacity); const arena_alloc = c.arena_state.allocator(); const pkg_json_str = std.mem.span(package_json_path); const http = c.http orelse return .network_error; var res = resolver.Resolver.init( arena_alloc, c.allocator, &c.string_pool, http, c.cache_db, if (c.options.registry_url) |url| std.mem.span(url) else "https://registry.npmjs.org", &c.metadata_cache, ); defer res.deinit(); const ResolvedEntry = struct { name: []const u8, version_str: []const u8, }; const resolved = arena_alloc.alloc(ResolvedEntry, count) catch return .out_of_memory; for (0..count) |i| { const spec_str = std.mem.span(package_specs[i]); var pkg_name: []const u8 = spec_str; var version_constraint: []const u8 = "latest"; if (std.mem.indexOf(u8, spec_str, "@")) |at_idx| { if (at_idx == 0) { if (std.mem.indexOfPos(u8, spec_str, 1, "@")) |second_at| { pkg_name = spec_str[0..second_at]; version_constraint = spec_str[second_at + 1 ..]; } } else { pkg_name = spec_str[0..at_idx]; version_constraint = spec_str[at_idx + 1 ..]; } } const resolved_pkg = res.resolve(pkg_name, version_constraint, 0) catch |err| { setResolveError(c, pkg_name, err); return .resolve_error; }; const version_str = resolved_pkg.version.format(arena_alloc) catch return .out_of_memory; resolved[i] = .{ .name = pkg_name, .version_str = version_str }; } const content = blk: { const file = std.fs.cwd().openFile(pkg_json_str, .{ .mode = .read_only }) catch |err| { if (err == error.FileNotFound) break :blk "{}"; c.setError("Failed to open package.json"); return .io_error; }; defer file.close(); break :blk file.readToEndAlloc(arena_alloc, 10 * 1024 * 1024) catch { c.setError("Failed to read package.json"); return .io_error; }; }; const parsed = std.json.parseFromSlice(std.json.Value, arena_alloc, content, .{}) catch { c.setError("Failed to parse package.json"); return .invalid_argument; }; defer parsed.deinit(); if (parsed.value != .object) { c.setError("Invalid package.json format"); return .invalid_argument; } const target_key = if (dev) "devDependencies" else "dependencies"; var deps = if (parsed.value.object.get(target_key)) |d| if (d == .object) d.object else std.json.ObjectMap.init(arena_alloc) else std.json.ObjectMap.init(arena_alloc); for (resolved) |entry| { const version_with_caret = std.fmt.allocPrint(arena_alloc, "^{s}", .{entry.version_str}) catch return .out_of_memory; deps.put(entry.name, .{ .string = version_with_caret }) catch return .out_of_memory; } var writer = json.JsonWriter.init() catch return .out_of_memory; defer writer.deinit(); const root_obj = writer.createObject(); writer.setRoot(root_obj); var found_target = false; for (parsed.value.object.keys(), parsed.value.object.values()) |key, value| { if (std.mem.eql(u8, key, target_key)) { found_target = true; const deps_obj = writer.createObject(); var dep_iter = deps.iterator(); while (dep_iter.next()) |entry| { if (entry.value_ptr.* == .string) { writer.objectAdd(deps_obj, entry.key_ptr.*, writer.createString(entry.value_ptr.string)); } } writer.objectAdd(root_obj, key, deps_obj); } else { const json_val = jsonValueToMut(&writer, value) catch continue; writer.objectAdd(root_obj, key, json_val); } } if (!found_target) { const deps_obj = writer.createObject(); for (resolved) |entry| { const version_with_caret = std.fmt.allocPrint(arena_alloc, "^{s}", .{entry.version_str}) catch return .out_of_memory; writer.objectAdd(deps_obj, entry.name, writer.createString(version_with_caret)); } writer.objectAdd(root_obj, target_key, deps_obj); } const pkg_json_z = arena_alloc.dupeZ(u8, pkg_json_str) catch return .out_of_memory; writer.writeToFile(pkg_json_z) catch { c.setError("Failed to write package.json"); return .io_error; }; return .ok; } fn jsonValueToMut(writer: *json.JsonWriter, value: std.json.Value) !*json.yyjson.yyjson_mut_val { return switch (value) { .null => writer.createNull(), .bool => |b| writer.createBool(b), .integer => |i| writer.createInt(i), .float => |f| writer.createReal(f), .string => |s| writer.createString(s), .array => |arr| blk: { const json_arr = writer.createArray(); for (arr.items) |item| { const item_val = try jsonValueToMut(writer, item); writer.arrayAppend(json_arr, item_val); } break :blk json_arr; }, .object => |obj| blk: { const json_obj = writer.createObject(); for (obj.keys(), obj.values()) |k, v| { const v_mut = try jsonValueToMut(writer, v); writer.objectAdd(json_obj, k, v_mut); } break :blk json_obj; }, .number_string => |s| writer.createString(s), }; } export fn pkg_remove( ctx: ?*PkgContext, package_json_path: [*:0]const u8, package_name: [*:0]const u8, ) PkgError { const c = ctx orelse return .invalid_argument; _ = c.arena_state.reset(.retain_capacity); const arena_alloc = c.arena_state.allocator(); const pkg_json_str = std.mem.span(package_json_path); const name_str = std.mem.span(package_name); const content = std.fs.cwd().readFileAlloc(arena_alloc, pkg_json_str, 10 * 1024 * 1024) catch { c.setError("Failed to read package.json"); return .io_error; }; const parsed = std.json.parseFromSlice(std.json.Value, arena_alloc, content, .{}) catch { c.setError("Failed to parse package.json"); return .invalid_argument; }; defer parsed.deinit(); if (parsed.value != .object) { c.setError("Invalid package.json format"); return .invalid_argument; } const dep_keys = [_][]const u8{ "dependencies", "devDependencies", "peerDependencies", "optionalDependencies" }; const found = found: { for (dep_keys) |dep_key| { const deps = parsed.value.object.get(dep_key) orelse continue; if (deps != .object) continue; if (deps.object.get(name_str) != null) break :found true; } break :found false; }; if (!found) { c.setErrorFmt("Package {s} not found in dependencies", .{name_str}); return .not_found; } var writer = json.JsonWriter.init() catch return .out_of_memory; defer writer.deinit(); const root_obj = writer.createObject(); writer.setRoot(root_obj); for (parsed.value.object.keys(), parsed.value.object.values()) |key, value| { const is_dep_obj = for (dep_keys) |dk| { if (std.mem.eql(u8, key, dk)) break true; } else false; if (!is_dep_obj or value != .object) { const val_mut = jsonValueToMut(&writer, value) catch continue; writer.objectAdd(root_obj, key, val_mut); continue; } const filtered_obj = writer.createObject(); for (value.object.keys(), value.object.values()) |dk, dv| { if (std.mem.eql(u8, dk, name_str)) continue; const dv_mut = jsonValueToMut(&writer, dv) catch continue; writer.objectAdd(filtered_obj, dk, dv_mut); } writer.objectAdd(root_obj, key, filtered_obj); } const pkg_json_z = arena_alloc.dupeZ(u8, pkg_json_str) catch return .out_of_memory; writer.writeToFile(pkg_json_z) catch { c.setError("Failed to write package.json"); return .io_error; }; return .ok; } export fn pkg_error_string(ctx: ?*const PkgContext) [*:0]const u8 { if (ctx) |c| if (c.last_error) |e| return e.ptr; return "Unknown error"; } export fn pkg_cache_sync(ctx: ?*PkgContext) void { if (ctx) |c| if (c.cache_db) |db| db.sync(); } export fn pkg_cache_stats(ctx: ?*PkgContext, out: *CacheStats) PkgError { const c = ctx orelse return .invalid_argument; const db = c.cache_db orelse return .cache_error; const stats = db.stats() catch return .cache_error; out.* = .{ .total_size = stats.cache_size, .db_size = stats.db_size, .package_count = @intCast(stats.entries), }; return .ok; } export fn pkg_cache_prune(ctx: ?*PkgContext, max_age_days: u32) i32 { const c = ctx orelse return @intFromEnum(PkgError.invalid_argument); const db = c.cache_db orelse return @intFromEnum(PkgError.cache_error); const pruned = db.prune(max_age_days) catch return @intFromEnum(PkgError.cache_error); return @intCast(pruned); } export fn pkg_get_bin_path( node_modules_path: [*:0]const u8, bin_name: [*:0]const u8, out_path: [*]u8, out_path_len: usize, ) c_int { const nm_path = std.mem.span(node_modules_path); const full = std.mem.span(bin_name); const start: usize = if (full.len > 0 and full[0] == '@') (std.mem.indexOfScalar(u8, full[1..], '/') orelse return -1) + 2 else 0; const name, const constraint_str = if (std.mem.indexOfScalar(u8, full[start..], '@')) |i| .{ full[0..start + i], full[start + i + 1..] } else .{ full, @as([]const u8, "") }; var path_buf: [std.fs.max_path_bytes]u8 = undefined; const bin_path = std.fmt.bufPrint(&path_buf, "{s}/.bin/{s}", .{ nm_path, name }) catch return -1; std.fs.cwd().access(bin_path, .{}) catch return -1; if (constraint_str.len > 0) { const constraint = resolver.Constraint.parse(constraint_str) catch return -1; if (constraint.kind != .any) { var pkg_buf: [std.fs.max_path_bytes]u8 = undefined; const pkg_path = std.fmt.bufPrint(&pkg_buf, "{s}/{s}/package.json", .{ nm_path, name }) catch return -1; const pkg_path_z = pkg_buf[0..pkg_path.len :0]; var doc = json.JsonDoc.parseFile(pkg_path_z) catch return -1; defer doc.deinit(); const version_str = doc.root().getString("version") orelse return -1; const installed = resolver.Version.parse(version_str) catch return -1; if (!constraint.satisfies(installed)) return -1; } } var real_path_buf: [std.fs.max_path_bytes]u8 = undefined; const real_path = std.fs.cwd().realpath(bin_path, &real_path_buf) catch return -1; if (real_path.len >= out_path_len) return -1; @memcpy(out_path[0..real_path.len], real_path); out_path[real_path.len] = 0; return @intCast(real_path.len); } export fn pkg_list_bins( node_modules_path: [*:0]const u8, callback: ?*const fn ([*:0]const u8, ?*anyopaque) callconv(.c) void, user_data: ?*anyopaque, ) c_int { const nm_path = std.mem.span(node_modules_path); var path_buf: [std.fs.max_path_bytes]u8 = undefined; const bin_dir_path = std.fmt.bufPrint(&path_buf, "{s}/.bin", .{nm_path}) catch return -1; var dir = std.fs.cwd().openDir(bin_dir_path, .{ .iterate = true }) catch return -1; defer dir.close(); var count: c_int = 0; var iter = dir.iterate(); while (iter.next() catch null) |entry| { if (entry.kind == .sym_link or entry.kind == .file) { if (callback) |cb| { var name_buf: [256]u8 = undefined; if (entry.name.len < name_buf.len) { @memcpy(name_buf[0..entry.name.len], entry.name); name_buf[entry.name.len] = 0; cb(@ptrCast(&name_buf), user_data); } } count += 1; } } return count; } export fn pkg_list_package_bins( node_modules_path: [*:0]const u8, package_name: [*:0]const u8, callback: ?*const fn ([*:0]const u8, ?*anyopaque) callconv(.c) void, user_data: ?*anyopaque, ) c_int { const nm_path = std.mem.span(node_modules_path); const pkg_name = std.mem.span(package_name); var path_buf: [std.fs.max_path_bytes]u8 = undefined; const pkg_json_path = std.fmt.bufPrint(&path_buf, "{s}/{s}/package.json", .{ nm_path, pkg_name }) catch return -1; const file = std.fs.cwd().openFile(pkg_json_path, .{}) catch return -1; defer file.close(); const content = file.readToEndAlloc(global_allocator, 1024 * 1024) catch return -1; defer global_allocator.free(content); var doc = json.JsonDoc.parse(content) catch return -1; defer doc.deinit(); const root_val = doc.root(); var count: c_int = 0; if (root_val.getObject("bin")) |bin_obj| { var iter = bin_obj.objectIterator() orelse return 0; while (iter.next()) |entry| { if (callback) |cb| { var name_buf: [256]u8 = undefined; if (entry.key.len < name_buf.len) { @memcpy(name_buf[0..entry.key.len], entry.key); name_buf[entry.key.len] = 0; cb(@ptrCast(&name_buf), user_data); } } count += 1; } } else if (root_val.getString("bin")) |_| { const simple_name = if (std.mem.indexOf(u8, pkg_name, "/")) |slash| pkg_name[slash + 1 ..] else pkg_name; if (callback) |cb| { var name_buf: [256]u8 = undefined; if (simple_name.len < name_buf.len) { @memcpy(name_buf[0..simple_name.len], simple_name); name_buf[simple_name.len] = 0; cb(@ptrCast(&name_buf), user_data); } } count = 1; } return count; } export fn pkg_get_script( package_json_path: [*:0]const u8, script_name: [*:0]const u8, out_script: [*]u8, out_script_len: usize, ) c_int { const allocator = global_allocator; const name = std.mem.span(script_name); var doc = json.JsonDoc.parseFile(std.mem.span(package_json_path)) catch return -1; defer doc.deinit(); const root_val = doc.root(); if (root_val.getObject("scripts")) |scripts_obj| { if (scripts_obj.getString(std.mem.span(script_name))) |script| { if (script.len >= out_script_len) return -1; @memcpy(out_script[0..script.len], script); out_script[script.len] = 0; return @intCast(script.len); } } if (std.mem.eql(u8, name, "start")) { if (root_val.getString("main")) |main_file| { const script = std.fmt.allocPrint(allocator, "ant {s}", .{main_file}) catch return -1; defer allocator.free(script); if (script.len >= out_script_len) return -1; @memcpy(out_script[0..script.len], script); out_script[script.len] = 0; return @intCast(script.len); } if (std.fs.cwd().access("server.js", .{})) |_| { const script = "ant server.js"; if (script.len >= out_script_len) return -1; @memcpy(out_script[0..script.len], script); out_script[script.len] = 0; return @intCast(script.len); } else |_| {} } return -1; } pub const ScriptResult = extern struct { exit_code: c_int, signal: c_int, }; fn runScriptCommand( allocator: std.mem.Allocator, script: []const u8, extra_args: ?[*:0]const u8, env_map: *std.process.EnvMap, ) !ScriptResult { const final_script = if (extra_args) |args| blk: { const args_str = std.mem.span(args); if (args_str.len > 0) { break :blk try std.fmt.allocPrint(allocator, "{s} {s}", .{ script, args_str }); } break :blk try allocator.dupe(u8, script); } else try allocator.dupe(u8, script); defer allocator.free(final_script); const script_z = try allocator.dupeZ(u8, final_script); defer allocator.free(script_z); const shell_argv: []const []const u8 = if (builtin.os.tag == .windows) &[_][]const u8{ "cmd", "/c", script_z } else &[_][]const u8{ "sh", "-c", script_z }; var child = std.process.Child.init(shell_argv, allocator); child.env_map = env_map; try child.spawn(); const term = try child.wait(); return switch (term) { .Exited => |code| .{ .exit_code = code, .signal = 0 }, .Signal => |sig| .{ .exit_code = -1, .signal = @intCast(sig) }, else => .{ .exit_code = -1, .signal = 0 }, }; } export fn pkg_run_script( package_json_path: [*:0]const u8, script_name: [*:0]const u8, node_modules_path: [*:0]const u8, extra_args: ?[*:0]const u8, result: ?*ScriptResult, ) PkgError { const allocator = global_allocator; const name = std.mem.span(script_name); var doc = json.JsonDoc.parseFile(std.mem.span(package_json_path)) catch return .io_error; defer doc.deinit(); const root_val = doc.root(); var script_buf: [8192]u8 = undefined; const script_len = pkg_get_script(package_json_path, script_name, &script_buf, script_buf.len); if (script_len < 0) return .not_found; const script = script_buf[0..@intCast(script_len)]; var pre_script: ?[]const u8 = null; var post_script: ?[]const u8 = null; if (root_val.getObject("scripts")) |scripts_obj| { var pre_key_buf: [256]u8 = undefined; var post_key_buf: [256]u8 = undefined; const pre_key = std.fmt.bufPrintZ(&pre_key_buf, "pre{s}", .{name}) catch null; const post_key = std.fmt.bufPrintZ(&post_key_buf, "post{s}", .{name}) catch null; if (pre_key) |pk| pre_script = scripts_obj.getString(pk); if (post_key) |pk| post_script = scripts_obj.getString(pk); } var env_map = std.process.getEnvMap(allocator) catch return .out_of_memory; defer env_map.deinit(); const nm_path = std.mem.span(node_modules_path); const cwd = std.fs.cwd(); const abs_nm_path = cwd.realpathAlloc(allocator, nm_path) catch nm_path; defer if (abs_nm_path.ptr != nm_path.ptr) allocator.free(abs_nm_path); const bin_path = std.fmt.allocPrint(allocator, "{s}/.bin", .{abs_nm_path}) catch return .out_of_memory; defer allocator.free(bin_path); const current_path = env_map.get("PATH") orelse ""; const new_path = if (builtin.os.tag == .windows) std.fmt.allocPrint(allocator, "{s};{s}", .{ bin_path, current_path }) catch return .out_of_memory else std.fmt.allocPrint(allocator, "{s}:{s}", .{ bin_path, current_path }) catch return .out_of_memory; defer allocator.free(new_path); env_map.put("PATH", new_path) catch return .out_of_memory; env_map.put("npm_lifecycle_event", name) catch {}; if (root_val.getObject("config")) |config_obj| { if (config_obj.objectIterator()) |*config_iter_ptr| { var config_iter = config_iter_ptr.*; while (config_iter.next()) |entry| { if (entry.value.asString()) |value| { const env_key = std.fmt.allocPrint(allocator, "npm_package_config_{s}", .{entry.key}) catch continue; defer allocator.free(env_key); env_map.put(env_key, value) catch {}; } } } } if (root_val.getString("name")) |pkg_name| env_map.put("npm_package_name", pkg_name) catch {}; if (root_val.getString("version")) |pkg_version| env_map.put("npm_package_version", pkg_version) catch {}; if (pre_script) |pre| { const pre_event = std.fmt.allocPrint(allocator, "pre{s}", .{name}) catch name; defer if (pre_event.ptr != name.ptr) allocator.free(pre_event); env_map.put("npm_lifecycle_event", pre_event) catch {}; const pre_result = runScriptCommand(allocator, pre, null, &env_map) catch return .io_error; if (pre_result.exit_code != 0) { if (result) |r| r.* = pre_result; return .ok; } } env_map.put("npm_lifecycle_event", name) catch {}; const main_result = runScriptCommand(allocator, script, extra_args, &env_map) catch return .io_error; if (main_result.exit_code != 0) { if (result) |r| r.* = main_result; return .ok; } if (post_script) |post| { const post_event = std.fmt.allocPrint(allocator, "post{s}", .{name}) catch name; defer if (post_event.ptr != name.ptr) allocator.free(post_event); env_map.put("npm_lifecycle_event", post_event) catch {}; const post_result = runScriptCommand(allocator, post, null, &env_map) catch return .io_error; if (result) |r| r.* = post_result; return .ok; } if (result) |r| r.* = main_result; return .ok; } pub const DepType = packed struct(u8) { peer: bool = false, dev: bool = false, optional: bool = false, direct: bool = false, _reserved: u4 = 0, }; pub const DepCallback = ?*const fn ( name: [*:0]const u8, version: [*:0]const u8, constraint: [*:0]const u8, dep_type: DepType, user_data: ?*anyopaque, ) callconv(.c) void; pub const WhyInfo = extern struct { target_version: [64]u8, found: bool, is_peer: bool, is_dev: bool, is_direct: bool, }; export fn pkg_why_info( lockfile_path: [*:0]const u8, package_name: [*:0]const u8, out: *WhyInfo, ) c_int { const lf = lockfile.Lockfile.open(std.mem.span(lockfile_path)) catch return -1; defer @constCast(&lf).close(); const target_name = std.mem.span(package_name); out.found = false; out.is_peer = false; out.is_dev = false; out.is_direct = false; @memset(&out.target_version, 0); for (lf.packages) |*pkg| { const pkg_name = pkg.name.slice(lf.string_table); if (std.mem.eql(u8, pkg_name, target_name)) { const ver_str = pkg.versionString(global_allocator, lf.string_table) catch return -1; defer global_allocator.free(ver_str); if (ver_str.len < out.target_version.len) { @memcpy(out.target_version[0..ver_str.len], ver_str); out.target_version[ver_str.len] = 0; } out.found = true; out.is_dev = pkg.flags.dev; out.is_direct = pkg.flags.direct; } const deps = lf.getPackageDeps(pkg); for (deps) |dep| { const dep_pkg = &lf.packages[dep.package_index]; const dep_name = dep_pkg.name.slice(lf.string_table); if (std.mem.eql(u8, dep_name, target_name) and dep.flags.peer) { out.is_peer = true; } } } return 0; } export fn pkg_why( lockfile_path: [*:0]const u8, package_name: [*:0]const u8, callback: DepCallback, user_data: ?*anyopaque, ) c_int { const lf = lockfile.Lockfile.open(std.mem.span(lockfile_path)) catch return -1; defer @constCast(&lf).close(); const target_name = std.mem.span(package_name); var count: c_int = 0; var name_buf: [512]u8 = undefined; var ver_buf: [64]u8 = undefined; var constraint_buf: [128]u8 = undefined; for (lf.packages) |*pkg| { const deps = lf.getPackageDeps(pkg); for (deps) |dep| { const dep_pkg = &lf.packages[dep.package_index]; const dep_name = dep_pkg.name.slice(lf.string_table); if (std.mem.eql(u8, dep_name, target_name)) { const pkg_name = pkg.name.slice(lf.string_table); const constraint = dep.constraint.slice(lf.string_table); if (callback) |cb| { if (pkg_name.len < name_buf.len) { @memcpy(name_buf[0..pkg_name.len], pkg_name); name_buf[pkg_name.len] = 0; const ver_str = pkg.versionString(global_allocator, lf.string_table) catch continue; defer global_allocator.free(ver_str); if (ver_str.len < ver_buf.len) { @memcpy(ver_buf[0..ver_str.len], ver_str); ver_buf[ver_str.len] = 0; if (constraint.len < constraint_buf.len) { @memcpy(constraint_buf[0..constraint.len], constraint); constraint_buf[constraint.len] = 0; const dep_type = DepType{ .peer = dep.flags.peer, .dev = dep.flags.dev or pkg.flags.dev, .optional = dep.flags.optional, .direct = pkg.flags.direct, }; cb(@ptrCast(&name_buf), @ptrCast(&ver_buf), @ptrCast(&constraint_buf), dep_type, user_data); } } } } count += 1; } } } for (lf.packages) |*pkg| { const pkg_name = pkg.name.slice(lf.string_table); if (std.mem.eql(u8, pkg_name, target_name) and pkg.flags.direct) { if (callback) |cb| { const direct_str = "package.json"; var direct_buf: [16]u8 = undefined; @memcpy(direct_buf[0..direct_str.len], direct_str); direct_buf[direct_str.len] = 0; var empty_buf: [1]u8 = .{0}; var dep_buf: [16]u8 = undefined; const constraint_str = "dependencies"; @memcpy(dep_buf[0..constraint_str.len], constraint_str); dep_buf[constraint_str.len] = 0; const dep_type = DepType{ .peer = false, .dev = pkg.flags.dev, .optional = false, .direct = true, }; cb(@ptrCast(&direct_buf), @ptrCast(&empty_buf), @ptrCast(&dep_buf), dep_type, user_data); } count += 1; } } return count; } export fn pkg_list_scripts( package_json_path: [*:0]const u8, callback: ?*const fn ([*:0]const u8, [*:0]const u8, ?*anyopaque) callconv(.c) void, user_data: ?*anyopaque, ) c_int { var doc = json.JsonDoc.parseFile(std.mem.span(package_json_path)) catch return -1; defer doc.deinit(); const root_val = doc.root(); const scripts_obj = root_val.getObject("scripts") orelse return 0; var iter = scripts_obj.objectIterator() orelse return 0; defer iter.deinit(); var count: c_int = 0; while (iter.next()) |entry| { if (callback) |cb| { var name_buf: [256]u8 = undefined; var cmd_buf: [4096]u8 = undefined; if (entry.key.len < name_buf.len) { @memcpy(name_buf[0..entry.key.len], entry.key); name_buf[entry.key.len] = 0; if (entry.value.asString()) |cmd| { if (cmd.len < cmd_buf.len) { @memcpy(cmd_buf[0..cmd.len], cmd); cmd_buf[cmd.len] = 0; cb(@ptrCast(&name_buf), @ptrCast(&cmd_buf), user_data); } } } } count += 1; } return count; } export fn pkg_info( ctx: ?*PkgContext, package_spec: [*:0]const u8, out: *PkgInfo, ) PkgError { const c = ctx orelse return .invalid_argument; c.clearInfo(); _ = c.arena_state.reset(.retain_capacity); const arena = c.arena_state.allocator(); const http = c.http orelse return .network_error; const spec = std.mem.span(package_spec); var name: []const u8 = spec; var requested_version: ?[]const u8 = null; if (std.mem.lastIndexOf(u8, spec, "@")) |at_idx| { if (at_idx > 0) { name = spec[0..at_idx]; requested_version = spec[at_idx + 1..]; } } const data = http.fetchMetadataFull(name, true, c.allocator) catch |err| { c.setErrorFmt("Failed to fetch package info: {}", .{err}); return .network_error; }; defer c.allocator.free(data); var doc = json.JsonDoc.parse(data) catch { c.setError("Failed to parse package metadata"); return .resolve_error; }; defer doc.deinit(); const root = doc.root(); const versions_obj = root.getObject("versions") orelse { c.setError("No versions found"); return .not_found; }; var versions_iter = versions_obj.objectIterator() orelse return .resolve_error; defer versions_iter.deinit(); var version_count: u32 = 0; while (versions_iter.next()) |_| version_count += 1; var version_str: []const u8 = ""; if (requested_version) |rv| { version_str = rv; } else if (root.getObject("dist-tags")) |tags| { version_str = tags.getString("latest") orelse ""; } const version_z = arena.dupeZ(u8, version_str) catch return .out_of_memory; const version_obj = versions_obj.getObject(version_z) orelse { c.setErrorFmt("Version {s} not found", .{version_str}); return .not_found; }; var dep_count: u32 = 0; if (version_obj.getObject("dependencies")) |deps| { var deps_iter = deps.objectIterator() orelse return .resolve_error; defer deps_iter.deinit(); while (deps_iter.next()) |entry| { dep_count += 1; if (entry.value.asString()) |ver| { c.info_dependencies.append(c.allocator, .{ .name = c.storeInfoString(entry.key) catch continue, .version = c.storeInfoString(ver) catch continue, }) catch continue; } } } const dist = version_obj.getObject("dist"); const unpacked_size: u64 = if (dist) |d| @as(u64, @intCast(d.getInt("unpackedSize") orelse 0)) else 0; var keywords_buf = std.ArrayListUnmanaged(u8){}; defer keywords_buf.deinit(c.allocator); if (version_obj.getArray("keywords")) |kw_arr| { var kw_iter = kw_arr.arrayIterator() orelse return .resolve_error; defer kw_iter.deinit(); var first = true; while (kw_iter.next()) |kw_val| { if (kw_val.asString()) |kw| { if (!first) keywords_buf.appendSlice(c.allocator, ", ") catch {}; keywords_buf.appendSlice(c.allocator, kw) catch {}; first = false; } } } out.* = .{ .name = c.storeInfoString(root.getString("name") orelse name) catch return .out_of_memory, .version = c.storeInfoString(version_str) catch return .out_of_memory, .description = c.storeInfoString(version_obj.getString("description") orelse "") catch return .out_of_memory, .license = c.storeInfoString(version_obj.getString("license") orelse "") catch return .out_of_memory, .homepage = c.storeInfoString(version_obj.getString("homepage") orelse "") catch return .out_of_memory, .tarball = c.storeInfoString(if (dist) |d| d.getString("tarball") orelse "" else "") catch return .out_of_memory, .shasum = c.storeInfoString(if (dist) |d| d.getString("shasum") orelse "" else "") catch return .out_of_memory, .integrity = c.storeInfoString(if (dist) |d| d.getString("integrity") orelse "" else "") catch return .out_of_memory, .keywords = c.storeInfoString(keywords_buf.items) catch return .out_of_memory, .published = c.storeInfoString(if (root.getObject("time")) |t| t.getString(version_z) orelse "" else "") catch return .out_of_memory, .dep_count = dep_count, .version_count = version_count, .unpacked_size = unpacked_size, }; if (root.getObject("dist-tags")) |tags| { var tags_iter = tags.objectIterator() orelse return .ok; defer tags_iter.deinit(); while (tags_iter.next()) |entry| { if (entry.value.asString()) |ver| { c.info_dist_tags.append(c.allocator, .{ .tag = c.storeInfoString(entry.key) catch continue, .version = c.storeInfoString(ver) catch continue, }) catch continue; } } } if (root.getArray("maintainers")) |maint_arr| { var maint_iter = maint_arr.arrayIterator() orelse return .ok; defer maint_iter.deinit(); while (maint_iter.next()) |maint_val| { const maint_name = maint_val.getString("name") orelse continue; const maint_email = maint_val.getString("email") orelse ""; c.info_maintainers.append(c.allocator, .{ .name = c.storeInfoString(maint_name) catch continue, .email = c.storeInfoString(maint_email) catch continue, }) catch continue; } } return .ok; } export fn pkg_info_dist_tag_count(ctx: ?*const PkgContext) u32 { const c = ctx orelse return 0; return @intCast(c.info_dist_tags.items.len); } export fn pkg_info_get_dist_tag(ctx: ?*const PkgContext, index: u32, out: *DistTag) PkgError { const c = ctx orelse return .invalid_argument; if (index >= c.info_dist_tags.items.len) return .invalid_argument; out.* = c.info_dist_tags.items[index]; return .ok; } export fn pkg_info_maintainer_count(ctx: ?*const PkgContext) u32 { const c = ctx orelse return 0; return @intCast(c.info_maintainers.items.len); } export fn pkg_info_get_maintainer(ctx: ?*const PkgContext, index: u32, out: *Maintainer) PkgError { const c = ctx orelse return .invalid_argument; if (index >= c.info_maintainers.items.len) return .invalid_argument; out.* = c.info_maintainers.items[index]; return .ok; } export fn pkg_info_dependency_count(ctx: ?*const PkgContext) u32 { const c = ctx orelse return 0; return @intCast(c.info_dependencies.items.len); } export fn pkg_info_get_dependency(ctx: ?*const PkgContext, index: u32, out: *Dependency) PkgError { const c = ctx orelse return .invalid_argument; if (index >= c.info_dependencies.items.len) return .invalid_argument; out.* = c.info_dependencies.items[index]; return .ok; } const BinSelection = struct { err: PkgError, name: []const u8 = "", }; fn packageSimpleName(pkg_name: []const u8) []const u8 { if (std.mem.lastIndexOfScalar(u8, pkg_name, '/')) |slash| { return pkg_name[slash + 1 ..]; } return pkg_name; } fn normalizedBinTarget(path: []const u8) []const u8 { if (std.mem.startsWith(u8, path, "./")) return path[2..]; return path; } fn appendBinName(list: *std.ArrayList(u8), allocator: std.mem.Allocator, name: []const u8) !void { if (list.items.len > 0) try list.appendSlice(allocator, ", "); try list.appendSlice(allocator, name); } fn selectPackageBinName( c: *PkgContext, allocator: std.mem.Allocator, node_modules_path: []const u8, pkg_name: []const u8, ) BinSelection { const simple_name = packageSimpleName(pkg_name); const pkg_json_path = std.fmt.allocPrint(allocator, "{s}/{s}/package.json", .{ node_modules_path, pkg_name, }) catch return .{ .err = .out_of_memory }; const file = std.fs.cwd().openFile(pkg_json_path, .{}) catch { c.setErrorFmt("Package '{s}' was installed, but its package.json could not be opened", .{pkg_name}); return .{ .err = .io_error }; }; defer file.close(); const content = file.readToEndAlloc(allocator, 1024 * 1024) catch { c.setErrorFmt("Package '{s}' was installed, but its package.json could not be read", .{pkg_name}); return .{ .err = .io_error }; }; var doc = json.JsonDoc.parse(content) catch { c.setErrorFmt("Package '{s}' was installed, but its package.json could not be parsed", .{pkg_name}); return .{ .err = .io_error }; }; defer doc.deinit(); const root_val = doc.root(); if (root_val.getString("bin")) |_| { const selected = allocator.dupe(u8, simple_name) catch return .{ .err = .out_of_memory }; return .{ .err = .ok, .name = selected }; } const bin_obj = root_val.getObject("bin") orelse { c.setErrorFmt("Package '{s}' does not declare any binaries", .{pkg_name}); return .{ .err = .not_found }; }; var iter = bin_obj.objectIterator() orelse { c.setErrorFmt("Package '{s}' does not declare any usable binaries", .{pkg_name}); return .{ .err = .not_found }; }; var valid_count: usize = 0; var first_name: []const u8 = ""; var first_target: []const u8 = ""; var package_named_bin: ?[]const u8 = null; var all_targets_same = true; var names: std.ArrayList(u8) = .empty; while (iter.next()) |entry| { const target = entry.value.asString() orelse continue; if (valid_count == 0) { first_name = entry.key; first_target = normalizedBinTarget(target); } else if (!std.mem.eql(u8, normalizedBinTarget(target), first_target)) { all_targets_same = false; } appendBinName(&names, allocator, entry.key) catch return .{ .err = .out_of_memory }; valid_count += 1; if (std.mem.eql(u8, entry.key, simple_name)) { package_named_bin = entry.key; } } const selected = package_named_bin orelse if (valid_count == 1 or all_targets_same) first_name else ""; if (selected.len > 0) { const selected_copy = allocator.dupe(u8, selected) catch return .{ .err = .out_of_memory }; return .{ .err = .ok, .name = selected_copy }; } if (valid_count == 0) { c.setErrorFmt("Package '{s}' does not declare any usable binaries", .{pkg_name}); } else { c.setErrorFmt("Package '{s}' exposes multiple binaries ({s}) and no default could be inferred", .{ pkg_name, names.items, }); } return .{ .err = .not_found }; } export fn pkg_exec_temp( ctx: ?*PkgContext, package_spec: [*:0]const u8, out_bin_path: [*]u8, out_bin_path_len: usize, ) PkgError { const c = ctx orelse return .invalid_argument; _ = c.arena_state.reset(.retain_capacity); const arena_alloc = c.arena_state.allocator(); const spec_str = std.mem.span(package_spec); var pkg_name: []const u8 = spec_str; var bin_name: []const u8 = spec_str; var version_constraint: []const u8 = "latest"; if (std.mem.indexOf(u8, spec_str, "@")) |at_idx| { if (at_idx == 0) { if (std.mem.indexOfPos(u8, spec_str, 1, "@")) |second_at| { pkg_name = spec_str[0..second_at]; version_constraint = spec_str[second_at + 1 ..]; } } else { pkg_name = spec_str[0..at_idx]; version_constraint = spec_str[at_idx + 1 ..]; } } if (std.mem.lastIndexOfScalar(u8, pkg_name, '/')) |slash| { bin_name = pkg_name[slash + 1 ..]; } else { bin_name = pkg_name; } const exec_base = std.fmt.allocPrint(arena_alloc, "{s}/exec", .{c.cache_dir}) catch return .out_of_memory; const temp_nm_path = std.fmt.allocPrint(arena_alloc, "{s}/{s}", .{exec_base, pkg_name}) catch return .out_of_memory; const temp_pkg_json = std.fmt.allocPrint(arena_alloc, "{s}/package.json", .{temp_nm_path}) catch return .out_of_memory; const temp_nm_dir = std.fmt.allocPrint(arena_alloc, "{s}/node_modules", .{temp_nm_path}) catch return .out_of_memory; const temp_lockfile = std.fmt.allocPrint(arena_alloc, "{s}/ant.lockb", .{temp_nm_path}) catch return .out_of_memory; if (std.fs.cwd().openDir(exec_base, .{ .iterate = true })) |dir| { var d = dir; defer d.close(); const stat = d.statFile(pkg_name) catch null; if (stat) |s| { const now: i128 = std.time.nanoTimestamp(); const mtime: i128 = s.mtime; const age_ns = now - mtime; const hours_24_ns: i128 = 24 * 60 * 60 * 1_000_000_000; if (age_ns > hours_24_ns) { debug.log("exec: cleaning stale cache for {s} (age: {d}h)", .{ pkg_name, @divFloor(age_ns, 60 * 60 * 1_000_000_000), }); d.deleteTree(pkg_name) catch {}; } } } else |_| {} std.fs.cwd().makePath(temp_nm_path) catch {}; const pkg_json_content = std.fmt.allocPrint(arena_alloc, \\{{"dependencies":{{"{s}":"{s}"}}}} , .{pkg_name, version_constraint}) catch return .out_of_memory; const pkg_json_file = std.fs.cwd().createFile(temp_pkg_json, .{}) catch { c.setError("Failed to create temp package.json"); return .io_error; }; pkg_json_file.writeAll(pkg_json_content) catch { pkg_json_file.close(); c.setError("Failed to write temp package.json"); return .io_error; }; pkg_json_file.close(); const http = c.http orelse return .network_error; const db = c.cache_db orelse return .cache_error; var interleaved = InterleavedContext.init(c.allocator, arena_alloc, db, http, c); defer interleaved.deinit(); var res = resolver.Resolver.init( arena_alloc, c.allocator, &c.string_pool, http, db, if (c.options.registry_url) |url| std.mem.span(url) else "https://registry.npmjs.org", &c.metadata_cache, ); defer res.deinit(); res.setOnPackageResolved(InterleavedContext.onPackageResolved, &interleaved); res.resolveFromPackageJson(temp_pkg_json) catch |err| { setResolveError(c, pkg_name, err); return .resolve_error; }; debug.log("exec: resolved {d} packages, queued {d} tarballs", .{ interleaved.callbacks_received, interleaved.tarballs_queued, }); http.run() catch {}; var pkg_linker = linker.Linker.init(c.allocator); defer pkg_linker.deinit(); pkg_linker.setNodeModulesPath(temp_nm_dir) catch |err| { c.setErrorFmt("Failed to set up exec directory: {}", .{err}); return .io_error; }; for (interleaved.extract_contexts.items) |ectx| { defer ectx.ext.deinit(); if (ectx.has_error) continue; const stats = ectx.ext.stats(); db.insert(&.{ .integrity = ectx.integrity, .path = ectx.cache_path, .unpacked_size = stats.bytes, .file_count = stats.files, .cached_at = std.time.timestamp(), }, ectx.pkg_name, ectx.version_str) catch continue; pkg_linker.linkPackage(.{ .cache_path = ectx.cache_path, .node_modules_path = temp_nm_dir, .name = ectx.pkg_name, .parent_path = ectx.parent_path, .file_count = stats.files, .has_bin = ectx.has_bin, }) catch continue; } var resolved_iter = res.resolved.valueIterator(); while (resolved_iter.next()) |pkg_ptr| { const pkg = pkg_ptr.*; if (db.lookup(&pkg.integrity)) |cache_entry| { var entry = cache_entry; defer entry.deinit(); const pkg_cache_path = arena_alloc.dupe(u8, entry.path) catch continue; pkg_linker.linkPackage(.{ .cache_path = pkg_cache_path, .node_modules_path = temp_nm_dir, .name = pkg.name.slice(), .parent_path = pkg.parent_path, .file_count = entry.file_count, .has_bin = pkg.has_bin, }) catch continue; } } res.writeLockfile(temp_lockfile) catch {}; var trusted = std.StringHashMap(void).init(arena_alloc); var resolved_iter2 = res.resolved.valueIterator(); while (resolved_iter2.next()) |pkg_ptr| { trusted.put(pkg_ptr.*.name.slice(), {}) catch continue; } runTrustedPostinstall(c, &trusted, temp_nm_dir, arena_alloc); const selected_bin = selectPackageBinName(c, arena_alloc, temp_nm_dir, pkg_name); if (selected_bin.err != .ok) return selected_bin.err; bin_name = selected_bin.name; var bin_path_buf: [std.fs.max_path_bytes]u8 = undefined; const bin_link_path = std.fmt.bufPrint(&bin_path_buf, "{s}/.bin/{s}", .{ temp_nm_dir, bin_name }) catch return .io_error; debug.log("exec: looking for bin at {s}", .{bin_link_path}); std.fs.cwd().access(bin_link_path, .{}) catch { c.setErrorFmt("Binary '{s}' not found in package", .{bin_name}); return .not_found; }; var real_path_buf: [std.fs.max_path_bytes]u8 = undefined; const real_path = std.fs.cwd().realpath(bin_link_path, &real_path_buf) catch return .io_error; if (real_path.len >= out_bin_path_len) return .io_error; @memcpy(out_bin_path[0..real_path.len], real_path); out_bin_path[real_path.len] = 0; return .ok; } fn getGlobalDir(allocator: std.mem.Allocator) ![]const u8 { if (builtin.os.tag != .windows) { if (try getLegacyAntDirIfExists(allocator)) |dir| { defer allocator.free(dir); return std.fmt.allocPrint(allocator, "{s}/pkg/global", .{dir}); } if (getAbsoluteEnv("XDG_DATA_HOME")) |base| { return std.fmt.allocPrint(allocator, "{s}/ant/pkg/global", .{base}); } const home = try getHomeDir(allocator); defer allocator.free(home); return std.fmt.allocPrint(allocator, "{s}/.local/share/ant/pkg/global", .{home}); } const home = try getHomeDir(allocator); defer allocator.free(home); return std.fmt.allocPrint(allocator, "{s}/.ant/pkg/global", .{home}); } fn getGlobalBinDir(allocator: std.mem.Allocator) ![]const u8 { const home = try getHomeDir(allocator); defer allocator.free(home); if (builtin.os.tag != .windows) { if (try getLegacyAntDirIfExists(allocator)) |dir| { defer allocator.free(dir); return std.fmt.allocPrint(allocator, "{s}/bin", .{dir}); } return std.fmt.allocPrint(allocator, "{s}/.local/bin", .{home}); } return std.fmt.allocPrint(allocator, "{s}/.ant/bin", .{home}); } fn ensureGlobalPackageJson(allocator: std.mem.Allocator, global_dir: []const u8) !void { const pkg_json_path = try std.fmt.allocPrint(allocator, "{s}/package.json", .{global_dir}); defer allocator.free(pkg_json_path); std.fs.cwd().access(pkg_json_path, .{}) catch { std.fs.cwd().makePath(global_dir) catch {}; const file = try std.fs.cwd().createFile(pkg_json_path, .{}); defer file.close(); try file.writeAll("{\"dependencies\":{}}\n"); }; } fn linkGlobalBins(allocator: std.mem.Allocator, nm_path: []const u8, pkg_name: []const u8) void { const bin_dir = getGlobalBinDir(allocator) catch return; defer allocator.free(bin_dir); std.fs.cwd().makePath(bin_dir) catch return; const pkg_bin_dir = std.fmt.allocPrint(allocator, "{s}/{s}", .{nm_path, pkg_name}) catch return; defer allocator.free(pkg_bin_dir); const pkg_json_path = std.fmt.allocPrint(allocator, "{s}/package.json", .{pkg_bin_dir}) catch return; defer allocator.free(pkg_json_path); const content = std.fs.cwd().readFileAlloc(allocator, pkg_json_path, 1024 * 1024) catch return; defer allocator.free(content); const parsed = std.json.parseFromSlice(std.json.Value, allocator, content, .{}) catch return; defer parsed.deinit(); const bin_val = parsed.value.object.get("bin") orelse return; switch (bin_val) { .string => |s| { const base_name = if (std.mem.lastIndexOfScalar(u8, pkg_name, '/')) |idx| pkg_name[idx + 1..] else pkg_name; linkSingleBin(allocator, bin_dir, nm_path, pkg_name, base_name, s); }, .object => |obj| { for (obj.keys(), obj.values()) |bin_name, path_val| { if (path_val == .string) linkSingleBin(allocator, bin_dir, nm_path, pkg_name, bin_name, path_val.string); } }, else => {}, } } fn 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 { const target = std.fmt.allocPrint(allocator, "{s}/{s}/{s}", .{nm_path, pkg_name, bin_rel_path}) catch return; defer allocator.free(target); const link_path = std.fmt.allocPrint(allocator, "{s}/{s}", .{bin_dir, bin_name}) catch return; defer allocator.free(link_path); std.fs.cwd().deleteFile(link_path) catch {}; linker.createSymlinkAbsolute(target, link_path); debug.log("linked global bin: {s} -> {s}", .{link_path, target}); } fn unlinkGlobalBins(allocator: std.mem.Allocator, pkg_name: []const u8) void { const bin_dir = getGlobalBinDir(allocator) catch return; defer allocator.free(bin_dir); var dir = std.fs.cwd().openDir(bin_dir, .{ .iterate = true }) catch return; defer dir.close(); var iter = dir.iterate(); while (iter.next() catch null) |entry| { if (entry.kind != .sym_link) continue; var target_buf: [std.fs.max_path_bytes]u8 = undefined; const target = dir.readLink(entry.name, &target_buf) catch continue; const pattern = std.fmt.allocPrint(allocator, "/{s}/", .{pkg_name}) catch continue; defer allocator.free(pattern); const pattern_end = std.fmt.allocPrint(allocator, "/{s}", .{pkg_name}) catch continue; defer allocator.free(pattern_end); if (std.mem.indexOf(u8, target, pattern) != null or std.mem.endsWith(u8, target, pattern_end)) { dir.deleteFile(entry.name) catch continue; debug.log("unlinked global bin: {s}", .{entry.name}); } } } export fn pkg_add_global( ctx: ?*PkgContext, package_spec: [*:0]const u8, ) PkgError { const specs = [_][*:0]const u8{package_spec}; return pkg_add_global_many(ctx, &specs, 1); } export fn pkg_add_global_many( ctx: ?*PkgContext, package_specs: [*]const [*:0]const u8, count: u32, ) PkgError { const c = ctx orelse return .invalid_argument; const allocator = c.allocator; const global_dir = getGlobalDir(allocator) catch { c.setError("HOME not set"); return .invalid_argument; }; defer allocator.free(global_dir); ensureGlobalPackageJson(allocator, global_dir) catch { c.setError("Failed to create global package.json"); return .io_error; }; const pkg_json_path = std.fmt.allocPrintSentinel(allocator, "{s}/package.json", .{global_dir}, 0) catch return .out_of_memory; defer allocator.free(pkg_json_path); const lockfile_path = std.fmt.allocPrintSentinel(allocator, "{s}/ant.lockb", .{global_dir}, 0) catch return .out_of_memory; defer allocator.free(lockfile_path); const nm_path = std.fmt.allocPrintSentinel(allocator, "{s}/node_modules", .{global_dir}, 0) catch return .out_of_memory; defer allocator.free(nm_path); const add_result = pkg_add_many(c, pkg_json_path.ptr, package_specs, count, false); if (add_result != .ok) return add_result; const install_result = pkg_resolve_and_install(c, pkg_json_path.ptr, lockfile_path.ptr, nm_path.ptr); if (install_result != .ok) return install_result; for (0..count) |i| { const spec_str = std.mem.span(package_specs[i]); var pkg_name: []const u8 = spec_str; if (std.mem.indexOf(u8, spec_str, "@")) |at_idx| { if (at_idx == 0) { if (std.mem.indexOfPos(u8, spec_str, 1, "@")) |second_at| pkg_name = spec_str[0..second_at]; } else pkg_name = spec_str[0..at_idx]; } linkGlobalBins(allocator, nm_path, pkg_name); } return .ok; } export fn pkg_remove_global( ctx: ?*PkgContext, package_name: [*:0]const u8, ) PkgError { const c = ctx orelse return .invalid_argument; const allocator = c.allocator; const global_dir = getGlobalDir(allocator) catch { c.setError("HOME not set"); return .invalid_argument; }; defer allocator.free(global_dir); const pkg_json_path = std.fmt.allocPrintSentinel(allocator, "{s}/package.json", .{global_dir}, 0) catch return .out_of_memory; defer allocator.free(pkg_json_path); const lockfile_path = std.fmt.allocPrintSentinel(allocator, "{s}/ant.lockb", .{global_dir}, 0) catch return .out_of_memory; defer allocator.free(lockfile_path); const nm_path = std.fmt.allocPrintSentinel(allocator, "{s}/node_modules", .{global_dir}, 0) catch return .out_of_memory; defer allocator.free(nm_path); const name_str = std.mem.span(package_name); unlinkGlobalBins(allocator, name_str); const remove_result = pkg_remove(c, pkg_json_path.ptr, package_name); if (remove_result != .ok and remove_result != .not_found) return remove_result; if (remove_result == .not_found) return .not_found; const install_result = pkg_resolve_and_install(c, pkg_json_path.ptr, lockfile_path.ptr, nm_path.ptr); if (install_result != .ok) return install_result; return .ok; } export fn pkg_count_local(ctx: ?*PkgContext) u32 { var pd = cli.get_dependencies(ctx, null, true) orelse return 0; defer pd.deinit(); return pd.count(); } export fn pkg_count_global(ctx: ?*PkgContext) u32 { const global_dir = getGlobalDir(global_allocator) catch return 0; var pd = cli.get_dependencies(ctx, global_dir, false) orelse return 0; defer pd.deinit(); return pd.count(); } export fn pkg_list_local( ctx: ?*PkgContext, callback: ?*const fn (name: [*:0]const u8, version: [*:0]const u8, user_data: ?*anyopaque) callconv(.c) void, user_data: ?*anyopaque, ) PkgError { var pd = cli.get_dependencies(ctx, null, true) orelse return .ok; defer pd.deinit(); if (callback) |cb| cli.list_dependencies(&pd, cb, user_data); return .ok; } export fn pkg_list_global( ctx: ?*PkgContext, callback: ?*const fn (name: [*:0]const u8, version: [*:0]const u8, user_data: ?*anyopaque) callconv(.c) void, user_data: ?*anyopaque, ) PkgError { const global_dir = getGlobalDir(global_allocator) catch return .invalid_argument; var pd = cli.get_dependencies(ctx, global_dir, false) orelse return .ok; defer pd.deinit(); if (callback) |cb| cli.list_dependencies(&pd, cb, user_data); return .ok; }