#!/usr/bin/env php
<?php
declare(strict_types=1);

$viewCache = [];
$args = parseArgs(array_slice($argv, 1));
if (empty($args['packages'])) usage(1);

$outDir = realOrAbsolute($args['out'] ?? './generated-npm-ebuilds');
$category = $args['category'] ?? 'dev-node';
$appCategory = $args['appCategory'] ?? 'app-misc';
$includeOptional = !empty($args['includeOptional']);
$maxDepth = array_key_exists('maxDepth', $args) ? (int)$args['maxDepth'] : PHP_INT_MAX;
$dryRun = !empty($args['dryRun']);
$progress = !empty($args['progress']);
$depsOnly = !empty($args['depsOnly']);
$generationCommand = shellJoin($argv);

$seen = [];
$queue = [];
foreach ($args['packages'] as $packageSpec) {
    $root = resolveSpec($packageSpec);
    $queue[] = ['name' => $root['name'], 'version' => $root['version'], 'depth' => 0, 'root' => !$depsOnly];
}

for ($qi = 0; $qi < count($queue); $qi++) {
    $item = $queue[$qi];
    $key = $item['name'] . '@' . $item['version'];
    if (isset($seen[$key])) continue;
    if ($progress) writeStderr('[' . (count($seen) + 1) . '/' . count($queue) . "] {$key} depth={$item['depth']}\n");
    $meta = npmView($item['name'] . '@' . $item['version']);
    $meta['_depth'] = $item['depth'];
    $meta['_root'] = $item['root'];
    $cat = !empty($meta['_root']) ? $appCategory : $category;
    $meta['_includeOptional'] = effectiveIncludeOptionalForExisting(
        $outDir,
        $cat,
        gentooPn($meta['name']),
        gentooPv((string)$meta['version']),
        $includeOptional
    );
    $seen[$key] = $meta;

    if ($item['depth'] >= $maxDepth) continue;
    foreach (depMap($meta, !empty($meta['_includeOptional'])) as $depName => $range) {
        $dep = resolveDependencySpec($depName, (string)$range);
        $queue[] = ['name' => $dep['name'], 'version' => $dep['version'], 'depth' => $item['depth'] + 1, 'root' => false];
    }
}

$metas = array_values($seen);
$roots = array_values(array_filter($metas, fn($m) => !empty($m['_root'])));
$deps = array_values(array_filter($metas, fn($m) => empty($m['_root'])));

if (!$dryRun) mkdirRecursive($outDir);
$writeStats = ['written' => 0, 'revised' => 0, 'unchanged' => 0];
$changedEbuilds = [];

foreach ($metas as $meta) {
    $cat = !empty($meta['_root']) ? $appCategory : $category;
    $pn = gentooPn($meta['name']);
    $pv = gentooPv((string)$meta['version']);
    $dir = $outDir . '/' . $cat . '/' . $pn;
    $body = renderEbuild($meta, [
        'category' => $cat,
        'depCategory' => $category,
        'includeOptional' => !empty($meta['_includeOptional']),
        'generationCommand' => $generationCommand,
    ]);
    $target = selectEbuildTarget($dir, $pn, $pv, $body);
    $body = $target['body'] ?? $body;
    if (!$dryRun) {
        mkdirRecursive($dir);
        if ($target['action'] === 'skip') {
            $writeStats['unchanged']++;
            if ($progress) writeStderr("[skip] {$cat}/{$pn}-{$pv}: unchanged signature already exists at " . basename($target['file']) . "\n");
        } else {
            file_put_contents($target['file'], $body);
            $writeStats[$target['action'] === 'revise' ? 'revised' : 'written']++;
            $changedEbuilds[] = $target['file'];
            if ($progress) writeStderr("[write] {$cat}/" . basename($target['file']) . ($target['action'] === 'revise' ? ' (new revision)' : '') . "\n");
        }
    }
}

printf("resolved %d package/version pairs (%d deps + %d root)\n", count($metas), count($deps), count($roots));
echo "slots are exact npm versions; duplicate package names with different versions are parallel-installable\n";
if (!$dryRun) printf("wrote %s (%d new, %d revisions, %d unchanged skipped)\n", $outDir, $writeStats['written'], $writeStats['revised'], $writeStats['unchanged']);

if (!$dryRun && $changedEbuilds) {
    foreach ($changedEbuilds as $f) runManifestForEbuild($f, $progress);
}


function isElectronNpmPackage(string $npmName): bool {
    return $npmName === 'electron';
}

function extraPackageRdeps(array $meta, array $opts, string $pn): array {
    $category = (string)($opts['category'] ?? '');
    if ($category === 'dev-node' && $pn === 'npm-sharp') {
        return ["\t!app-misc/npm-sharp"];
    }
    return [];
}

function electronMajorSlot(string $version): string {
    $parts = explode('.', $version, 2);
    return preg_replace('/[^0-9].*$/', '', $parts[0]) ?: $version;
}

function renderEbuild(array $meta, array $opts): string {
    $npmName = $meta['name'];
    $pn = gentooPn($npmName);
    $pv = gentooPv((string)$meta['version']);
    $tarball = $meta['dist']['tarball'] ?? null;
    if (!$tarball) throw new RuntimeException("No tarball for {$npmName}@{$meta['version']}");
    if (isMariozechnerClipboardPlatformPackage($npmName)) {
        throw new RuntimeException("{$npmName}@{$meta['version']} is a prebuilt platform package; generate @mariozechner/clipboard instead");
    }
    if ($npmName === '@napi-rs/canvas' || isNapiRsCanvasPlatformPackage($npmName)) {
        throw new RuntimeException("{$npmName}@{$meta['version']} needs a custom Skia source-build ebuild; prebuilt @napi-rs/canvas platform packages are not allowed");
    }
    if ($npmName === '@mariozechner/clipboard') return renderMariozechnerClipboardSourceEbuild($meta, $opts);
    if (isLydellNodePtyLinuxPackage($npmName)) return renderLydellNodePtyLinuxSourceEbuild($meta, $opts);
    if (isLydellNodePtyPlatformPackage($npmName)) {
        throw new RuntimeException("{$npmName}@{$meta['version']} is an unsupported prebuilt platform package; only linux x64/arm64 are source-built");
    }
    if ($npmName === 'koffi') return renderKoffiSourceEbuild($meta, $opts);
    if ($npmName === 'sqlite-vec-linux-x64' || $npmName === 'sqlite-vec-linux-arm64') return renderSqliteVecPlatformEbuild($meta, $opts);
    if ($npmName === '@img/sharp-linux-x64' || $npmName === '@img/sharp-linux-arm64') return renderSharpNativeSourceEbuild($meta, $opts);
    if ($npmName === '@img/sharp-libvips-linux-x64' || $npmName === '@img/sharp-libvips-linux-arm64') return renderSharpLibvipsStubEbuild($meta, $opts);
    if (str_starts_with($npmName, 'tree-sitter-')) return renderTreeSitterNodeAddonSourceEbuild($meta, $opts);
    if (isNodeGypBuildSourcePackage($meta)) return renderNodeGypBuildAddonSourceEbuild($meta, $opts);
    if ($npmName === '7zip-bin') return render7zipBinSystemEbuild($meta, $opts);
    if ($npmName === 'app-builder-bin') return renderAppBuilderBinSystemEbuild($meta, $opts);
    if ($npmName === 'quickjs-wasi' && (string)$meta['version'] === '2.2.0') return renderQuickjsWasiEbuild($meta, $opts);

    $npmVersion = (string)$meta['version'];
    $storeSuffix = 'node_modules/.gentoo-npm/' . $npmName . '/' . slot($npmVersion) . '/package';
    $storeExpr = '/usr/$(get_libdir)/' . $storeSuffix;
    $packageSlot = !empty($meta['_root']) ? '0' : slot($npmVersion);
    $depEntries = depMap($meta, !empty($opts['includeOptional']));
    $rdeps = [];
    if (isElectronNpmPackage($npmName)) {
        $rdeps[] = "\tdev-util/electron:" . electronMajorSlot($npmVersion);
    }
    foreach (extraPackageRdeps($meta, $opts, $pn) as $rdep) {
        $rdeps[] = $rdep;
    }
    foreach ($depEntries as $name => $range) {
        $resolved = resolveDependencySpec($name, (string)$range);
        $atom = npmDepAtom($opts['depCategory'], $resolved['name'], (string)$resolved['version']);
        $platformArch = optionalPlatformArch($npmName, $name);
        if ($platformArch) $rdeps[] = "	{$platformArch}? ( {$atom} )";
        else $rdeps[] = "	{$atom}";
    }


    $license = normalizeLicense($meta['license'] ?? null);
    $description = substr(str_replace("\n", ' ', $meta['description'] ?? "npm package {$npmName}"), 0, 120);
    $homepage = is_array($meta['homepage'] ?? null) ? ($meta['homepage'][0] ?? '') : ($meta['homepage'] ?? "https://www.npmjs.com/package/{$npmName}");
    $lines = [];
    pushGentooHeader($lines);
    $lines[] = 'EAPI=8';
    $lines[] = '';
    $lines[] = '# Generated by: ' . ebuildComment($opts['generationCommand']);
    $lines[] = '';
    $lines[] = 'inherit edo';
    $lines[] = '';
    $lines[] = 'NPM_NAME=' . bashQuote($npmName);
    $lines[] = 'NPM_VERSION=' . bashQuote($npmVersion);
    $lines[] = 'NPM_SLOT=' . bashQuote(slot($npmVersion));
    $lines[] = 'NPM_STORE_SUFFIX=' . bashQuote($storeSuffix);
    $lines[] = '';
    $lines[] = 'DESCRIPTION=' . bashQuote($description);
    $lines[] = 'HOMEPAGE=' . bashQuote($homepage ?: "https://www.npmjs.com/package/{$npmName}");
    $lines[] = 'SRC_URI=' . bashQuote($tarball . ' -> ' . distfileName($npmName, $npmVersion));
    $lines[] = 'S="${WORKDIR}"';
    $lines[] = '';
    $lines[] = 'LICENSE=' . bashQuote($license);
    $lines[] = 'SLOT=' . bashQuote($packageSlot);
    if (str_ends_with($npmName, '-linux-x64')) $lines[] = 'KEYWORDS="amd64"';
    elseif (str_ends_with($npmName, '-linux-arm64')) $lines[] = 'KEYWORDS="arm64"';
    else $lines[] = 'KEYWORDS="amd64 arm64"';
    $lines[] = 'RESTRICT="test"';
    $lines[] = '';
    $lines[] = 'RDEPEND="';
    if (!empty($meta['_root'])) $lines[] = "\t!<{$opts['category']}/{$pn}-{$pv}";
    foreach ($rdeps as $rdep) $lines[] = $rdep;
    if (!empty($meta['_root'])) $lines[] = '\tnet-libs/nodejs:0/24';
    $lines[] = '"';
    $lines[] = '';
    $lines[] = 'src_unpack() {';
    $lines[] = '\t# Some npm tarballs contain directory entries without execute bits.';
    $lines[] = '\t# GNU tar applies those modes before creating nested paths, causing';
    $lines[] = '\t# nested mkdirs to fail with EACCES. Pre-create directories with';
    $lines[] = '\t# sane permissions, then extract file entries only.';
    $lines[] = '\tlocal archive="${DISTDIR}/' . distfileName($npmName, $npmVersion) . '"';
    $lines[] = '\tlocal listing="${T}/npm-tar-list"';
    $lines[] = '\tlocal files="${T}/npm-tar-files"';
    $lines[] = '\tlocal mode owner size date time path dir';
    $lines[] = '';
    $lines[] = '\ttar -tvzf "${archive}" > "${listing}" || die';
    $lines[] = '\t: > "${files}" || die';
    $lines[] = '';
    $lines[] = '\twhile read -r mode owner size date time path; do';
    $lines[] = '\t\t[[ -n ${path} ]] || continue';
    $lines[] = '\t\tcase ${path} in';
    $lines[] = '\t\t\t./*|/*|../*|*/../*) die "unsafe tarball path: ${path}" ;;';
    $lines[] = '\t\tesac';
    $lines[] = '';
    $lines[] = '\t\tif [[ ${mode} == d* ]]; then';
    $lines[] = '\t\t\tmkdir -p "${WORKDIR}/${path}" || die';
    $lines[] = '\t\telse';
    $lines[] = '\t\t\tdir="${path%/*}"';
    $lines[] = '\t\t\tif [[ ${dir} != "${path}" ]]; then';
    $lines[] = '\t\t\t\tmkdir -p "${WORKDIR}/${dir}" || die';
    $lines[] = '\t\t\tfi';
    $lines[] = '\t\t\tprintf \'%s\\n\' "${path}" >> "${files}" || die';
    $lines[] = '\t\tfi';
    $lines[] = '\tdone < "${listing}"';
    $lines[] = '';
    $lines[] = '\tedo tar -C "${WORKDIR}" -xzf "${archive}" --no-same-owner --no-same-permissions -T "${files}"';
    $lines[] = '\tfind "${WORKDIR}" -type d -exec chmod u+rwx,go+rx {} + || die';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_install() {';
    $lines[] = '\tlocal npm_store="/usr/$(get_libdir)/${NPM_STORE_SUFFIX}"';
    $lines[] = '\tlocal npm_source="${WORKDIR}/package"';
    $lines[] = '\tif [[ ! -d ${npm_source} ]]; then';
    $lines[] = '\t\tlocal source_dirs=( "${WORKDIR}"/* )';
    $lines[] = '\t\tlocal real_source_dirs=()';
    $lines[] = '\t\tlocal source_dir';
    $lines[] = '\t\tfor source_dir in "${source_dirs[@]}"; do';
    $lines[] = '\t\t\t[[ -d ${source_dir} ]] && real_source_dirs+=( "${source_dir}" )';
    $lines[] = '\t\tdone';
    $lines[] = '\t\t[[ ${#real_source_dirs[@]} -eq 1 ]] || die "could not determine npm tarball source directory"';
    $lines[] = '\t\tnpm_source="${real_source_dirs[0]}"';
    $lines[] = '\tfi';
    $lines[] = '\tdodir "${npm_store}"';
    $lines[] = '\tcp -a "${npm_source}"/. "${D}${npm_store}" || die';
    pushPreShippedBinaryGuard($lines);
    $lines[] = '\tchmod -R u+rwX,go+rX "${D}${npm_store}" || die';
    $lines[] = '\tif [[ -f "${D}${npm_store}/package.json" ]]; then';
    $lines[] = '\t\techo "${NPM_NAME}@${NPM_VERSION}" > "${D}${npm_store}/.gentoo-npm-id" || die';
    $lines[] = '\tfi';

    if (isElectronNpmPackage($npmName)) {
        $lines[] = '';
        $lines[] = '\t# Upstream electron\'s npm wrapper normally downloads and records a';
        $lines[] = '\t# prebuilt Electron binary during install.js.  Gentoo must not run that';
        $lines[] = '\t# downloader; point the wrapper at the source-built slotted system Electron.';
        $lines[] = '\t# index.js reads path.txt verbatim, so do not write a trailing newline.';
        $lines[] = '\tprintf \'%s\' "electron" > "${D}${npm_store}/path.txt" || die';
        $lines[] = '\tdodir "${npm_store}/dist"';
        $lines[] = '\tdosym "/usr/bin/electron-${NPM_VERSION%%.*}" "${npm_store}/dist/electron"';
    }

    if ($depEntries) {
        $lines[] = '\t# npm tarballs can ship bundled node_modules. Remove them from the image';
        $lines[] = '\t# before creating Gentoo-managed dependency symlinks; otherwise dosym sees';
        $lines[] = '\t# an existing directory at the link path and fails.';
        $lines[] = '\trm -rf "${D}${npm_store}/node_modules" || die';
        $lines[] = '\tdodir "${npm_store}/node_modules"';
        $platformDeps = array_filter($depEntries, fn($range, $name) => optionalPlatformArch($npmName, $name) !== null, ARRAY_FILTER_USE_BOTH);
        if ($platformDeps) {
            $lines[] = '\tcase ${ARCH} in';
            foreach (['amd64', 'arm64'] as $arch) {
                $archDeps = array_filter($platformDeps, fn($range, $name) => optionalPlatformArch($npmName, $name) === $arch, ARRAY_FILTER_USE_BOTH);
                if (!$archDeps) continue;
                $lines[] = "\t\t{$arch})";
                foreach ($archDeps as $name => $range) {
                    $resolved = resolveDependencySpec($name, (string)$range);
                    $targetSuffix = 'node_modules/.gentoo-npm/' . $resolved['name'] . '/' . slot((string)$resolved['version']) . '/package';
                    $linkPath = nodeModulesLinkPath($name);
                    $parent = posixDirname($linkPath);
                    if ($parent !== '.') $lines[] = "\t\t\tdodir \"\${npm_store}/node_modules/{$parent}\"";
                    $lines[] = "\t\t\tdosym \"/usr/$(get_libdir)/{$targetSuffix}\" \"\${npm_store}/node_modules/{$linkPath}\"";
                }
                $lines[] = '\t\t\t;;';
            }
            $lines[] = '\tesac';
        }
        foreach ($depEntries as $name => $range) {
            if (optionalPlatformArch($npmName, $name)) continue;
            $resolved = resolveDependencySpec($name, (string)$range);
            $targetSuffix = 'node_modules/.gentoo-npm/' . $resolved['name'] . '/' . slot((string)$resolved['version']) . '/package';
            $linkPath = nodeModulesLinkPath($name);
            $parent = posixDirname($linkPath);
            if ($parent !== '.') $lines[] = "\tdodir \"\${npm_store}/node_modules/{$parent}\"";
            $lines[] = "\tdosym \"/usr/$(get_libdir)/{$targetSuffix}\" \"\${npm_store}/node_modules/{$linkPath}\"";
        }
    }
    if (!empty($meta['_root'])) {
        $rootLinkPath = nodeModulesLinkPath($npmName);
        $rootLinkParent = posixDirname($rootLinkPath);
        if ($rootLinkParent !== '.') $lines[] = "\tdodir \"/usr/$(get_libdir)/node_modules/{$rootLinkParent}\"";
        $lines[] = "\tdosym \"{$storeExpr}\" \"/usr/$(get_libdir)/node_modules/{$rootLinkPath}\"";
    }
    if (!empty($meta['_root']) && !empty($meta['bin'])) {
        $bins = is_string($meta['bin']) ? [preg_replace('/^npm-/', '', $pn) => $meta['bin']] : $meta['bin'];
        foreach ($bins as $binName => $rel) {
            $safeBin = preg_replace('#^@[^/]+/#', '', (string)$binName);
            $lines[] = "\tfperms +x \"{$storeExpr}/{$rel}\"";
            $lines[] = "\tdosym \"{$storeExpr}/{$rel}\" \"/usr/bin/{$safeBin}\"";
        }
    }
    $lines[] = '}';
    $lines[] = '';
    return renderLines($lines);
}

function renderMariozechnerClipboardSourceEbuild(array $meta, array $opts): string {
    $npmName = $meta['name'];
    $version = (string)$meta['version'];
    $slot = slot($version);
    $dist = distfileName($npmName, $version);
    $storeSuffix = 'node_modules/.gentoo-npm/' . $npmName . '/' . $slot . '/package';
    $crates = cargoCratesFromNpmTarball($npmName, $version);
    $description = substr(str_replace("\n", ' ', $meta['description'] ?? 'Clipboard access for Node.js'), 0, 120);
    $homepage = is_array($meta['homepage'] ?? null) ? ($meta['homepage'][0] ?? '') : ($meta['homepage'] ?? 'https://github.com/badlogic/clipboard');
    $depEntries = depMap($meta, !empty($opts['includeOptional']));
    $rdeps = [];
    foreach ($depEntries as $name => $range) {
        $resolved = resolveDependencySpec($name, (string)$range);
        $rdeps[] = "\t" . npmDepAtom($opts['depCategory'], $resolved['name'], (string)$resolved['version']);
    }

    $lines = [];
    pushGentooHeader($lines);
    $lines[] = 'EAPI=8';
    $lines[] = '';
    $lines[] = '# Generated by: ' . ebuildComment($opts['generationCommand']);
    $lines[] = '';
    pushCargoCrates($lines, $crates);
    $lines[] = 'inherit cargo edo';
    $lines[] = '';
    $lines[] = 'NPM_NAME=' . bashQuote($npmName);
    $lines[] = 'NPM_VERSION=' . bashQuote($version);
    $lines[] = 'NPM_SLOT=' . bashQuote($slot);
    $lines[] = 'NPM_STORE_SUFFIX=' . bashQuote($storeSuffix);
    $lines[] = '';
    $lines[] = 'DESCRIPTION=' . bashQuote($description . ' (native binding built from source)');
    $lines[] = 'HOMEPAGE=' . bashQuote($homepage ?: 'https://github.com/badlogic/clipboard');
    $lines[] = 'SRC_URI="';
    $lines[] = "\t{$meta['dist']['tarball']} -> {$dist}";
    $lines[] = '\t${CARGO_CRATE_URIS}';
    $lines[] = '"';
    $lines[] = 'S="${WORKDIR}/package"';
    $lines[] = '';
    $lines[] = 'LICENSE=' . bashQuote(normalizeLicense($meta['license'] ?? null));
    $lines[] = 'SLOT=' . bashQuote($slot);
    $lines[] = 'KEYWORDS="amd64 arm64"';
    $lines[] = 'RESTRICT="test"';
    $lines[] = '';
    $lines[] = 'RDEPEND="';
    foreach ($rdeps as $rdep) $lines[] = $rdep;
    $lines[] = '"';
    $lines[] = 'BDEPEND="';
    $lines[] = '\tnet-libs/nodejs';
    $lines[] = '"';
    $lines[] = '';
    $lines[] = 'src_unpack() {';
    $lines[] = '\tedo tar -C "${WORKDIR}" -xzf "${DISTDIR}/' . $dist . '" --no-same-owner --no-same-permissions';
    $lines[] = '\tcargo_src_unpack';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_prepare() {';
    $lines[] = '\tdefault';
    $lines[] = '';
    $lines[] = '\t# Respect Gentoo strip/LTO policy instead of upstream release profile.';
    $lines[] = '\tsed -i -e \'/^lto =/d\' -e \'/^strip =/d\' Cargo.toml || die';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_compile() {';
    $lines[] = '\tcargo_src_compile';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_install() {';
    $lines[] = '\tlocal npm_store="/usr/$(get_libdir)/${NPM_STORE_SUFFIX}"';
    $lines[] = '\tlocal npm_source="${S}"';
    $lines[] = '\tlocal native_name';
    $lines[] = '\tcase ${ARCH}:${CHOST} in';
    $lines[] = '\t\tamd64:*musl*) native_name="clipboard.linux-x64-musl.node" ;;';
    $lines[] = '\t\tamd64:*) native_name="clipboard.linux-x64-gnu.node" ;;';
    $lines[] = '\t\tarm64:*musl*) native_name="clipboard.linux-arm64-musl.node" ;;';
    $lines[] = '\t\tarm64:*) native_name="clipboard.linux-arm64-gnu.node" ;;';
    $lines[] = '\t\t*) die "unsupported ARCH/CHOST: ${ARCH}/${CHOST}" ;;';
    $lines[] = '\tesac';
    $lines[] = '';
    $lines[] = '\tlocal built_node="${S}/$(cargo_target_dir)/libcrosscopy_clipboard.so"';
    $lines[] = '\t[[ -f ${built_node} ]] || die "cargo did not produce ${built_node}"';
    $lines[] = '';
    $lines[] = '\tdodir "${npm_store}"';
    $lines[] = '\tcp -a "${npm_source}"/. "${D}${npm_store}" || die';
    $lines[] = '\trm -rf "${D}${npm_store}/target" "${D}${npm_store}/node_modules" || die';
    $lines[] = '\trm -f "${D}${npm_store}"/clipboard.*.node || die';
    pushPreShippedBinaryGuard($lines);
    $lines[] = '\tinstall -m0755 "${built_node}" "${D}${npm_store}/${native_name}" || die';
    if ($depEntries) {
        $lines[] = '\tdodir "${npm_store}/node_modules"';
        foreach ($depEntries as $name => $range) {
            $resolved = resolveDependencySpec($name, (string)$range);
            $targetSuffix = 'node_modules/.gentoo-npm/' . $resolved['name'] . '/' . slot((string)$resolved['version']) . '/package';
            $linkPath = nodeModulesLinkPath($name);
            $parent = posixDirname($linkPath);
            if ($parent !== '.') $lines[] = "\tdodir \"\${npm_store}/node_modules/{$parent}\"";
            $lines[] = "\tdosym \"/usr/$(get_libdir)/{$targetSuffix}\" \"\${npm_store}/node_modules/{$linkPath}\"";
        }
    }
    $lines[] = '\tchmod -R u+rwX,go+rX "${D}${npm_store}" || die';
    $lines[] = '\techo "${NPM_NAME}@${NPM_VERSION}" > "${D}${npm_store}/.gentoo-npm-id" || die';
    $lines[] = '}';
    $lines[] = '';
    return renderLines($lines);
}

function renderLydellNodePtyLinuxSourceEbuild(array $meta, array $opts): string {
    $npmName = $meta['name'];
    $version = (string)$meta['version'];
    $slot = slot($version);
    $arch = str_ends_with($npmName, '-linux-x64') ? 'amd64' : 'arm64';
    $dist = distfileName($npmName, $version);
    $nodePtyMeta = npmView('node-pty@' . $version);
    $nodePtyDist = distfileName('node-pty', (string)$nodePtyMeta['version']);
    $nodePtyDeps = $nodePtyMeta['dependencies'] ?? [];
    if (!isset($nodePtyDeps['node-addon-api'])) throw new RuntimeException("node-pty@{$nodePtyMeta['version']} has no node-addon-api dependency");
    $nodeAddonApi = resolveDependencySpec('node-addon-api', (string)$nodePtyDeps['node-addon-api']);
    $storeSuffix = 'node_modules/.gentoo-npm/' . $npmName . '/' . $slot . '/package';
    $description = substr(str_replace("\n", ' ', $meta['description'] ?? 'node-pty platform package'), 0, 120);
    $homepage = is_array($meta['homepage'] ?? null) ? ($meta['homepage'][0] ?? '') : ($meta['homepage'] ?? 'https://github.com/lydell/node-pty');

    $lines = [];
    pushGentooHeader($lines);
    $lines[] = 'EAPI=8';
    $lines[] = '';
    $lines[] = '# Generated by: ' . ebuildComment($opts['generationCommand']);
    $lines[] = '';
    $lines[] = 'inherit edo';
    $lines[] = '';
    $lines[] = 'NPM_NAME=' . bashQuote($npmName);
    $lines[] = 'NPM_VERSION=' . bashQuote($version);
    $lines[] = 'NPM_SLOT=' . bashQuote($slot);
    $lines[] = 'NPM_STORE_SUFFIX=' . bashQuote($storeSuffix);
    $lines[] = 'NODE_ADDON_API_SLOT=' . bashQuote(slot((string)$nodeAddonApi['version']));
    $lines[] = '';
    $lines[] = 'DESCRIPTION=' . bashQuote($description . ' (native binding built from source)');
    $lines[] = 'HOMEPAGE=' . bashQuote($homepage ?: 'https://github.com/lydell/node-pty');
    $lines[] = 'SRC_URI="';
    $lines[] = "\t{$meta['dist']['tarball']} -> {$dist}";
    $lines[] = "\t{$nodePtyMeta['dist']['tarball']} -> {$nodePtyDist}";
    $lines[] = '"';
    $lines[] = 'S="${WORKDIR}/node-pty-source"';
    $lines[] = '';
    $lines[] = 'LICENSE=' . bashQuote(normalizeLicense($meta['license'] ?? null));
    $lines[] = 'SLOT=' . bashQuote($slot);
    $lines[] = 'KEYWORDS=' . bashQuote($arch);
    $lines[] = 'RESTRICT="test ccache"';
    $lines[] = '';
    $lines[] = 'BDEPEND="';
    $lines[] = '\tnet-libs/nodejs[npm]';
    $lines[] = '\t' . npmDepAtom($opts['depCategory'], $nodeAddonApi['name'], (string)$nodeAddonApi['version']);
    $lines[] = '"';
    $lines[] = '';
    $lines[] = 'src_unpack() {';
    $lines[] = '\tmkdir -p "${WORKDIR}/npm-package" "${S}" || die';
    $lines[] = "\tedo tar -C \"\${WORKDIR}/npm-package\" --strip-components=1 -xzf \"\${DISTDIR}/{$dist}\" --no-same-owner --no-same-permissions";
    $lines[] = "\tedo tar -C \"\${S}\" --strip-components=1 -xzf \"\${DISTDIR}/{$nodePtyDist}\" --no-same-owner --no-same-permissions";
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_prepare() {';
    $lines[] = '\tdefault';
    $lines[] = '\trm -rf prebuilds third_party || die';
    $lines[] = '\tlocal npm_deps="/usr/$(get_libdir)/node_modules/.gentoo-npm"';
    $lines[] = '\tmkdir -p node_modules || die';
    $lines[] = '\tcp -a "${npm_deps}/node-addon-api/${NODE_ADDON_API_SLOT}/package" node_modules/node-addon-api || die';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_compile() {';
    $lines[] = '\tlocal node_arch';
    $lines[] = '\tcase ${ARCH} in';
    $lines[] = '\t\tamd64) node_arch="x64" ;;';
    $lines[] = '\t\tarm64) node_arch="arm64" ;;';
    $lines[] = '\t\t*) die "unsupported ARCH=${ARCH}" ;;';
    $lines[] = '\tesac';
    $lines[] = '';
    $lines[] = '\texport npm_config_arch="${node_arch}"';
    $lines[] = '\texport npm_config_nodedir=/usr';
    pushPkgConfigMultilibEnv($lines);
    $lines[] = '\tedo node /usr/$(get_libdir)/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild --nodedir=/usr';
    pushNodeGypPkgConfigIncludeGuard($lines);
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_install() {';
    $lines[] = '\tlocal node_arch platform_dir';
    $lines[] = '\tcase ${ARCH} in';
    $lines[] = '\t\tamd64) node_arch="x64" ;;';
    $lines[] = '\t\tarm64) node_arch="arm64" ;;';
    $lines[] = '\t\t*) die "unsupported ARCH=${ARCH}" ;;';
    $lines[] = '\tesac';
    $lines[] = '\tplatform_dir="linux-${node_arch}"';
    $lines[] = '';
    $lines[] = '\tlocal npm_store="/usr/$(get_libdir)/${NPM_STORE_SUFFIX}"';
    $lines[] = '\tlocal built_node="${S}/build/Release/pty.node"';
    $lines[] = '\t[[ -f ${built_node} ]] || die "node-gyp did not produce ${built_node}"';
    $lines[] = '';
    $lines[] = '\tdodir "${npm_store}"';
    $lines[] = '\tcp -a "${WORKDIR}/npm-package"/. "${D}${npm_store}" || die';
    $lines[] = '\trm -rf "${D}${npm_store}/prebuilds" || die';
    pushPreShippedBinaryGuard($lines);
    $lines[] = '\tinstall -Dm0755 "${built_node}" "${D}${npm_store}/prebuilds/${platform_dir}/pty.node" || die';
    $lines[] = '\tchmod -R u+rwX,go+rX "${D}${npm_store}" || die';
    $lines[] = '\techo "${NPM_NAME}@${NPM_VERSION}" > "${D}${npm_store}/.gentoo-npm-id" || die';
    $lines[] = '}';
    $lines[] = '';
    return renderLines($lines);
}

function renderKoffiSourceEbuild(array $meta, array $opts): string {
    $npmName = $meta['name'];
    $version = (string)$meta['version'];
    $slot = slot($version);
    $dist = distfileName($npmName, $version);
    $storeSuffix = 'node_modules/.gentoo-npm/' . $npmName . '/' . $slot . '/package';
    $description = substr(str_replace("\n", ' ', $meta['description'] ?? 'Fast and simple C FFI for Node.js'), 0, 120);
    $homepage = is_array($meta['homepage'] ?? null) ? ($meta['homepage'][0] ?? '') : ($meta['homepage'] ?? 'https://koffi.dev/');
    $depEntries = depMap($meta, !empty($opts['includeOptional']));
    $rdeps = [];
    foreach ($depEntries as $name => $range) {
        $resolved = resolveDependencySpec($name, (string)$range);
        $rdeps[] = "\t" . npmDepAtom($opts['depCategory'], $resolved['name'], (string)$resolved['version']);
    }

    $lines = [];
    pushGentooHeader($lines);
    $lines[] = 'EAPI=8';
    $lines[] = '';
    $lines[] = '# Generated by: ' . ebuildComment($opts['generationCommand']);
    $lines[] = '';
    $lines[] = 'inherit edo';
    $lines[] = '';
    $lines[] = 'NPM_NAME=' . bashQuote($npmName);
    $lines[] = 'NPM_VERSION=' . bashQuote($version);
    $lines[] = 'NPM_SLOT=' . bashQuote($slot);
    $lines[] = 'NPM_STORE_SUFFIX=' . bashQuote($storeSuffix);
    $lines[] = '';
    $lines[] = 'DESCRIPTION=' . bashQuote($description . ' (native binding built from source)');
    $lines[] = 'HOMEPAGE=' . bashQuote($homepage ?: 'https://koffi.dev/');
    $lines[] = 'SRC_URI=' . bashQuote($meta['dist']['tarball'] . ' -> ' . $dist);
    $lines[] = 'S="${WORKDIR}/package"';
    $lines[] = '';
    $lines[] = 'LICENSE=' . bashQuote(normalizeLicense($meta['license'] ?? null));
    $lines[] = 'SLOT=' . bashQuote($slot);
    $lines[] = 'KEYWORDS="amd64 arm64"';
    $lines[] = 'RESTRICT="test ccache"';
    $lines[] = '';
    $lines[] = 'RDEPEND="';
    foreach ($rdeps as $rdep) $lines[] = $rdep;
    $lines[] = '"';
    $lines[] = 'BDEPEND="';
    $lines[] = '\tdev-build/cmake';
    $lines[] = '\tnet-libs/nodejs';
    $lines[] = '"';
    $lines[] = '';
    $lines[] = 'src_unpack() {';
    $lines[] = '\tedo tar -C "${WORKDIR}" -xzf "${DISTDIR}/' . $dist . '" --no-same-owner --no-same-permissions';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_prepare() {';
    $lines[] = '\tdefault';
    $lines[] = '';
    $lines[] = '\t# The npm tarball ships prebuilt native addons for many platforms.';
    $lines[] = '\t# Remove them before building the one for the current Gentoo host.';
    $lines[] = '\trm -rf build/koffi || die';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_compile() {';
    $lines[] = '\tedo node src/cnoke/cnoke.js build -P . -D src/koffi --release';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_install() {';
    $lines[] = '\tlocal npm_store="/usr/$(get_libdir)/${NPM_STORE_SUFFIX}"';
    $lines[] = '\tlocal triplet';
    $lines[] = '\tcase ${ARCH} in';
    $lines[] = '\t\tamd64) triplet="linux_x64" ;;';
    $lines[] = '\t\tarm64) triplet="linux_arm64" ;;';
    $lines[] = '\t\t*) die "unsupported ARCH=${ARCH}" ;;';
    $lines[] = '\tesac';
    $lines[] = '';
    $lines[] = '\tlocal built_node="${S}/build/koffi/${triplet}/koffi.node"';
    $lines[] = '\t[[ -f ${built_node} ]] || die "cnoke did not produce ${built_node}"';
    $lines[] = '';
    $lines[] = '\tdodir "${npm_store}"';
    $lines[] = '\tcp -a "${S}"/. "${D}${npm_store}" || die';
    $lines[] = '\trm -rf "${D}${npm_store}/build/koffi" || die';
    pushPreShippedBinaryGuard($lines);
    $lines[] = '\tinstall -Dm0755 "${built_node}" "${D}${npm_store}/build/koffi/${triplet}/koffi.node" || die';
    $lines[] = '\tchmod -R u+rwX,go+rX "${D}${npm_store}" || die';
    $lines[] = '\techo "${NPM_NAME}@${NPM_VERSION}" > "${D}${npm_store}/.gentoo-npm-id" || die';
    $lines[] = '}';
    $lines[] = '';
    return renderLines($lines);
}


function renderSharpNativeSourceEbuild(array $meta, array $opts): string {
    $npmName = $meta['name'];
    $version = (string)$meta['version'];
    $slot = slot($version);
    $arch = str_ends_with($npmName, '-linux-x64') ? 'amd64' : 'arm64';
    $nodeArch = $arch === 'amd64' ? 'x64' : 'arm64';
    $dist = distfileName($npmName, $version);
    $sharpMeta = npmView('sharp@' . $version);
    $sharpDist = distfileName('sharp', $version);
    $storeSuffix = 'node_modules/.gentoo-npm/' . $npmName . '/' . $slot . '/package';
    $description = substr(str_replace("\n", ' ', $meta['description'] ?? 'sharp platform package'), 0, 120);
    $homepage = is_array($meta['homepage'] ?? null) ? ($meta['homepage'][0] ?? '') : ($meta['homepage'] ?? 'https://sharp.pixelplumbing.com');
    $lines = [];
    pushGentooHeader($lines);
    $lines[] = 'EAPI=8';
    $lines[] = '';
    $lines[] = '# Generated by: ' . ebuildComment($opts['generationCommand']);
    $lines[] = '';
    $lines[] = 'inherit edo';
    $lines[] = '';
    $lines[] = 'NPM_NAME=' . bashQuote($npmName);
    $lines[] = 'NPM_VERSION=' . bashQuote($version);
    $lines[] = 'NPM_SLOT=' . bashQuote($slot);
    $lines[] = 'NPM_STORE_SUFFIX=' . bashQuote($storeSuffix);
    $lines[] = 'NODE_ADDON_API_SLOT="8.7.0"';
    $lines[] = 'DETECT_LIBC_SLOT="2.1.2"';
    $lines[] = 'SEMVER_SLOT="7.8.0"';
    $lines[] = '';
    $lines[] = 'DESCRIPTION=' . bashQuote($description);
    $lines[] = 'HOMEPAGE=' . bashQuote($homepage ?: 'https://sharp.pixelplumbing.com');
    $lines[] = 'SRC_URI="';
    $lines[] = "\t{$meta['dist']['tarball']} -> {$dist}";
    $lines[] = "\t{$sharpMeta['dist']['tarball']} -> {$sharpDist}";
    $lines[] = '"';
    $lines[] = 'S="${WORKDIR}/sharp-source"';
    $lines[] = '';
    $lines[] = 'LICENSE="Apache-2.0"';
    $lines[] = 'SLOT=' . bashQuote($slot);
    $lines[] = 'KEYWORDS=' . bashQuote($arch);
    $lines[] = 'RESTRICT="test ccache"';
    $lines[] = '';
    $lines[] = 'RDEPEND="';
    $lines[] = '\t>=media-libs/vips-8.17.3:=';
    $lines[] = '"';
    $lines[] = 'DEPEND="${RDEPEND}"';
    $lines[] = 'BDEPEND="';
    $lines[] = '\tnet-libs/nodejs[npm]';
    $lines[] = '\t' . npmDepAtom($opts['depCategory'], 'node-addon-api', '8.7.0');
    $lines[] = '\t' . npmDepAtom($opts['depCategory'], 'detect-libc', '2.1.2');
    $lines[] = '\t' . npmDepAtom($opts['depCategory'], 'semver', '7.8.0');
    $lines[] = '"';
    $lines[] = '';
    $lines[] = 'src_unpack() {';
    $lines[] = '\tmkdir -p "${WORKDIR}/npm-package" "${S}" || die';
    $lines[] = "\tedo tar -C \"\${WORKDIR}/npm-package\" --strip-components=1 -xzf \"\${DISTDIR}/{$dist}\" --no-same-owner --no-same-permissions";
    $lines[] = "\tedo tar -C \"\${S}\" --strip-components=1 -xzf \"\${DISTDIR}/{$sharpDist}\" --no-same-owner --no-same-permissions";
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_prepare() {';
    $lines[] = '\tdefault';
    $lines[] = '\tlocal npm_deps="/usr/$(get_libdir)/node_modules/.gentoo-npm"';
    $lines[] = '\tmkdir -p "${S}/node_modules" || die';
    $lines[] = '\tcp -a "${npm_deps}/node-addon-api/${NODE_ADDON_API_SLOT}/package" "${S}/node_modules/node-addon-api" || die';
    $lines[] = '\tcp -a "${npm_deps}/detect-libc/${DETECT_LIBC_SLOT}/package" "${S}/node_modules/detect-libc" || die';
    $lines[] = '\tcp -a "${npm_deps}/semver/${SEMVER_SLOT}/package" "${S}/node_modules/semver" || die';
    $lines[] = '\tlocal sharp_pkg_config_path="${EPREFIX}/usr/$(get_libdir)/pkgconfig:${EPREFIX}/usr/share/pkgconfig"';
    $lines[] = '\tsed -i -e "s#PKG_CONFIG_PATH=\"<(pkg_config_path)\" pkg-config#PKG_CONFIG_LIBDIR=\"${sharp_pkg_config_path}\" PKG_CONFIG_PATH=\"${sharp_pkg_config_path}\" pkg-config#g" src/binding.gyp || die';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_compile() {';
    $lines[] = '\tlocal node_arch';
    $lines[] = '\tcase ${ARCH} in';
    $lines[] = '\t\tamd64) node_arch="x64" ;;';
    $lines[] = '\t\tarm64) node_arch="arm64" ;;';
    $lines[] = '\t\t*) die "unsupported ARCH=${ARCH}" ;;';
    $lines[] = '\tesac';
    $lines[] = '';
    $lines[] = '\texport SHARP_FORCE_GLOBAL_LIBVIPS=1';
    $lines[] = '\texport npm_config_platform=linux';
    $lines[] = '\texport npm_config_arch="${node_arch}"';
    $lines[] = '\texport npm_config_nodedir=/usr';
    $lines[] = '\tunset npm_config_libc';
    pushPkgConfigMultilibEnv($lines);
    $lines[] = '\tedo node /usr/$(get_libdir)/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild --directory=src --nodedir=/usr';
    pushNodeGypPkgConfigIncludeGuard($lines, '${S}/src/build');
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_install() {';
    $lines[] = '\tlocal node_arch';
    $lines[] = '\tcase ${ARCH} in';
    $lines[] = '\t\tamd64) node_arch="x64" ;;';
    $lines[] = '\t\tarm64) node_arch="arm64" ;;';
    $lines[] = '\t\t*) die "unsupported ARCH=${ARCH}" ;;';
    $lines[] = '\tesac';
    $lines[] = '';
    $lines[] = '\tlocal npm_store="/usr/$(get_libdir)/${NPM_STORE_SUFFIX}"';
    $lines[] = '\tlocal npm_source="${WORKDIR}/npm-package"';
    $lines[] = '\tlocal built_node="${S}/src/build/Release/sharp-linux-${node_arch}.node"';
    $lines[] = '\tdodir "${npm_store}"';
    $lines[] = '\tcp -a "${npm_source}"/. "${D}${npm_store}" || die';
    $lines[] = '\trm -f "${D}${npm_store}"/lib/*.node || die';
    pushPreShippedBinaryGuard($lines);
    $lines[] = '\tinstall -m0755 "${built_node}" "${D}${npm_store}/lib/sharp-linux-${node_arch}.node" || die';
    $lines[] = '\tchmod -R u+rwX,go+rX "${D}${npm_store}" || die';
    $lines[] = '\techo "${NPM_NAME}@${NPM_VERSION}" > "${D}${npm_store}/.gentoo-npm-id" || die';
    $lines[] = '}';
    $lines[] = '';
    return renderLines($lines);
}

function renderSharpLibvipsStubEbuild(array $meta, array $opts): string {
    $npmName = $meta['name'];
    $version = (string)$meta['version'];
    $slot = slot($version);
    $arch = str_ends_with($npmName, '-linux-x64') ? 'amd64' : 'arm64';
    $dist = distfileName($npmName, $version);
    $storeSuffix = 'node_modules/.gentoo-npm/' . $npmName . '/' . $slot . '/package';
    $description = substr(str_replace("\n", ' ', $meta['description'] ?? 'sharp libvips platform package'), 0, 120);
    $homepage = is_array($meta['homepage'] ?? null) ? ($meta['homepage'][0] ?? '') : ($meta['homepage'] ?? 'https://sharp.pixelplumbing.com');
    $lines = [];
    pushGentooHeader($lines);
    $lines[] = 'EAPI=8';
    $lines[] = '';
    $lines[] = '# Generated by: ' . ebuildComment($opts['generationCommand']);
    $lines[] = '';
    $lines[] = 'inherit edo';
    $lines[] = '';
    $lines[] = 'NPM_NAME=' . bashQuote($npmName);
    $lines[] = 'NPM_VERSION=' . bashQuote($version);
    $lines[] = 'NPM_SLOT=' . bashQuote($slot);
    $lines[] = 'NPM_STORE_SUFFIX=' . bashQuote($storeSuffix);
    $lines[] = '';
    $lines[] = 'DESCRIPTION=' . bashQuote($description . ' (Gentoo system libvips metadata stub)');
    $lines[] = 'HOMEPAGE=' . bashQuote($homepage ?: 'https://sharp.pixelplumbing.com');
    $lines[] = 'SRC_URI=' . bashQuote($meta['dist']['tarball'] . ' -> ' . $dist);
    $lines[] = 'S="${WORKDIR}"';
    $lines[] = '';
    $lines[] = 'LICENSE="LGPL-3.0-or-later LGPL-2.1+ MIT"';
    $lines[] = 'SLOT=' . bashQuote($slot);
    $lines[] = 'KEYWORDS=' . bashQuote($arch);
    $lines[] = 'RESTRICT="test"';
    $lines[] = 'RDEPEND=">=media-libs/vips-8.17.3:="';
    $lines[] = '';
    $lines[] = 'src_unpack() {';
    $lines[] = '\tmkdir -p "${WORKDIR}/npm-package" || die';
    $lines[] = "\tedo tar -C \"\${WORKDIR}/npm-package\" --strip-components=1 -xzf \"\${DISTDIR}/{$dist}\" --no-same-owner --no-same-permissions";
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_install() {';
    $lines[] = '\tlocal npm_store="/usr/$(get_libdir)/${NPM_STORE_SUFFIX}"';
    $lines[] = '\tdodir "${npm_store}"';
    $lines[] = '\tcp -a "${WORKDIR}/npm-package"/. "${D}${npm_store}" || die';
    $lines[] = '\trm -f "${D}${npm_store}"/lib/*.so* || die';
    $lines[] = '\tchmod -R u+rwX,go+rX "${D}${npm_store}" || die';
    $lines[] = '\techo "${NPM_NAME}@${NPM_VERSION}" > "${D}${npm_store}/.gentoo-npm-id" || die';
    $lines[] = '}';
    $lines[] = '';
    return renderLines($lines);
}

function sharpLibvipsVersion(string $sharpLibvipsVersion): string {
    return match ($sharpLibvipsVersion) {
        '1.2.4' => '8.17.3',
        default => throw new RuntimeException("No source-build libvips mapping for @img/sharp-libvips version {$sharpLibvipsVersion}"),
    };
}

function sharpLibvipsAbiVersion(string $vipsVersion): string {
    return match ($vipsVersion) {
        '8.17.3' => '42.19.3',
        default => throw new RuntimeException("No libvips ABI mapping for version {$vipsVersion}"),
    };
}

function renderQuickjsWasiEbuild(array $meta, array $opts): string {
    $pv = gentooPv((string)$meta['version']);
    $dist = distfileName($meta['name'], (string)$meta['version']);
    $quickjsNgCommit = 'dec012362bd93876449f3ecff4f835b2eba89bab';
    $description = substr(str_replace(["\n", '"'], [' ', '\\"'], $meta['description'] ?? 'quickjs-wasi'), 0, 120);
    $gentooHeader = gentooHeaderText();
    return <<<EBUILD
{$gentooHeader}

EAPI=8

# Generated by: {$opts['generationCommand']}

inherit edo

NPM_NAME="quickjs-wasi"
NPM_VERSION="{$meta['version']}"
NPM_SLOT="{$meta['version']}"
NPM_STORE_SUFFIX="node_modules/.gentoo-npm/quickjs-wasi/{$meta['version']}/package"
QUICKJS_NG_COMMIT="{$quickjsNgCommit}"
WASI_SDK_VERSION="32.0"

DESCRIPTION="{$description}"
HOMEPAGE="https://github.com/vercel-labs/quickjs-wasi#readme"
SRC_URI="
	{$meta['dist']['tarball']} -> {$dist}
	https://codeload.github.com/vercel-labs/quickjs-wasi/tar.gz/refs/tags/quickjs-wasi@{$meta['version']} -> quickjs-wasi-{$pv}-src.tar.gz
	https://codeload.github.com/quickjs-ng/quickjs/tar.gz/{$quickjsNgCommit} -> quickjs-ng-{$quickjsNgCommit}.tar.gz
	amd64? ( https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-32/wasi-sdk-32.0-x86_64-linux.tar.gz )
	arm64? ( https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-32/wasi-sdk-32.0-arm64-linux.tar.gz )
"
S="\${WORKDIR}/quickjs-wasi-quickjs-wasi-{$meta['version']}"

LICENSE="MIT"
SLOT="{$meta['version']}"
KEYWORDS="amd64 arm64"
RESTRICT="test strip"

RDEPEND="
"

src_unpack() {
	mkdir -p "\${WORKDIR}/npm-package" || die
	edo tar -C "\${WORKDIR}/npm-package" --strip-components=1 -xzf "\${DISTDIR}/{$dist}" --no-same-owner --no-same-permissions
	edo tar -C "\${WORKDIR}" -xzf "\${DISTDIR}/quickjs-wasi-{$pv}-src.tar.gz" --no-same-owner --no-same-permissions

	rm -rf "\${S}/quickjs-ng" || die
	mkdir -p "\${S}/quickjs-ng" || die
	edo tar -C "\${S}/quickjs-ng" --strip-components=1 -xzf "\${DISTDIR}/quickjs-ng-{$quickjsNgCommit}.tar.gz" --no-same-owner --no-same-permissions

	local wasi_sdk_archive
	case \${ARCH} in
		amd64) wasi_sdk_archive="wasi-sdk-32.0-x86_64-linux.tar.gz" ;;
		arm64) wasi_sdk_archive="wasi-sdk-32.0-arm64-linux.tar.gz" ;;
		*) die "unsupported ARCH=\${ARCH}" ;;
	esac
	mkdir -p "\${WORKDIR}/wasi-sdk" || die
	edo tar -C "\${WORKDIR}/wasi-sdk" --strip-components=1 -xzf "\${DISTDIR}/\${wasi_sdk_archive}" --no-same-owner --no-same-permissions

	find "\${WORKDIR}" -type d -exec chmod u+rwx,go+rx {} + || die
}

src_compile() {
	emake WASI_SDK="\${WORKDIR}/wasi-sdk" \
		quickjs.wasm \
		extensions/url/url.so \
		extensions/structured-clone/structured-clone.so \
		extensions/headers/headers.so \
		extensions/encoding/encoding.so \
		extensions/crypto/crypto.so \
		extensions/base64/base64.so
}

src_install() {
	local npm_store="/usr/$(get_libdir)/\${NPM_STORE_SUFFIX}"
	local npm_source="\${WORKDIR}/npm-package"
	dodir "\${npm_store}"
	cp -a "\${npm_source}"/. "\${D}\${npm_store}" || die
	local preshipped_binary
	preshipped_binary=$(find "\${D}\${npm_store}" -type f -exec grep -al $'\177ELF' {} + | head -n 1) || die
	[[ -z \${preshipped_binary} ]] || die "pre shipped binary not allowed: \${preshipped_binary#\${D}}"

	install -m0644 "\${S}/quickjs.wasm" "\${D}\${npm_store}/quickjs.wasm" || die
	install -m0644 "\${S}/extensions/url/url.so" "\${D}\${npm_store}/extensions/url/url.so" || die
	install -m0644 "\${S}/extensions/structured-clone/structured-clone.so" "\${D}\${npm_store}/extensions/structured-clone/structured-clone.so" || die
	install -m0644 "\${S}/extensions/headers/headers.so" "\${D}\${npm_store}/extensions/headers.so" || die
	install -m0644 "\${S}/extensions/encoding/encoding.so" "\${D}\${npm_store}/extensions/encoding.so" || die
	install -m0644 "\${S}/extensions/crypto/crypto.so" "\${D}\${npm_store}/extensions/crypto.so" || die
	install -m0644 "\${S}/extensions/base64/base64.so" "\${D}\${npm_store}/extensions/base64.so" || die

	chmod -R u+rwX,go+rX "\${D}\${npm_store}" || die
	if [[ -f "\${D}\${npm_store}/package.json" ]]; then
		echo "\${NPM_NAME}@\${NPM_VERSION}" > "\${D}\${npm_store}/.gentoo-npm-id" || die
	fi
}
EBUILD;
}

function renderSqliteVecPlatformEbuild(array $meta, array $opts): string {
    $npmName = $meta['name'];
    $pv = gentooPv((string)$meta['version']);
    $arch = str_ends_with($npmName, '-x64') ? 'amd64' : 'arm64';
    $dist = distfileName($npmName, (string)$meta['version']);
    $srcDist = "sqlite-vec-{$pv}-src.tar.gz";
    $lines = [];
    pushGentooHeader($lines);
    $lines[] = 'EAPI=8';
    $lines[] = '';
    $lines[] = '# Generated by: ' . ebuildComment($opts['generationCommand']);
    $lines[] = '';
    $lines[] = 'inherit toolchain-funcs';
    $lines[] = '';
    $lines[] = 'NPM_NAME=' . bashQuote($npmName);
    $lines[] = 'NPM_VERSION=' . bashQuote((string)$meta['version']);
    $lines[] = 'NPM_SLOT=' . bashQuote(slot((string)$meta['version']));
    $lines[] = 'NPM_STORE_SUFFIX=' . bashQuote('node_modules/.gentoo-npm/' . $npmName . '/' . slot((string)$meta['version']) . '/package');
    $lines[] = '';
    $lines[] = 'DESCRIPTION=' . bashQuote(substr(str_replace("\n", ' ', $meta['description'] ?? 'sqlite-vec platform package'), 0, 120));
    $lines[] = 'HOMEPAGE="https://github.com/asg017/sqlite-vec"';
    $lines[] = 'SRC_URI="';
    $lines[] = "\t{$meta['dist']['tarball']} -> {$dist}";
    $lines[] = "\thttps://github.com/asg017/sqlite-vec/archive/refs/tags/v{$meta['version']}.tar.gz -> {$srcDist}";
    $lines[] = '"';
    $lines[] = 'S="${WORKDIR}/sqlite-vec-' . $meta['version'] . '"';
    $lines[] = '';
    $lines[] = 'LICENSE="|| ( MIT Apache-2.0 )"';
    $lines[] = 'SLOT=' . bashQuote(slot((string)$meta['version']));
    $lines[] = 'KEYWORDS=' . bashQuote($arch);
    $lines[] = 'RESTRICT="test"';
    $lines[] = 'DEPEND="dev-db/sqlite"';
    $lines[] = 'RDEPEND="dev-db/sqlite"';
    $lines[] = 'BDEPEND="sys-devel/gettext"';
    $lines[] = '';
    $lines[] = 'src_unpack() {';
    $lines[] = '\tmkdir -p "${WORKDIR}/npm-package" || die';
    $lines[] = '\ttar -C "${WORKDIR}/npm-package" --strip-components=1 -xzf "${DISTDIR}/' . $dist . '" || die';
    $lines[] = '\ttar -C "${WORKDIR}" -xzf "${DISTDIR}/' . $srcDist . '" || die';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_compile() {';
    $lines[] = '\temake CC="$(tc-getCC)" loadable';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_install() {';
    $lines[] = '\tlocal npm_store="/usr/$(get_libdir)/${NPM_STORE_SUFFIX}"';
    $lines[] = '\tdodir "${npm_store}"';
    $lines[] = '\tinsinto "${npm_store}"';
    $lines[] = '\tdoins "${WORKDIR}/npm-package/package.json"';
    $lines[] = '\t[[ -f "${WORKDIR}/npm-package/README.md" ]] && doins "${WORKDIR}/npm-package/README.md"';
    pushPreShippedBinaryGuard($lines);
    $lines[] = '\tdoins "${S}/dist/vec0.so"';
    $lines[] = '\tchmod -R u+rwX,go+rX "${D}${npm_store}" || die';
    $lines[] = '\tif [[ -f "${D}${npm_store}/package.json" ]]; then';
    $lines[] = '\t\techo "${NPM_NAME}@${NPM_VERSION}" > "${D}${npm_store}/.gentoo-npm-id" || die';
    $lines[] = '\tfi';
    $lines[] = '}';
    $lines[] = '';
    return renderLines($lines);
}


function renderTreeSitterNodeAddonSourceEbuild(array $meta, array $opts): string {
    $npmName = $meta['name'];
    $version = (string)$meta['version'];
    $slot = slot($version);
    $dist = distfileName($npmName, $version);
    $storeSuffix = 'node_modules/.gentoo-npm/' . $npmName . '/' . $slot . '/package';
    $depEntries = depMap($meta, !empty($opts['includeOptional']));
    if (!isset($depEntries['node-addon-api'])) throw new RuntimeException("{$npmName}@{$version} has no node-addon-api dependency");
    if (!isset($depEntries['node-gyp-build'])) throw new RuntimeException("{$npmName}@{$version} has no node-gyp-build dependency");
    $nodeAddonApi = resolveDependencySpec('node-addon-api', (string)$depEntries['node-addon-api']);
    $nodeGypBuild = resolveDependencySpec('node-gyp-build', (string)$depEntries['node-gyp-build']);
    $description = substr(str_replace("\n", ' ', $meta['description'] ?? "Tree-sitter grammar {$npmName}"), 0, 120);
    $homepage = is_array($meta['homepage'] ?? null) ? ($meta['homepage'][0] ?? '') : ($meta['homepage'] ?? "https://www.npmjs.com/package/{$npmName}");
    $lines = [];
    pushGentooHeader($lines);
    $lines[] = 'EAPI=8';
    $lines[] = '';
    $lines[] = '# Generated by: ' . ebuildComment($opts['generationCommand']);
    $lines[] = '';
    $lines[] = 'inherit edo';
    $lines[] = '';
    $lines[] = 'NPM_NAME=' . bashQuote($npmName);
    $lines[] = 'NPM_VERSION=' . bashQuote($version);
    $lines[] = 'NPM_SLOT=' . bashQuote($slot);
    $lines[] = 'NPM_STORE_SUFFIX=' . bashQuote($storeSuffix);
    $lines[] = 'NODE_ADDON_API_SLOT=' . bashQuote(slot((string)$nodeAddonApi['version']));
    $lines[] = 'NODE_GYP_BUILD_SLOT=' . bashQuote(slot((string)$nodeGypBuild['version']));
    $lines[] = '';
    $lines[] = 'DESCRIPTION=' . bashQuote($description . ' (native addon built from source)');
    $lines[] = 'HOMEPAGE=' . bashQuote($homepage ?: "https://www.npmjs.com/package/{$npmName}");
    $lines[] = 'SRC_URI=' . bashQuote($meta['dist']['tarball'] . ' -> ' . $dist);
    $lines[] = 'S="${WORKDIR}/npm-package"';
    $lines[] = '';
    $lines[] = 'LICENSE=' . bashQuote(normalizeLicense($meta['license'] ?? null));
    $lines[] = 'SLOT=' . bashQuote($slot);
    $lines[] = 'KEYWORDS="amd64 arm64"';
    $lines[] = 'RESTRICT="test ccache"';
    $lines[] = '';
    $lines[] = 'RDEPEND="';
    $lines[] = '\t' . npmDepAtom($opts['depCategory'], $nodeAddonApi['name'], (string)$nodeAddonApi['version']);
    $lines[] = '\t' . npmDepAtom($opts['depCategory'], $nodeGypBuild['name'], (string)$nodeGypBuild['version']);
    $lines[] = '"';
    $lines[] = 'BDEPEND="';
    $lines[] = '\tnet-libs/nodejs[npm]';
    $lines[] = '\t' . npmDepAtom($opts['depCategory'], $nodeAddonApi['name'], (string)$nodeAddonApi['version']);
    $lines[] = '"';
    $lines[] = '';
    $lines[] = 'src_unpack() {';
    $lines[] = '\tmkdir -p "${S}" || die';
    $lines[] = '\tedo tar -C "${S}" --strip-components=1 -xzf "${DISTDIR}/' . $dist . '" --no-same-owner --no-same-permissions';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_prepare() {';
    $lines[] = '\tdefault';
    $lines[] = '\trm -rf prebuilds || die';
    $lines[] = '\tlocal npm_deps="/usr/$(get_libdir)/node_modules/.gentoo-npm"';
    $lines[] = '\tmkdir -p node_modules || die';
    $lines[] = '\tcp -a "${npm_deps}/node-addon-api/${NODE_ADDON_API_SLOT}/package" node_modules/node-addon-api || die';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_compile() {';
    $lines[] = '\texport npm_config_nodedir=/usr';
    pushPkgConfigMultilibEnv($lines);
    $lines[] = '\tedo node /usr/$(get_libdir)/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild --nodedir=/usr';
    pushNodeGypPkgConfigIncludeGuard($lines);
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_install() {';
    $lines[] = '\tlocal npm_store="/usr/$(get_libdir)/${NPM_STORE_SUFFIX}"';
    $lines[] = '\tlocal built_node';
    $lines[] = '\tbuilt_node=$(find "${S}/build/Release" -maxdepth 1 -type f -name \'*.node\' -print -quit) || die';
    $lines[] = '\t[[ -n ${built_node} ]] || die "node-gyp did not produce a native addon"';
    $lines[] = '\tdodir "${npm_store}"';
    $lines[] = '\tcp -a "${S}"/. "${D}${npm_store}" || die';
    $lines[] = '\trm -rf "${D}${npm_store}/prebuilds" "${D}${npm_store}/build" "${D}${npm_store}/node_modules" || die';
    pushPreShippedBinaryGuard($lines);
    $lines[] = '\tinstall -Dm0755 "${built_node}" "${D}${npm_store}/build/Release/${built_node##*/}" || die';
    $lines[] = '\tdodir "${npm_store}/node_modules"';
    $lines[] = '\tdosym "/usr/$(get_libdir)/node_modules/.gentoo-npm/node-addon-api/${NODE_ADDON_API_SLOT}/package" "${npm_store}/node_modules/node-addon-api"';
    $lines[] = '\tdosym "/usr/$(get_libdir)/node_modules/.gentoo-npm/node-gyp-build/${NODE_GYP_BUILD_SLOT}/package" "${npm_store}/node_modules/node-gyp-build"';
    $lines[] = '\tchmod -R u+rwX,go+rX "${D}${npm_store}" || die';
    $lines[] = '\techo "${NPM_NAME}@${NPM_VERSION}" > "${D}${npm_store}/.gentoo-npm-id" || die';
    $lines[] = '}';
    $lines[] = '';
    return renderLines($lines);
}


function renderNodeGypBuildAddonSourceEbuild(array $meta, array $opts): string {
    $npmName = $meta['name'];
    $version = (string)$meta['version'];
    $slot = slot($version);
    $dist = distfileName($npmName, $version);
    $storeSuffix = 'node_modules/.gentoo-npm/' . $npmName . '/' . $slot . '/package';
    $depEntries = depMap($meta, !empty($opts['includeOptional']));
    if (!isset($depEntries['node-gyp-build'])) throw new RuntimeException("{$npmName}@{$version} has no node-gyp-build dependency");
    $nodeGypBuild = resolveDependencySpec('node-gyp-build', (string)$depEntries['node-gyp-build']);
    $nodeAddonApi = isset($depEntries['node-addon-api']) ? resolveDependencySpec('node-addon-api', (string)$depEntries['node-addon-api']) : null;
    $description = substr(str_replace("\n", ' ', $meta['description'] ?? "native Node addon {$npmName}"), 0, 120);
    $homepage = is_array($meta['homepage'] ?? null) ? ($meta['homepage'][0] ?? '') : ($meta['homepage'] ?? "https://www.npmjs.com/package/{$npmName}");
    $lines = [];
    pushGentooHeader($lines);
    $lines[] = 'EAPI=8';
    $lines[] = '';
    $lines[] = '# Generated by: ' . ebuildComment($opts['generationCommand']);
    $lines[] = '';
    $lines[] = 'inherit edo';
    $lines[] = '';
    $lines[] = 'NPM_NAME=' . bashQuote($npmName);
    $lines[] = 'NPM_VERSION=' . bashQuote($version);
    $lines[] = 'NPM_SLOT=' . bashQuote($slot);
    $lines[] = 'NPM_STORE_SUFFIX=' . bashQuote($storeSuffix);
    $lines[] = 'NODE_GYP_BUILD_SLOT=' . bashQuote(slot((string)$nodeGypBuild['version']));
    if ($nodeAddonApi) $lines[] = 'NODE_ADDON_API_SLOT=' . bashQuote(slot((string)$nodeAddonApi['version']));
    $lines[] = '';
    $lines[] = 'DESCRIPTION=' . bashQuote($description . ' (native addon built from source)');
    $lines[] = 'HOMEPAGE=' . bashQuote($homepage ?: "https://www.npmjs.com/package/{$npmName}");
    $lines[] = 'SRC_URI=' . bashQuote($meta['dist']['tarball'] . ' -> ' . $dist);
    $lines[] = 'S="${WORKDIR}/npm-package"';
    $lines[] = '';
    $lines[] = 'LICENSE=' . bashQuote(normalizeLicense($meta['license'] ?? null));
    $lines[] = 'SLOT=' . bashQuote($slot);
    $lines[] = 'KEYWORDS="amd64 arm64"';
    $lines[] = 'RESTRICT="test ccache"';
    $lines[] = '';
    $lines[] = 'RDEPEND="';
    if ($nodeAddonApi) $lines[] = '\t' . npmDepAtom($opts['depCategory'], $nodeAddonApi['name'], (string)$nodeAddonApi['version']);
    $lines[] = '\t' . npmDepAtom($opts['depCategory'], $nodeGypBuild['name'], (string)$nodeGypBuild['version']);
    $lines[] = '"';
    $lines[] = 'BDEPEND="';
    $lines[] = '\tnet-libs/nodejs[npm]';
    if ($nodeAddonApi) $lines[] = '\t' . npmDepAtom($opts['depCategory'], $nodeAddonApi['name'], (string)$nodeAddonApi['version']);
    $lines[] = '"';
    $lines[] = '';
    $lines[] = 'src_unpack() {';
    $lines[] = '\tmkdir -p "${S}" || die';
    $lines[] = '\tedo tar -C "${S}" --strip-components=1 -xzf "${DISTDIR}/' . $dist . '" --no-same-owner --no-same-permissions';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_prepare() {';
    $lines[] = '\tdefault';
    $lines[] = '\trm -rf prebuilds || die';
    if ($nodeAddonApi) {
        $lines[] = '\tlocal npm_deps="/usr/$(get_libdir)/node_modules/.gentoo-npm"';
        $lines[] = '\tmkdir -p node_modules || die';
        $lines[] = '\tcp -a "${npm_deps}/node-addon-api/${NODE_ADDON_API_SLOT}/package" node_modules/node-addon-api || die';
    }
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_compile() {';
    $lines[] = '\texport npm_config_nodedir=/usr';
    pushPkgConfigMultilibEnv($lines);
    $lines[] = '\tedo node /usr/$(get_libdir)/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild --nodedir=/usr';
    pushNodeGypPkgConfigIncludeGuard($lines);
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_install() {';
    $lines[] = '\tlocal npm_store="/usr/$(get_libdir)/${NPM_STORE_SUFFIX}"';
    $lines[] = '\tlocal built_node';
    $lines[] = '\tbuilt_node=$(find "${S}/build/Release" -maxdepth 1 -type f -name \'*.node\' -print -quit) || die';
    $lines[] = '\t[[ -n ${built_node} ]] || die "node-gyp did not produce a native addon"';
    $lines[] = '\tdodir "${npm_store}"';
    $lines[] = '\tcp -a "${S}"/. "${D}${npm_store}" || die';
    $lines[] = '\trm -rf "${D}${npm_store}/prebuilds" "${D}${npm_store}/build" "${D}${npm_store}/node_modules" || die';
    pushPreShippedBinaryGuard($lines);
    $lines[] = '\tinstall -Dm0755 "${built_node}" "${D}${npm_store}/build/Release/${built_node##*/}" || die';
    $lines[] = '\tdodir "${npm_store}/node_modules"';
    if ($nodeAddonApi) $lines[] = '\tdosym "/usr/$(get_libdir)/node_modules/.gentoo-npm/node-addon-api/${NODE_ADDON_API_SLOT}/package" "${npm_store}/node_modules/node-addon-api"';
    $lines[] = '\tdosym "/usr/$(get_libdir)/node_modules/.gentoo-npm/node-gyp-build/${NODE_GYP_BUILD_SLOT}/package" "${npm_store}/node_modules/node-gyp-build"';
    $lines[] = '\tchmod -R u+rwX,go+rX "${D}${npm_store}" || die';
    $lines[] = '\techo "${NPM_NAME}@${NPM_VERSION}" > "${D}${npm_store}/.gentoo-npm-id" || die';
    $lines[] = '}';
    $lines[] = '';
    return renderLines($lines);
}


function isNodeGypBuildSourcePackage(array $meta): bool {
    $install = $meta['scripts']['install'] ?? '';
    if (!is_string($install) || !str_contains($install, 'node-gyp-build')) return false;
    if (!isset($meta['dependencies']['node-gyp-build']) && !isset($meta['optionalDependencies']['node-gyp-build'])) return false;
    return packageTarballContainsPath($meta, 'binding.gyp');
}


function render7zipBinSystemEbuild(array $meta, array $opts): string {
    $npmName = $meta['name'];
    $version = (string)$meta['version'];
    $slot = slot($version);
    $pn = gentooPn($npmName);
    $pv = gentooPv($version);
    $dist = distfileName($npmName, $version);
    $storeSuffix = 'node_modules/.gentoo-npm/' . $npmName . '/' . $slot . '/package';
    $description = substr(str_replace("\n", ' ', $meta['description'] ?? '7-Zip npm system wrapper'), 0, 120);
    $homepage = is_array($meta['homepage'] ?? null) ? ($meta['homepage'][0] ?? '') : ($meta['homepage'] ?? 'https://www.npmjs.com/package/7zip-bin');
    $lines = [];
    pushGentooHeader($lines);
    $lines[] = 'EAPI=8';
    $lines[] = '';
    $lines[] = '# Generated by: ' . ebuildComment($opts['generationCommand']);
    $lines[] = '';
    $lines[] = 'NPM_NAME=' . bashQuote($npmName);
    $lines[] = 'NPM_VERSION=' . bashQuote($version);
    $lines[] = 'NPM_SLOT=' . bashQuote($slot);
    $lines[] = 'NPM_STORE_SUFFIX=' . bashQuote($storeSuffix);
    $lines[] = '';
    $lines[] = 'DESCRIPTION=' . bashQuote($description . ' (using Gentoo system 7za)');
    $lines[] = 'HOMEPAGE=' . bashQuote($homepage ?: 'https://www.npmjs.com/package/7zip-bin');
    $lines[] = 'SRC_URI=' . bashQuote($meta['dist']['tarball'] . ' -> ' . $dist);
    $lines[] = 'S="${WORKDIR}"';
    $lines[] = '';
    $lines[] = 'LICENSE="MIT"';
    $lines[] = 'SLOT=' . bashQuote($slot);
    $lines[] = 'KEYWORDS="amd64 arm64"';
    $lines[] = 'RESTRICT="test"';
    $lines[] = '';
    $lines[] = 'RDEPEND="';
    $lines[] = '\t|| ( app-arch/7zip[symlink] app-arch/p7zip )';
    $lines[] = '"';
    $lines[] = '';
    $lines[] = 'src_unpack() {';
    $lines[] = '\tmkdir -p "${WORKDIR}/npm-package" || die';
    $lines[] = '\ttar -C "${WORKDIR}/npm-package" --strip-components=1 -xzf "${DISTDIR}/' . $dist . '" --no-same-owner --no-same-permissions || die';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_install() {';
    $lines[] = '\tlocal npm_store="/usr/$(get_libdir)/${NPM_STORE_SUFFIX}"';
    $lines[] = '\tdodir "${npm_store}"';
    $lines[] = '\tcp -a "${WORKDIR}/npm-package"/. "${D}${npm_store}" || die';
    $lines[] = '\trm -rf "${D}${npm_store}"/linux "${D}${npm_store}"/mac "${D}${npm_store}"/win || die';
    $lines[] = '\tsed -i \'s/process.env.USE_SYSTEM_7ZA === "true"/true/\' "${D}${npm_store}/index.js" || die';
    pushPreShippedBinaryGuard($lines);
    $lines[] = '\tchmod -R u+rwX,go+rX "${D}${npm_store}" || die';
    $lines[] = '\techo "${NPM_NAME}@${NPM_VERSION}" > "${D}${npm_store}/.gentoo-npm-id" || die';
    $lines[] = '}';
    $lines[] = '';
    return renderLines($lines);
}


function renderAppBuilderBinSystemEbuild(array $meta, array $opts): string {
    $npmName = $meta['name'];
    $version = (string)$meta['version'];
    $slot = slot($version);
    $dist = distfileName($npmName, $version);
    $storeSuffix = 'node_modules/.gentoo-npm/' . $npmName . '/' . $slot . '/package';
    $description = substr(str_replace("\n", ' ', $meta['description'] ?? 'app-builder npm system wrapper'), 0, 120);
    $homepage = is_array($meta['homepage'] ?? null) ? ($meta['homepage'][0] ?? '') : ($meta['homepage'] ?? 'https://www.npmjs.com/package/app-builder-bin');
    $lines = [];
    pushGentooHeader($lines);
    $lines[] = 'EAPI=8';
    $lines[] = '';
    $lines[] = '# Generated by: ' . ebuildComment($opts['generationCommand']);
    $lines[] = '';
    $lines[] = 'NPM_NAME=' . bashQuote($npmName);
    $lines[] = 'NPM_VERSION=' . bashQuote($version);
    $lines[] = 'NPM_SLOT=' . bashQuote($slot);
    $lines[] = 'NPM_STORE_SUFFIX=' . bashQuote($storeSuffix);
    $lines[] = '';
    $lines[] = 'DESCRIPTION=' . bashQuote($description . ' (using Gentoo system app-builder)');
    $lines[] = 'HOMEPAGE=' . bashQuote($homepage ?: 'https://www.npmjs.com/package/app-builder-bin');
    $lines[] = 'SRC_URI=' . bashQuote($meta['dist']['tarball'] . ' -> ' . $dist);
    $lines[] = 'S="${WORKDIR}"';
    $lines[] = '';
    $lines[] = 'LICENSE="MIT"';
    $lines[] = 'SLOT=' . bashQuote($slot);
    $lines[] = 'KEYWORDS="amd64 arm64"';
    $lines[] = 'RESTRICT="test"';
    $lines[] = '';
    $lines[] = 'RDEPEND="';
    $lines[] = '\tdev-util/app-builder';
    $lines[] = '"';
    $lines[] = '';
    $lines[] = 'src_unpack() {';
    $lines[] = '\tmkdir -p "${WORKDIR}/npm-package" || die';
    $lines[] = '\ttar -C "${WORKDIR}/npm-package" --strip-components=1 -xzf "${DISTDIR}/' . $dist . '" --no-same-owner --no-same-permissions || die';
    $lines[] = '}';
    $lines[] = '';
    $lines[] = 'src_install() {';
    $lines[] = '\tlocal npm_store="/usr/$(get_libdir)/${NPM_STORE_SUFFIX}"';
    $lines[] = '\tdodir "${npm_store}"';
    $lines[] = '\tcp -a "${WORKDIR}/npm-package"/. "${D}${npm_store}" || die';
    $lines[] = '\trm -rf "${D}${npm_store}"/linux "${D}${npm_store}"/mac "${D}${npm_store}"/win || die';
    $lines[] = '\tsed -i \'s/process.env.USE_SYSTEM_APP_BUILDER === "true"/true/\' "${D}${npm_store}/index.js" || die';
    pushPreShippedBinaryGuard($lines);
    $lines[] = '\tchmod -R u+rwX,go+rX "${D}${npm_store}" || die';
    $lines[] = '\techo "${NPM_NAME}@${NPM_VERSION}" > "${D}${npm_store}/.gentoo-npm-id" || die';
    $lines[] = '}';
    $lines[] = '';
    return renderLines($lines);
}


function pushPkgConfigMultilibEnv(array &$lines): void {
    $lines[] = '\texport PKG_CONFIG_PATH="${EPREFIX}/usr/$(get_libdir)/pkgconfig:${EPREFIX}/usr/share/pkgconfig"';
    $lines[] = '\texport PKG_CONFIG_LIBDIR="${EPREFIX}/usr/$(get_libdir)/pkgconfig:${EPREFIX}/usr/share/pkgconfig"';
}

function pushNodeGypPkgConfigIncludeGuard(array &$lines, string $root = '${S}'): void {
    $lines[] = '\tif [[ $(get_libdir) != "lib" ]]; then';
    $lines[] = '\t\tlocal bad_pkg_config_includes';
    $lines[] = '\t\tbad_pkg_config_includes=$(find "' . $root . '" -path \'*/.deps/*\' -type f -exec grep -H -E \'/usr/lib/(glib-2.0|libffi)/include\' {} + 2>/dev/null || true)';
    $lines[] = '\t\t[[ -z ${bad_pkg_config_includes} ]] || die "node-gyp used non-multilib pkg-config include paths: ${bad_pkg_config_includes}"';
    $lines[] = '\tfi';
}

function pushPreShippedBinaryGuard(array &$lines): void {
    $lines[] = '\tlocal preshipped_binary';
    $lines[] = "\tpreshipped_binary=\$(find \"\${D}\${npm_store}\" -type f -exec grep -al \$'\\177ELF' {} + | head -n 1) || die";
    $lines[] = '\t[[ -z ${preshipped_binary} ]] || die "pre shipped binary not allowed: ${preshipped_binary#${D}}"';
}

function pushCargoCrates(array &$lines, array $crates): void {
    sort($crates, SORT_NATURAL);
    $lines[] = 'CRATES="';
    foreach ($crates as $crate) $lines[] = "\t{$crate}";
    $lines[] = '"';
    $lines[] = '';
}

function cargoCratesFromNpmTarball(string $npmName, string $version): array {
    static $cache = [];
    $key = $npmName . '@' . $version;
    if (isset($cache[$key])) return $cache[$key];

    $tmp = sys_get_temp_dir() . '/npm-ebuild-generator-cargo-' . getmypid() . '-' . bin2hex(random_bytes(4));
    mkdirRecursive($tmp);
    try {
        $packed = trim(runCommandCapture(['npm', 'pack', $key, '--pack-destination', $tmp, '--silent'], 120));
        $packLines = preg_split('/\r?\n/', $packed);
        $archive = $packLines ? trim($packLines[count($packLines) - 1]) : '';
        if ($archive === '') throw new RuntimeException("npm pack did not return an archive for {$key}");
        if (!str_starts_with($archive, '/')) $archive = $tmp . '/' . $archive;

        runCommandCapture(['tar', '-xzf', $archive, '-C', $tmp], 120);
        $sourceDir = $tmp . '/package';
        if (!file_exists($sourceDir . '/Cargo.toml')) {
            throw new RuntimeException("{$key} npm tarball has no Cargo.toml; cannot generate cargo crate URIs");
        }

        runCommandCaptureCwd(['cargo', 'generate-lockfile'], 180, $sourceDir);
        $lock = file_get_contents($sourceDir . '/Cargo.lock');
        if ($lock === false) throw new RuntimeException("cargo did not generate Cargo.lock for {$key}");

        $crates = [];
        foreach (preg_split('/\n\[\[package\]\]\n/', "\n" . $lock) ?: [] as $block) {
            if (!str_contains($block, 'source = "registry+')) continue;
            if (!preg_match('/^name = "([^"]+)"/m', $block, $name)) continue;
            if (!preg_match('/^version = "([^"]+)"/m', $block, $ver)) continue;
            $crates[] = $name[1] . '@' . $ver[1];
        }
        $cache[$key] = array_values(array_unique($crates));
        return $cache[$key];
    } finally {
        removeRecursive($tmp);
    }
}

function renderLines(array $lines): string {
    return str_replace('\\t', "\t", implode("\n", $lines) . "\n");
}

function selectEbuildTarget(string $dir, string $pn, string $pv, string $body): array {
    $baseFile = $dir . '/' . $pn . '-' . $pv . '.ebuild';
    if (!is_dir($dir)) return ['action' => 'write', 'file' => $baseFile];
    $existing = matchingEbuilds($dir, $pn, $pv);
    if (!$existing) return ['action' => 'write', 'file' => $baseFile];
    $newest = $existing[count($existing) - 1];
    $oldBody = file_get_contents($newest['file']);
    $body = mergeGeneratedByHistory($body, $oldBody ?: '');
    if (ebuildChangeSignature($oldBody ?: '') === ebuildChangeSignature($body)) return ['action' => 'skip', 'file' => $newest['file']];
    $nextRev = $newest['rev'] + 1;
    return ['action' => 'revise', 'file' => $dir . '/' . $pn . '-' . $pv . '-r' . $nextRev . '.ebuild', 'body' => $body];
}

function effectiveIncludeOptionalForExisting(string $outDir, string $cat, string $pn, string $pv, bool $requested): bool {
    if ($requested) return true;
    $latest = latestMatchingEbuild($outDir . '/' . $cat . '/' . $pn, $pn, $pv);
    if ($latest === null) return false;
    $body = file_get_contents($latest['file']);
    return generatedByUsedOption($body ?: '', '--include-optional');
}

function latestMatchingEbuild(string $dir, string $pn, string $pv): ?array {
    if (!is_dir($dir)) return null;
    $existing = matchingEbuilds($dir, $pn, $pv);
    return $existing ? $existing[count($existing) - 1] : null;
}

function matchingEbuilds(string $dir, string $pn, string $pv): array {
    $existing = [];
    foreach (scandir($dir) ?: [] as $name) {
        if (preg_match('/^' . preg_quote($pn, '/') . '-' . preg_quote($pv, '/') . '(?:-r([0-9]+))?\.ebuild$/', $name, $m)) {
            $existing[] = ['name' => $name, 'file' => $dir . '/' . $name, 'rev' => isset($m[1]) ? (int)$m[1] : 0];
        }
    }
    usort($existing, fn($a, $b) => $a['rev'] <=> $b['rev']);
    return $existing;
}

function generatedByUsedOption(string $body, string $option): bool {
    foreach (generatedByLines($body) as $line) {
        if (str_contains($line, $option)) return true;
    }
    return false;
}

function mergeGeneratedByHistory(string $newBody, string $oldBody): string {
    $oldGenerated = generatedByLines($oldBody);
    if (!$oldGenerated) return $newBody;
    $newGenerated = generatedByLines($newBody);
    if (!$newGenerated) return $newBody;
    $merged = [];
    foreach (array_merge($oldGenerated, $newGenerated) as $line) {
        if (!in_array($line, $merged, true)) $merged[] = $line;
    }
    if ($merged === $newGenerated) return $newBody;

    $lines = preg_split('/\r?\n/', $newBody);
    $out = [];
    $inserted = false;
    foreach ($lines as $line) {
        if (str_starts_with($line, '# Generated by: ')) {
            if (!$inserted) {
                foreach ($merged as $generatedLine) $out[] = $generatedLine;
                $inserted = true;
            }
            continue;
        }
        $out[] = $line;
    }
    return implode("\n", $out);
}

function generatedByLines(string $body): array {
    $lines = [];
    foreach (preg_split('/\r?\n/', $body) as $line) {
        if (str_starts_with($line, '# Generated by: ')) $lines[] = rtrim($line);
    }
    return $lines;
}

function ebuildChangeSignature(string $body): string {
    return json_encode([
        'srcUri' => normalizeSignature(extractSrcUri($body)),
        'rdepend' => normalizeSignature(extractEbuildAssignment($body, 'RDEPEND')),
        'depend' => normalizeSignature(extractEbuildAssignment($body, 'DEPEND')),
        'bdepend' => normalizeSignature(extractEbuildAssignment($body, 'BDEPEND')),
        'srcPrepare' => normalizeSignature(extractShellFunction($body, 'src_prepare')),
        'srcCompile' => normalizeSignature(extractShellFunction($body, 'src_compile')),
        'srcInstall' => normalizeSignature(extractShellFunction($body, 'src_install')),
    ], JSON_UNESCAPED_SLASHES);
}

function extractSrcUri(string $body): string {
    $lines = preg_split('/\r?\n/', $body);
    $out = [];
    for ($i = 0; $i < count($lines); $i++) {
        if (!str_starts_with($lines[$i], 'SRC_URI=')) continue;
        $out[] = $lines[$i];
        if (preg_match('/^SRC_URI="\s*$/', $lines[$i])) {
            for ($i++; $i < count($lines); $i++) {
                $out[] = $lines[$i];
                if (preg_match('/^"\s*$/', $lines[$i])) break;
            }
        }
        break;
    }
    return implode("\n", $out);
}

function extractEbuildAssignment(string $body, string $name): string {
    $lines = preg_split('/\r?\n/', $body);
    $out = [];
    for ($i = 0; $i < count($lines); $i++) {
        if (!str_starts_with($lines[$i], $name . '=')) continue;
        $out[] = $lines[$i];
        if (preg_match('/^' . preg_quote($name, '/') . '="\s*$/', $lines[$i])) {
            for ($i++; $i < count($lines); $i++) {
                $out[] = $lines[$i];
                if (preg_match('/^"\s*$/', $lines[$i])) break;
            }
        }
        break;
    }
    return implode("\n", $out);
}

function extractShellFunction(string $body, string $name): string {
    $lines = preg_split('/\r?\n/', $body);
    $out = [];
    $inFn = false;
    foreach ($lines as $line) {
        if (!$inFn) {
            if ($line === $name . '() {') {
                $inFn = true;
                $out[] = $line;
            }
            continue;
        }
        $out[] = $line;
        if ($line === '}') break;
    }
    return implode("\n", $out);
}

function normalizeSignature(string $s): string { return trim(preg_replace('/[ \t]+$/m', '', $s) ?? $s); }

function gentooHeaderText(): string {
    return '# Copyright 1999-' . date('Y') . " Gentoo Authors\n" .
        '# Distributed under the terms of the GNU General Public License v2';
}

function pushGentooHeader(array &$lines): void {
    foreach (explode("\n", gentooHeaderText()) as $line) $lines[] = $line;
    $lines[] = '';
}

function isSharpNativePackage(string $npmName): bool {
    return $npmName === '@img/sharp-linux-x64' || $npmName === '@img/sharp-linux-arm64';
}

function isMariozechnerClipboardPlatformPackage(string $npmName): bool {
    return preg_match('#^@mariozechner/clipboard-(?:android|darwin|freebsd|linux|win32)-#', $npmName) === 1
        || $npmName === '@mariozechner/clipboard-darwin-universal';
}

function isNapiRsCanvasPlatformPackage(string $npmName): bool {
    return preg_match('#^@napi-rs/canvas-(?:android|darwin|freebsd|linux|win32)-#', $npmName) === 1;
}

function isLydellNodePtyPlatformPackage(string $npmName): bool {
    return preg_match('#^@lydell/node-pty-(?:darwin|linux|win32)-#', $npmName) === 1;
}

function isLydellNodePtyLinuxPackage(string $npmName): bool {
    return $npmName === '@lydell/node-pty-linux-x64' || $npmName === '@lydell/node-pty-linux-arm64';
}

function optionalPlatformArch(string $parentName, string $depName): ?string {
    if ($parentName === 'sqlite-vec') {
        if ($depName === 'sqlite-vec-linux-x64') return 'amd64';
        if ($depName === 'sqlite-vec-linux-arm64') return 'arm64';
    }
    if ($parentName === 'sharp') {
        if ($depName === '@img/sharp-linux-x64') return 'amd64';
        if ($depName === '@img/sharp-linux-arm64') return 'arm64';
    }
    if ($parentName === '@lydell/node-pty') {
        if ($depName === '@lydell/node-pty-linux-x64') return 'amd64';
        if ($depName === '@lydell/node-pty-linux-arm64') return 'arm64';
    }
    return null;
}

function npmDepAtom(string $category, string $npmName, string $version): string {
    return '~' . $category . '/' . gentooPn($npmName) . '-' . gentooPv($version) . ':' . slot($version);
}

function depMap(array $meta, bool $optional): array {
    $deps = $meta['dependencies'] ?? [];
    $optionalDeps = $meta['optionalDependencies'] ?? [];
    if (($meta['name'] ?? '') === 'sqlite-vec') {
        foreach (array_keys($optionalDeps) as $name) {
            if ($name !== 'sqlite-vec-linux-x64' && $name !== 'sqlite-vec-linux-arm64') unset($optionalDeps[$name]);
        }
    }
    if (($meta['name'] ?? '') === 'sharp') {
        foreach (array_keys($optionalDeps) as $name) {
            if (optionalPlatformArch('sharp', $name) === null) unset($optionalDeps[$name]);
        }
    }
    if (($meta['name'] ?? '') === '@mariozechner/clipboard') {
        foreach (array_keys($optionalDeps) as $name) {
            if (isMariozechnerClipboardPlatformPackage($name)) unset($optionalDeps[$name]);
        }
    }
    if (($meta['name'] ?? '') === 'pdfjs-dist') {
        unset($optionalDeps['@napi-rs/canvas']);
    }
    if (($meta['name'] ?? '') === '@lydell/node-pty') {
        foreach (array_keys($optionalDeps) as $name) {
            if (optionalPlatformArch('@lydell/node-pty', $name) === null) unset($optionalDeps[$name]);
        }
    }
    if ($optional || ($meta['name'] ?? '') === 'openclaw' || ($meta['name'] ?? '') === 'sqlite-vec' || ($meta['name'] ?? '') === 'sharp' || ($meta['name'] ?? '') === '@lydell/node-pty') $deps = array_merge($deps, $optionalDeps);
    foreach (($meta['peerDependencies'] ?? []) as $name => $range) {
        $peerMeta = $meta['peerDependenciesMeta'][$name] ?? null;
        if (($peerMeta['optional'] ?? false) && !$optional) continue;
        if (!array_key_exists($name, $deps)) $deps[$name] = $range;
    }
    return $deps;
}

function resolveSpec(string $spec): array {
    $data = npmView($spec, ['name', 'version']);
    return ['name' => $data['name'], 'version' => $data['version']];
}

function resolveDependencySpec(string $depName, string $range): array {
    // npm alias dependencies are declared as e.g.
    //   "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
    // The package to fetch/install is wrap-ansi, but the node_modules entry
    // must keep the alias name wrap-ansi-cjs.  Passing
    // wrap-ansi-cjs@npm:wrap-ansi@^7.0.0 to `npm view` yields E404.
    $spec = str_starts_with($range, 'npm:') ? substr($range, 4) : $depName . '@' . $range;
    $data = resolveSpec($spec);
    $data['linkName'] = $depName;
    return $data;
}

function npmView(string $spec, ?array $fields = null): array {
    global $viewCache;
    $key = json_encode([$spec, $fields]);
    if (isset($viewCache[$key])) return $viewCache[$key];
    $argv = ['npm', 'view', $spec];
    if ($fields) array_push($argv, ...$fields);
    array_push($argv, '--json', '--fetch-timeout=10000', '--fetch-retries=1');
    $raw = runCommandCapture($argv, 30);
    $parsed = trim($raw) !== '' ? json_decode($raw, true, flags: JSON_THROW_ON_ERROR) : [];
    if (array_is_list($parsed) && $parsed) $parsed = $parsed[count($parsed) - 1];
    $viewCache[$key] = $parsed;
    return $parsed;
}

function packageTarballContainsPath(array $meta, string $wantedPath): bool {
    foreach (npmTarballList($meta) as $path) {
        $normalized = normalizeTarballPath($path);
        if ($normalized === $wantedPath) return true;
    }
    return false;
}

function npmTarballList(array $meta): array {
    static $cache = [];
    $npmName = (string)($meta['name'] ?? '');
    $version = (string)($meta['version'] ?? '');
    $url = (string)($meta['dist']['tarball'] ?? '');
    if ($npmName === '' || $version === '' || $url === '') throw new RuntimeException('Cannot inspect npm tarball without name, version and dist.tarball');
    $key = $npmName . '@' . $version;
    if (isset($cache[$key])) return $cache[$key];

    $distfile = distfileName($npmName, $version);
    $tarball = findCachedDistfile($distfile);
    $tmpDir = null;
    if ($tarball === null) {
        $tmpDir = rtrim(sys_get_temp_dir(), '/') . '/npm-ebuild-generator-' . bin2hex(random_bytes(6));
        mkdirRecursive($tmpDir);
        $tarball = $tmpDir . '/' . $distfile;
        try {
            runCommandCapture(['curl', '-fL', '--connect-timeout', '10', '--retry', '1', '-o', $tarball, $url], 60);
        } catch (Throwable $e) {
            removeRecursive($tmpDir);
            throw $e;
        }
    }

    try {
        $raw = runCommandCapture(['tar', '-tzf', $tarball], 60);
        $list = array_values(array_filter(array_map('trim', explode("\n", $raw)), fn($p) => $p !== ''));
        $cache[$key] = $list;
        return $list;
    } finally {
        if ($tmpDir !== null) removeRecursive($tmpDir);
    }
}

function findCachedDistfile(string $distfile): ?string {
    $dirs = [];
    foreach (['DISTDIR', 'PORTAGE_ACTUAL_DISTDIR'] as $envName) {
        $value = getenv($envName);
        if (is_string($value) && $value !== '') $dirs[] = $value;
    }
    $dirs[] = '/var/cache/distfiles';
    $dirs[] = '/var/cache/distfiles/npm';
    $dirs[] = getcwd();
    foreach (array_unique($dirs) as $dir) {
        $candidate = rtrim($dir, '/') . '/' . $distfile;
        if (is_file($candidate) && is_readable($candidate)) return $candidate;
    }
    return null;
}

function normalizeTarballPath(string $path): string {
    $path = ltrim($path, './');
    if (str_starts_with($path, 'package/')) return substr($path, 8);
    $slash = strpos($path, '/');
    if ($slash !== false) return substr($path, $slash + 1);
    return $path;
}

function runManifestForEbuild(string $file, bool $progress): void {
    $repoRoot = findGentooRepoRoot(dirname($file));
    if (!$repoRoot) {
        if ($progress) writeStderr("[manifest] skip {$file}: not inside a Gentoo repo with metadata/layout.conf\n");
        return;
    }
    $dir = dirname($file);
    $name = basename($file);
    if ($progress) writeStderr("[manifest] running in {$dir}: ebuild {$name} manifest\n");
    passthruCommand(['ebuild', $name, 'manifest'], $dir);
}

function findGentooRepoRoot(string $startDir): ?string {
    $dir = realpath($startDir) ?: $startDir;
    while (true) {
        if (file_exists($dir . '/metadata/layout.conf')) return $dir;
        $parent = dirname($dir);
        if ($parent === $dir) return null;
        $dir = $parent;
    }
}

function gentooPn(string $npmName): string {
    $s = strtolower($npmName);
    $s = preg_replace('/^@/', '', $s);
    $s = preg_replace('#[/_.]+#', '-', $s);
    $s = preg_replace('/[^a-z0-9+.-]/', '-', $s);
    $s = preg_replace('/-+/', '-', $s);
    // Sebastian masks generated packages containing "-bin-".  Treat npm's
    // standalone "bin" name segment as "binary" so packages like pngquant-bin
    // do not become masked by local policy.
    $s = preg_replace('/(^|-)bin(?=-|$)/', '$1binary', $s);
    // Gentoo atoms are ambiguous/invalid when the package name ends in a
    // dash-separated numeric segment, e.g. crc-32 -> npm-crc-32-1.2.2.  Fold a
    // trailing numeric segment into the previous segment: crc-32 -> npm-crc32.
    $s = preg_replace('/-([0-9]+)$/', '$1', $s);
    return 'npm-' . $s;
}

function gentooPv(string $v): string {
    $v = trim($v);

    // NPM semver allows arbitrary prerelease/build identifiers such as
    // 1.16.1-lts.  Gentoo PV suffixes may only use the known textual suffixes
    // (_alpha/_beta/_pre/_rc/_p) followed by digits, so keep common suffixes
    // readable and encode unknown semver labels numerically.
    if (preg_match('/^([0-9]+(?:\.[0-9]+)*[a-z]?)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/', $v, $m)) {
        $out = $m[1];
        $pre = $m[2] ?? '';
        $build = $m[3] ?? '';

        if ($pre !== '') {
            if (preg_match('/^(alpha|beta|pre|rc)(?:[.-]?([0-9.]+))?$/i', $pre, $pm)) {
                $out .= '_' . strtolower($pm[1]) . str_replace('.', '', $pm[2] ?? '');
            } else {
                $out .= '_pre' . gentooPvLabelCode($pre);
            }
        }
        if ($build !== '') $out .= '_p' . gentooPvLabelCode($build);
        return $out;
    }

    return preg_replace('/[^0-9A-Za-z._]/', '_', str_replace(['-', '+'], '_p', $v));
}

function gentooPvLabelCode(string $label): string {
    return sprintf('%u', crc32(strtolower($label)));
}

function slot(string $v): string { return gentooPv($v); }
function distfileName(string $name, string $version): string { return gentooPn($name) . '-' . gentooPv($version) . '.tgz'; }
function nodeModulesLinkPath(string $name): string { return str_starts_with($name, '@') ? $name : $name; }
function posixDirname(string $path): string { $d = str_replace('\\', '/', dirname($path)); return $d === '' ? '.' : $d; }

function normalizeLicense(mixed $x): string {
    if (!$x) return 'all-rights-reserved';
    if (is_array($x)) $x = $x['type'] ?? json_encode($x, JSON_UNESCAPED_SLASHES);
    $s = trim(str_replace(['(', ')'], '', (string)$x));
    $orParts = array_values(array_filter(array_map('trim', preg_split('/\s+OR\s+|\s*\|\|\s*/i', $s) ?: [])));
    if (count($orParts) > 1) {
        $preferred = ['MIT', 'BSD-2-Clause', 'BSD', 'ISC', 'Apache-2.0', 'MPL-2.0'];
        foreach ($preferred as $pref) foreach ($orParts as $part) if (strcasecmp($part, $pref) === 0) return $part;
        return $orParts[0];
    }
    return preg_replace('/\s+AND\s+/i', ' && ', $s) ?? $s;
}

function bashQuote(string $s): string {
    return '"' . str_replace(['\\', '"', '$', '`'], ['\\\\', '\\"', '\\$', '\\`'], $s) . '"';
}

function shellJoin(array $argv): string { return implode(' ', array_map('shellQuoteArg', $argv)); }
function shellQuoteArg(string $s): string { return preg_match('/^[A-Za-z0-9_@%+=:,\.\/-]+$/', $s) ? $s : "'" . str_replace("'", "'\\''", $s) . "'"; }
function ebuildComment(string $s): string { return trim(str_replace(["\r", "\n", "\t"], ' ', $s)); }
function mkdirRecursive(string $dir): void { if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) throw new RuntimeException("Could not mkdir {$dir}"); }
function realOrAbsolute(string $path): string { return str_starts_with($path, '/') ? $path : getcwd() . '/' . $path; }

function runCommandCapture(array $argv, int $timeoutSeconds): string {
    $cmd = implode(' ', array_map('escapeshellarg', $argv));
    $desc = [0 => ['file', '/dev/null', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
    $proc = proc_open($cmd, $desc, $pipes);
    if (!is_resource($proc)) throw new RuntimeException('Could not start command: ' . $cmd);
    $out = stream_get_contents($pipes[1]);
    $err = stream_get_contents($pipes[2]);
    fclose($pipes[1]); fclose($pipes[2]);
    $code = proc_close($proc);
    if ($code !== 0) throw new RuntimeException("Command failed ({$code}): {$cmd}\n{$err}");
    return $out;
}

function passthruCommand(array $argv, string $cwd): void {
    $cmd = implode(' ', array_map('escapeshellarg', $argv));
    $old = getcwd();
    chdir($cwd);
    passthru($cmd, $code);
    chdir($old);
    if ($code !== 0) writeStderr("warning: manifest failed for {$cwd}/" . ($argv[1] ?? '') . ": exit {$code}\n");
}

function runCommandCaptureCwd(array $argv, int $timeoutSeconds, string $cwd): string {
    $cmd = implode(' ', array_map('escapeshellarg', $argv));
    $desc = [0 => ['file', '/dev/null', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
    $proc = proc_open($cmd, $desc, $pipes, $cwd);
    if (!is_resource($proc)) throw new RuntimeException('Could not start command: ' . $cmd);
    $out = stream_get_contents($pipes[1]);
    $err = stream_get_contents($pipes[2]);
    fclose($pipes[1]); fclose($pipes[2]);
    $code = proc_close($proc);
    if ($code !== 0) throw new RuntimeException("Command failed ({$code}) in {$cwd}: {$cmd}\n{$err}");
    return $out;
}

function removeRecursive(string $path): void {
    if (!file_exists($path)) return;
    if (is_file($path) || is_link($path)) {
        @unlink($path);
        return;
    }
    foreach (scandir($path) ?: [] as $entry) {
        if ($entry === '.' || $entry === '..') continue;
        removeRecursive($path . '/' . $entry);
    }
    @rmdir($path);
}


function writeStderr(string $message): void {
    static $stderr = null;
    if ($stderr === null) $stderr = fopen('php://stderr', 'w');
    if (is_resource($stderr)) fwrite($stderr, $message);
}

function parseArgs(array $argv): array {
    $out = ['packages' => []];
    for ($i = 0; $i < count($argv); $i++) {
        $a = $argv[$i];
        if ($a === '--package' || $a === '-p') {
            $value = $argv[++$i] ?? null;
            if ($value === null || $value === '') throw new RuntimeException("Missing value for {$a}");
            $out['packages'][] = $value;
        }
        elseif ($a === '--out' || $a === '-o') $out['out'] = $argv[++$i] ?? null;
        elseif ($a === '--category') $out['category'] = $argv[++$i] ?? null;
        elseif ($a === '--app-category') $out['appCategory'] = $argv[++$i] ?? null;
        elseif ($a === '--include-optional') $out['includeOptional'] = true;
        elseif ($a === '--deps-only') $out['depsOnly'] = true;
        elseif ($a === '--max-depth') $out['maxDepth'] = $argv[++$i] ?? null;
        elseif ($a === '--dry-run') $out['dryRun'] = true;
        elseif ($a === '--progress') $out['progress'] = true;
        elseif ($a === '--help' || $a === '-h') usage(0);
        elseif (!str_starts_with($a, '-')) $out['packages'][] = $a;
        else throw new RuntimeException("Unknown argument: {$a}");
    }
    return $out;
}

function usage(int $code): never {
    echo "Usage: npm-ebuild-generator --package openclaw@2026.5.6 --out /path/to/overlay-staging [options]\n";
    echo "       npm-ebuild-generator --deps-only react@16.14.0 redux@4.2.1 --out /path/to/overlay-staging [options]\n\n";
    echo "Options:\n  --category dev-node       Category for generated npm dependency ebuilds\n  --app-category app-misc   Category for root package ebuild\n  --deps-only               Treat requested packages as dependency packages, not root apps\n  --include-optional        Also resolve optionalDependencies\n  --max-depth N             Limit recursion for testing\n  --dry-run                 Resolve only, write nothing\n  --progress                Print resolver progress to stderr\n\n";
    exit($code);
}
