tiny-runtime-injector
Version:
A library that helps you download complete, lightweight runtime environments like Node.js, Bun, uv, and Python. It's useful for including minimal runtimes when building applications like Electron.
907 lines (906 loc) • 40.3 kB
JavaScript
import path from "path";
import fs from "fs-extra";
import os from "os";
import { promisify } from "util";
import { exec } from "child_process";
import axios from "axios";
import { createWriteStream } from "fs";
import { pipeline } from "node:stream/promises";
import * as tar from "tar";
import { glob } from "glob";
const execAsync = promisify(exec);
// Platform configuration for ripgrep
const RIPGREP_PLATFORM = {
"x64-win32": { target: "x86_64-pc-windows-msvc", ext: "zip" },
"arm64-win32": { target: "aarch64-pc-windows-msvc", ext: "zip" },
"x64-linux": { target: "x86_64-unknown-linux-musl", ext: "tar.gz" },
"arm64-linux": { target: "aarch64-unknown-linux-gnu", ext: "tar.gz" },
"x64-darwin": { target: "x86_64-apple-darwin", ext: "tar.gz" },
"arm64-darwin": { target: "aarch64-apple-darwin", ext: "tar.gz" },
};
const RTK_PLATFORM = {
"x64-win32": { target: "x86_64-pc-windows-msvc", ext: "zip" },
"x64-linux": { target: "x86_64-unknown-linux-musl", ext: "tar.gz" },
"arm64-linux": { target: "aarch64-unknown-linux-gnu", ext: "tar.gz" },
"x64-darwin": { target: "x86_64-apple-darwin", ext: "tar.gz" },
"arm64-darwin": { target: "aarch64-apple-darwin", ext: "tar.gz" },
};
const RTK_SUPPORTED_PLATFORMS = "darwin-x64, darwin-arm64, linux-x64, linux-arm64, win32-x64";
// Default versions for each runtime
const DEFAULT_VERSIONS = {
node: "v24.12.0",
bun: "v1.3.5",
uv: "0.9.18",
ripgrep: "14.1.1",
python: "3.12.12+20251217",
rtk: "latest",
};
// Runtime configurations
const RUNTIME_CONFIGS = {
node: {
defaultVersion: DEFAULT_VERSIONS.node,
getDownloadUrl: (version, platform, arch) => {
const platformId = getNodePlatformIdentifier(platform, arch);
const fileExtension = platform === "win32" ? "zip" : "tar.gz";
const fileName = `node-${version}-${platformId}.${fileExtension}`;
return `https://nodejs.org/dist/${version}/${fileName}`;
},
getFileExtension: (platform, arch) => platform === "win32" ? "zip" : "tar.gz",
getExecutablePath: (targetDir, platform) => path.join(targetDir, platform === "win32" ? "node.exe" : "bin/node"),
extractFiles: async (extractedDir, targetDir, version, platform, arch) => {
const platformId = getNodePlatformIdentifier(platform, arch);
const nodeDirName = `node-${version}-${platformId}`;
const extractedNodeDir = path.join(extractedDir, nodeDirName);
const files = await fs.readdir(extractedNodeDir);
for (const file of files) {
const srcPath = path.join(extractedNodeDir, file);
const destPath = path.join(targetDir, file);
await fs.move(srcPath, destPath, { overwrite: true });
}
},
},
bun: {
defaultVersion: DEFAULT_VERSIONS.bun,
getDownloadUrl: (version, platform, arch) => {
const platformId = getBunPlatformIdentifier(platform, arch);
const fileName = `bun-${platformId}.zip`;
return `https://github.com/oven-sh/bun/releases/download/bun-${version}/${fileName}`;
},
getFileExtension: (platform, arch) => "zip",
getExecutablePath: (targetDir, platform) => path.join(targetDir, platform === "win32" ? "bun.exe" : "bun"),
extractFiles: async (extractedDir, targetDir, version, platform, arch) => {
const platformId = getBunPlatformIdentifier(platform, arch);
const bunDirName = `bun-${platformId}`;
const extractedBunDir = path.join(extractedDir, bunDirName);
// Check if bun directory exists, if not, look for the executable directly
let bunExecutable;
if (await fs.pathExists(extractedBunDir)) {
bunExecutable = path.join(extractedBunDir, platform === "win32" ? "bun.exe" : "bun");
}
else {
// Look for bun executable in the root of extracted directory
const files = await fs.readdir(extractedDir);
const execName = platform === "win32" ? "bun.exe" : "bun";
const foundExec = files.find((f) => f === execName);
if (foundExec) {
bunExecutable = path.join(extractedDir, foundExec);
}
else {
throw new Error(`Could not find bun executable in extracted files`);
}
}
const destPath = path.join(targetDir, platform === "win32" ? "bun.exe" : "bun");
await fs.move(bunExecutable, destPath, { overwrite: true });
},
},
uv: {
defaultVersion: DEFAULT_VERSIONS.uv,
getDownloadUrl: (version, platform, arch) => {
const platformId = getUvPlatformIdentifier(platform, arch);
const fileExtension = platform === "win32" ? "zip" : "tar.gz";
const fileName = `uv-${platformId}.${fileExtension}`;
return `https://github.com/astral-sh/uv/releases/download/${version}/${fileName}`;
},
getFileExtension: (platform, arch) => platform === "win32" ? "zip" : "tar.gz",
getExecutablePath: (targetDir, platform) => path.join(targetDir, platform === "win32" ? "uv.exe" : "uv"),
extractFiles: async (extractedDir, targetDir, version, platform, arch) => {
// uv archive contains executables in root or in a subdirectory
const files = await fs.readdir(extractedDir);
const execExtension = platform === "win32" ? ".exe" : "";
const uvExec = `uv${execExtension}`;
const uvxExec = `uvx${execExtension}`;
// Look for executables
const uvPath = files.includes(uvExec)
? path.join(extractedDir, uvExec)
: path.join(extractedDir, files[0], uvExec); // might be in subdirectory
const uvxPath = files.includes(uvxExec)
? path.join(extractedDir, uvxExec)
: path.join(extractedDir, files[0], uvxExec); // might be in subdirectory
// Copy both uv and uvx executables
if (await fs.pathExists(uvPath)) {
await fs.move(uvPath, path.join(targetDir, uvExec), {
overwrite: true,
});
}
if (await fs.pathExists(uvxPath)) {
await fs.move(uvxPath, path.join(targetDir, uvxExec), {
overwrite: true,
});
}
},
},
ripgrep: {
defaultVersion: DEFAULT_VERSIONS.ripgrep,
getDownloadUrl: (version, platform, arch) => {
const platformKey = `${arch}-${platform}`;
const platformConfig = RIPGREP_PLATFORM[platformKey];
if (!platformConfig) {
throw new Error(`Unsupported platform for ripgrep: ${platform}-${arch}`);
}
const fileName = `ripgrep-${version}-${platformConfig.target}.${platformConfig.ext}`;
return `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${fileName}`;
},
getFileExtension: (platform, arch) => {
const platformKey = `${arch}-${platform}`;
const platformConfig = RIPGREP_PLATFORM[platformKey];
if (!platformConfig) {
throw new Error(`Unsupported platform for ripgrep: ${platform}-${arch}`);
}
return platformConfig.ext;
},
getExecutablePath: (targetDir, platform) => path.join(targetDir, platform === "win32" ? "rg.exe" : "rg"),
extractFiles: async (extractedDir, targetDir, version, platform, arch) => {
const execName = platform === "win32" ? "rg.exe" : "rg";
const files = await fs.readdir(extractedDir);
// Find rg executable - it could be in the root or in a subdirectory
let rgPath;
// First, try to find it in the root
if (files.includes(execName)) {
rgPath = path.join(extractedDir, execName);
}
else {
// Search in subdirectories
for (const file of files) {
const filePath = path.join(extractedDir, file, execName);
if (await fs.pathExists(filePath)) {
rgPath = filePath;
break;
}
}
}
if (!rgPath) {
throw new Error(`Could not find ${execName} in extracted files`);
}
const destPath = path.join(targetDir, execName);
await fs.move(rgPath, destPath, { overwrite: true });
},
},
python: {
defaultVersion: DEFAULT_VERSIONS.python,
getDownloadUrl: (version, platform, arch) => {
const platformTarget = getPythonPlatformIdentifier(platform, arch);
const releaseDate = version.includes("+") ? version.split("+")[1] : "20250117";
const pythonVersion = version.includes("+") ? version.split("+")[0] : version;
const fileName = `cpython-${pythonVersion}+${releaseDate}-${platformTarget}-install_only.tar.gz`;
return `https://github.com/astral-sh/python-build-standalone/releases/download/${releaseDate}/${fileName}`;
},
getFileExtension: (platform, arch) => "tar.gz",
getExecutablePath: (targetDir, platform) => path.join(targetDir, "bin", platform === "win32" ? "python.exe" : "python3"),
extractFiles: async (extractedDir, targetDir, version, platform, arch) => {
const files = await fs.readdir(extractedDir);
// Python standalone tarball extracts to a python directory
const pythonDir = files.find((f) => f === "python");
if (!pythonDir) {
throw new Error(`Could not find python directory in extracted files`);
}
const extractedPythonDir = path.join(extractedDir, pythonDir);
// Move all contents from python directory to target directory
const pythonFiles = await fs.readdir(extractedPythonDir);
for (const file of pythonFiles) {
const srcPath = path.join(extractedPythonDir, file);
const destPath = path.join(targetDir, file);
await fs.move(srcPath, destPath, { overwrite: true });
}
},
},
rtk: {
defaultVersion: DEFAULT_VERSIONS.rtk,
getDownloadUrl: (version, platform, arch) => {
const fileName = getRtkAssetName(platform, arch);
return `https://github.com/rtk-ai/rtk/releases/download/${normalizeRtkVersion(version)}/${fileName}`;
},
getFileExtension: (platform, arch) => getRtkPlatformConfig(platform, arch).ext,
getExecutablePath: (targetDir, platform) => path.join(targetDir, platform === "win32" ? "rtk.exe" : "rtk"),
extractFiles: async (extractedDir, targetDir, version, platform, arch) => {
const execName = platform === "win32" ? "rtk.exe" : "rtk";
const executablePath = await findExecutableInExtractedDir(extractedDir, execName);
await fs.move(executablePath, path.join(targetDir, execName), {
overwrite: true,
});
},
},
};
function getNodePlatformIdentifier(platform, arch) {
const archStr = String(arch);
if (platform === "darwin") {
return archStr === "arm64" ? "darwin-arm64" : "darwin-x64";
}
else if (platform === "linux") {
if (archStr === "arm64")
return "linux-arm64";
if (archStr === "arm" || archStr === "armv7l")
return "linux-armv7l";
if (archStr === "ppc64" || archStr === "ppc64le")
return "linux-ppc64le";
if (archStr === "s390" || archStr === "s390x")
return "linux-s390x";
return "linux-x64";
}
else if (platform === "win32") {
if (archStr === "arm64")
return "win-arm64";
if (archStr === "ia32" || archStr === "x86")
return "win-x86";
return "win-x64";
}
throw new Error(`Unsupported platform: ${platform}-${archStr}`);
}
function getBunPlatformIdentifier(platform, arch) {
// Bun uses platform-arch naming
if (platform === "darwin") {
return arch === "arm64" ? "darwin-aarch64" : "darwin-x64";
}
else if (platform === "linux") {
return arch === "arm64" ? "linux-aarch64" : "linux-x64";
}
else if (platform === "win32") {
return arch === "arm64" ? "windows-aarch64" : "windows-x64";
}
throw new Error(`Unsupported platform for Bun: ${platform}-${arch}`);
}
function getUvPlatformIdentifier(platform, arch) {
if (platform === "darwin") {
return arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin";
}
else if (platform === "linux") {
// Standard Linux builds
if (arch === "arm64")
return "aarch64-unknown-linux-gnu";
if (arch === "x64" || arch === "x86_64")
return "x86_64-unknown-linux-gnu";
if (arch === "x86" || arch === "ia32")
return "i686-unknown-linux-gnu";
if (arch === "arm" || arch === "armv7l")
return "armv7-unknown-linux-gnueabihf";
if (arch === "ppc64")
return "powerpc64-unknown-linux-gnu";
if (arch === "ppc64le")
return "powerpc64le-unknown-linux-gnu";
if (arch === "s390x")
return "s390x-unknown-linux-gnu";
if (arch === "riscv64")
return "riscv64gc-unknown-linux-gnu";
// MUSL builds
if (arch === "arm64-musl")
return "aarch64-unknown-linux-musl";
if (arch === "x64-musl" || arch === "x86_64-musl")
return "x86_64-unknown-linux-musl";
if (arch === "x86-musl" || arch === "ia32-musl")
return "i686-unknown-linux-musl";
if (arch === "arm-musl")
return "arm-unknown-linux-musleabihf";
if (arch === "armv7-musl")
return "armv7-unknown-linux-musleabihf";
// Default to standard x64 GNU build for unknown architectures
return "x86_64-unknown-linux-gnu";
}
else if (platform === "win32") {
if (arch === "arm64")
return "aarch64-pc-windows-msvc";
if (arch === "x86" || arch === "ia32")
return "i686-pc-windows-msvc";
return "x86_64-pc-windows-msvc";
}
throw new Error(`Unsupported platform for uv: ${platform}-${arch}`);
}
function getRipgrepPlatformIdentifier(platform, arch) {
const platformKey = `${arch}-${platform}`;
if (platformKey in RIPGREP_PLATFORM) {
return RIPGREP_PLATFORM[platformKey].target;
}
throw new Error(`Unsupported platform for ripgrep: ${platform}-${arch}`);
}
function getPythonPlatformIdentifier(platform, arch) {
const archStr = String(arch);
if (platform === "darwin") {
return archStr === "arm64"
? "aarch64-apple-darwin"
: "x86_64-apple-darwin";
}
else if (platform === "linux") {
// Linux only supports x64 and arm64 (as per requirements)
if (archStr === "arm64")
return "aarch64-unknown-linux-gnu";
if (archStr === "x64" || archStr === "x86_64")
return "x86_64-unknown-linux-gnu";
// Other Linux architectures are not supported as per requirements
throw new Error(`Unsupported platform for Python: ${platform}-${archStr}. Only x64 and arm64 are supported for Linux.`);
}
else if (platform === "win32") {
// Windows only supports x64 (as per requirements)
if (archStr === "x64" || archStr === "x86_64")
return "x86_64-pc-windows-msvc";
if (archStr === "arm64")
return "aarch64-pc-windows-msvc";
// Windows x86 is not supported by python-build-standalone
throw new Error(`Unsupported platform for Python: ${platform}-${archStr}. Only x64 and arm64 are supported for Windows.`);
}
throw new Error(`Unsupported platform for Python: ${platform}-${archStr}`);
}
function getRtkPlatformConfig(platform, arch) {
const archStr = String(arch);
const platformKey = `${archStr}-${platform}`;
const platformConfig = RTK_PLATFORM[platformKey];
if (platformConfig) {
return platformConfig;
}
throw new Error(`Unsupported platform for rtk: ${platform}-${archStr}. Supported targets: ${RTK_SUPPORTED_PLATFORMS}.`);
}
function getRtkAssetName(platform, arch) {
const platformConfig = getRtkPlatformConfig(platform, arch);
return `rtk-${platformConfig.target}.${platformConfig.ext}`;
}
function normalizeRtkVersion(version) {
if (!version || version === "latest") {
return "latest";
}
return version.startsWith("v") ? version : `v${version}`;
}
function stripVersionPrefix(version) {
return version.startsWith("v") ? version.slice(1) : version;
}
async function findExecutableInExtractedDir(extractedDir, execName) {
const files = await fs.readdir(extractedDir);
if (files.includes(execName)) {
return path.join(extractedDir, execName);
}
for (const file of files) {
const execPath = path.join(extractedDir, file, execName);
if (await fs.pathExists(execPath)) {
return execPath;
}
}
throw new Error(`Could not find ${execName} in extracted files`);
}
const PROXY_ENV_KEYS = {
httpProxy: ["HTTP_PROXY", "http_proxy"],
httpsProxy: ["HTTPS_PROXY", "https_proxy"],
noProxy: ["NO_PROXY", "no_proxy"],
};
const GITHUB_TOKEN_ENV_KEYS = ["GITHUB_TOKEN", "GH_TOKEN"];
function getEnvValue(keys) {
for (const key of keys) {
const value = process.env[key];
if (value) {
return value;
}
}
return undefined;
}
function getTrimmedEnvValue(keys) {
for (const key of keys) {
const value = process.env[key];
if (!value) {
continue;
}
const trimmedValue = value.trim();
if (trimmedValue) {
return trimmedValue;
}
}
return undefined;
}
function getGitHubToken() {
return getTrimmedEnvValue(GITHUB_TOKEN_ENV_KEYS);
}
function getResponseMessage(data) {
if (!data || typeof data !== "object") {
return undefined;
}
const message = data.message;
return typeof message === "string" ? message : undefined;
}
function createRtkReleaseLookupError(error, hasGitHubToken) {
if (axios.isAxiosError(error)) {
const responseMessage = getResponseMessage(error.response?.data) ?? error.message;
const hint = hasGitHubToken
? "Check GITHUB_TOKEN or GH_TOKEN permissions/value, or pin a specific rtk version with --runtime-version to skip the latest release lookup."
: "Set GITHUB_TOKEN or GH_TOKEN in CI, or pin a specific rtk version with --runtime-version to skip the latest release lookup.";
if (error.response?.status === 401) {
return new Error(`Failed to authenticate with GitHub while resolving the latest rtk release. ${hint} GitHub API response: ${responseMessage}`);
}
if (error.response?.status === 403) {
const isRateLimitError = responseMessage.toLowerCase().includes("rate limit");
const prefix = !hasGitHubToken && isRateLimitError
? "GitHub API rate limit exceeded while resolving the latest rtk release."
: "GitHub API denied access while resolving the latest rtk release.";
return new Error(`${prefix} ${hint} GitHub API response: ${responseMessage}`);
}
return new Error(`Failed to resolve the latest rtk release from GitHub. GitHub API response: ${responseMessage}`);
}
const fallbackMessage = error instanceof Error ? error.message : String(error);
return new Error(`Failed to resolve the latest rtk release from GitHub. ${fallbackMessage}`);
}
function normalizeProxyUrl(proxyUrl, fallbackProtocol) {
if (/^[a-z]+:\/\//i.test(proxyUrl)) {
return proxyUrl;
}
const protocol = fallbackProtocol.endsWith(":")
? fallbackProtocol
: `${fallbackProtocol}:`;
return `${protocol}//${proxyUrl}`;
}
function getDefaultPort(protocol) {
return protocol === "https:" ? 443 : 80;
}
function parseNoProxyEntry(entry) {
let value = entry.trim();
if (!value) {
return { host: "" };
}
if (value.includes("://")) {
try {
value = new URL(value).host;
}
catch {
value = value.split("://")[1] || value;
}
}
value = value.split("/")[0];
if (value.startsWith("[") && value.includes("]")) {
const endIndex = value.indexOf("]");
const host = value.slice(1, endIndex);
const portValue = value.slice(endIndex + 1);
if (portValue.startsWith(":")) {
return { host, port: portValue.slice(1) };
}
return { host };
}
const lastColon = value.lastIndexOf(":");
if (lastColon > -1 && value.indexOf(":") === lastColon) {
return {
host: value.slice(0, lastColon),
port: value.slice(lastColon + 1),
};
}
return { host: value };
}
function isNoProxyMatch(targetUrl, noProxy) {
const entries = noProxy
.split(/[,\s]+/)
.map((entry) => entry.trim())
.filter(Boolean);
if (entries.length === 0) {
return false;
}
if (entries.includes("*")) {
return true;
}
const hostname = targetUrl.hostname.toLowerCase();
const port = targetUrl.port || String(getDefaultPort(targetUrl.protocol));
for (const entry of entries) {
if (entry === "*") {
return true;
}
const { host, port: entryPort } = parseNoProxyEntry(entry);
if (!host) {
continue;
}
if (entryPort && entryPort !== port) {
continue;
}
const normalizedHost = host.toLowerCase();
if (!normalizedHost) {
continue;
}
if (!/^[.*]/.test(normalizedHost)) {
if (hostname === normalizedHost) {
return true;
}
continue;
}
const matchHost = normalizedHost.startsWith("*")
? normalizedHost.slice(1)
: normalizedHost;
if (hostname.endsWith(matchHost)) {
return true;
}
}
return false;
}
function buildProxyConfig(proxyUrl, fallbackProtocol) {
const normalizedUrl = normalizeProxyUrl(proxyUrl, fallbackProtocol);
let parsed;
try {
parsed = new URL(normalizedUrl);
}
catch {
throw new Error(`Invalid proxy URL: ${proxyUrl}`);
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new Error(`Unsupported proxy protocol: ${parsed.protocol}`);
}
const port = parsed.port ? Number(parsed.port) : getDefaultPort(parsed.protocol);
if (Number.isNaN(port)) {
throw new Error(`Invalid proxy port in URL: ${proxyUrl}`);
}
const proxyConfig = {
protocol: parsed.protocol,
host: parsed.hostname,
port,
};
if (parsed.username || parsed.password) {
proxyConfig.auth = {
username: parsed.username,
password: parsed.password,
};
}
return proxyConfig;
}
export class RuntimeInjector {
constructor(options) {
const runtimeType = options.type || "node";
this.config = RUNTIME_CONFIGS[runtimeType];
this.options = {
type: runtimeType,
version: options.version || this.config.defaultVersion,
platform: options.platform || process.platform,
arch: options.arch || process.arch,
targetDir: options.targetDir,
cleanup: options.cleanup ?? true,
httpProxy: options.httpProxy,
httpsProxy: options.httpsProxy,
noProxy: options.noProxy,
};
this.runtimeInfo = {
type: this.options.type,
version: this.options.version,
platform: this.options.platform,
arch: this.options.arch,
targetDir: this.options.targetDir,
executablePath: this.config.getExecutablePath(this.options.targetDir, this.options.platform),
};
}
updateResolvedVersion(version) {
this.options.version = version;
this.runtimeInfo.version = version;
}
async fetchLatestRtkRelease() {
const releaseUrl = "https://api.github.com/repos/rtk-ai/rtk/releases/latest";
const proxyConfig = this.getProxyConfigForUrl(releaseUrl);
const githubToken = getGitHubToken();
const headers = {
Accept: "application/vnd.github+json",
"User-Agent": "tiny-runtime-injector",
};
if (githubToken) {
headers.Authorization = `Bearer ${githubToken}`;
}
try {
const response = await axios.get(releaseUrl, {
headers,
proxy: proxyConfig ?? false,
});
return {
tagName: normalizeRtkVersion(response.data.tag_name),
assets: (response.data.assets ?? []).map((asset) => asset.name),
};
}
catch (error) {
throw createRtkReleaseLookupError(error, Boolean(githubToken));
}
}
async resolveRtkVersion() {
if (this.runtimeInfo.type !== "rtk") {
return;
}
const normalizedVersion = normalizeRtkVersion(this.options.version);
if (normalizedVersion !== "latest") {
this.updateResolvedVersion(normalizedVersion);
return;
}
const expectedAsset = getRtkAssetName(this.runtimeInfo.platform, this.runtimeInfo.arch);
console.log("Resolving latest rtk release...");
const release = await this.fetchLatestRtkRelease();
if (!release.assets.includes(expectedAsset)) {
throw new Error(`Latest rtk release ${release.tagName} does not include asset ${expectedAsset} for ${this.runtimeInfo.platform}-${this.runtimeInfo.arch}`);
}
this.updateResolvedVersion(release.tagName);
console.log(`Resolved latest rtk version: ${release.tagName}`);
}
async isAlreadyInstalled() {
try {
const markerFile = path.join(this.runtimeInfo.targetDir, `${this.runtimeInfo.type}_${this.runtimeInfo.platform}_${this.runtimeInfo.arch}`);
if (await fs.pathExists(markerFile)) {
const installedVersion = await fs.readFile(markerFile, "utf8");
if (installedVersion.trim() === this.runtimeInfo.version) {
if (await fs.pathExists(this.runtimeInfo.executablePath)) {
try {
const execPath = this.runtimeInfo.executablePath.replace(/(\s+)/g, "\\$1");
let testCommand;
switch (this.runtimeInfo.type) {
case "node":
testCommand = `"${execPath}" -v`;
break;
case "bun":
testCommand = `"${execPath}" --version`;
break;
case "uv":
testCommand = `"${execPath}" --version`;
break;
case "ripgrep":
testCommand = `"${execPath}" --version`;
break;
case "python":
testCommand = `"${execPath}" --version`;
break;
case "rtk":
testCommand = `"${execPath}" --version`;
break;
default:
throw new Error(`Unknown runtime type: ${this.runtimeInfo.type}`);
}
const { stdout } = await execAsync(testCommand);
const actualVersion = stdout.trim();
// For different runtimes, version formats may vary
if (this.runtimeInfo.type === "node") {
return actualVersion === this.runtimeInfo.version;
}
else if (this.runtimeInfo.type === "bun") {
return actualVersion.includes(this.runtimeInfo.version.replace("v", ""));
}
else if (this.runtimeInfo.type === "uv") {
return actualVersion.includes(this.runtimeInfo.version);
}
else if (this.runtimeInfo.type === "ripgrep") {
return actualVersion.includes(this.runtimeInfo.version);
}
else if (this.runtimeInfo.type === "python") {
const pythonVersion = this.runtimeInfo.version.includes("+")
? this.runtimeInfo.version.split("+")[0]
: this.runtimeInfo.version;
return actualVersion.includes(pythonVersion);
}
else if (this.runtimeInfo.type === "rtk") {
return actualVersion.includes(stripVersionPrefix(this.runtimeInfo.version));
}
return true;
}
catch (err) {
if (err instanceof Error) {
console.log(`${this.runtimeInfo.type} execution test failed, will redownload: ${err.message}`);
}
else {
console.log(`${this.runtimeInfo.type} execution test failed, will redownload`);
}
}
}
}
}
return false;
}
catch (error) {
console.error(`Failed to check ${this.runtimeInfo.type} installation:`, error);
return false;
}
}
async downloadFile(url, destination) {
console.log(`Downloading: ${url}`);
await fs.ensureDir(path.dirname(destination));
const writer = createWriteStream(destination);
const proxyConfig = this.getProxyConfigForUrl(url);
const response = await axios({
method: "GET",
url: url,
responseType: "stream",
proxy: proxyConfig ?? false,
});
await pipeline(response.data, writer);
console.log(`File downloaded to: ${destination}`);
}
getProxyConfigForUrl(url) {
const parsedUrl = new URL(url);
const noProxy = this.getProxyOption("noProxy");
if (noProxy && isNoProxyMatch(parsedUrl, noProxy)) {
return undefined;
}
const proxyUrl = parsedUrl.protocol === "https:"
? this.getProxyOption("httpsProxy")
: this.getProxyOption("httpProxy");
if (!proxyUrl) {
return undefined;
}
return buildProxyConfig(proxyUrl, parsedUrl.protocol);
}
getProxyOption(key) {
const directValue = this.options[key];
if (directValue) {
return directValue;
}
return getEnvValue(PROXY_ENV_KEYS[key]);
}
async extractTarGz(filePath, destination) {
console.log(`Extracting: ${filePath}`);
await fs.ensureDir(destination);
await tar.extract({
file: filePath,
cwd: destination,
});
console.log(`Files extracted to: ${destination}`);
}
async extractZip(filePath, destination) {
console.log(`Extracting: ${filePath}`);
await fs.ensureDir(destination);
try {
if (process.platform === "win32") {
await execAsync(`powershell -command "Expand-Archive -Path '${filePath}' -DestinationPath '${destination}' -Force"`);
}
else {
await execAsync(`unzip -o "${filePath}" -d "${destination}"`);
}
console.log(`ZIP file extracted to: ${destination}`);
}
catch (error) {
console.error("Extraction failed:", error);
throw error;
}
}
async cleanupRuntime(cleanupConfig) {
const targetDir = this.runtimeInfo.targetDir;
if (this.runtimeInfo.platform === "darwin" ||
this.runtimeInfo.platform === "linux") {
console.log("Cleaning up for macOS/Linux: Removing share and include directories.");
const shareDir = path.join(targetDir, "share");
const includeDir = path.join(targetDir, "include");
try {
if (await fs.pathExists(shareDir)) {
await fs.remove(shareDir);
console.log(`Removed directory: ${shareDir}`);
}
if (await fs.pathExists(includeDir)) {
await fs.remove(includeDir);
console.log(`Removed directory: ${includeDir}`);
}
}
catch (error) {
console.error(`Error during directory removal: ${error}`);
}
}
else {
const { removeDocs = true, removeDevFiles = true, removeSourceMaps = true, customRules = [], } = cleanupConfig;
const patterns = [
...(removeDocs
? [
"**/*.md",
"**/docs/**",
"**/doc/**",
"share/doc/**",
"share/man/**",
]
: []),
...(removeDevFiles
? ["**/*.h", "**/*.cc", "**/*.cpp", "**/*.c", "include/**"]
: []),
...(removeSourceMaps ? ["**/*.map"] : []),
...customRules.map((rule) => rule.pattern),
];
for (const pattern of patterns) {
console.log(`Cleaning up based on pattern: ${pattern}`);
try {
const files = await glob(pattern, {
cwd: targetDir,
absolute: true,
nodir: true,
});
console.log(`Found ${files.length} files/items for pattern ${pattern}`);
for (const file of files) {
if (await fs.pathExists(file)) {
await fs.remove(file);
}
}
}
catch (error) {
console.error(`Error cleaning up pattern ${pattern}: ${error}`);
}
}
}
}
async inject() {
try {
await this.resolveRtkVersion();
console.log(`Checking ${this.runtimeInfo.type} ${this.runtimeInfo.version} for ${this.runtimeInfo.platform}-${this.runtimeInfo.arch}`);
await fs.ensureDir(this.runtimeInfo.targetDir);
if (await this.isAlreadyInstalled()) {
console.log(`${this.runtimeInfo.type} already installed, skipping download`);
return;
}
const downloadUrl = this.config.getDownloadUrl(this.runtimeInfo.version, this.runtimeInfo.platform, this.runtimeInfo.arch);
const fileExtension = this.config.getFileExtension(this.runtimeInfo.platform, this.runtimeInfo.arch);
const fileName = `${this.runtimeInfo.type}-${this.runtimeInfo.version}.${fileExtension}`;
const tempDir = path.join(os.tmpdir(), "tiny-runtime-injector-temp");
const downloadPath = path.join(tempDir, fileName);
await fs.ensureDir(tempDir);
await this.downloadFile(downloadUrl, downloadPath);
await fs.emptyDir(this.runtimeInfo.targetDir);
const extractedDir = path.join(tempDir, "extracted");
await fs.ensureDir(extractedDir);
if (fileExtension === "zip") {
await this.extractZip(downloadPath, extractedDir);
}
else {
await this.extractTarGz(downloadPath, extractedDir);
}
// Use runtime-specific extraction logic
if (this.config.extractFiles) {
await this.config.extractFiles(extractedDir, this.runtimeInfo.targetDir, this.runtimeInfo.version, this.runtimeInfo.platform, this.runtimeInfo.arch);
}
// Set executable permissions for non-Windows platforms
if (this.runtimeInfo.platform !== "win32") {
const execPath = this.runtimeInfo.executablePath;
if (await fs.pathExists(execPath)) {
await fs.chmod(execPath, 0o755);
}
// For uv, also set permissions for uvx
if (this.runtimeInfo.type === "uv") {
const uvxPath = path.join(this.runtimeInfo.targetDir, "uvx");
if (await fs.pathExists(uvxPath)) {
await fs.chmod(uvxPath, 0o755);
}
}
// For Node.js, set permissions for all binaries
if (this.runtimeInfo.type === "node") {
const binDir = path.join(this.runtimeInfo.targetDir, "bin");
if (await fs.pathExists(binDir)) {
const binFiles = await fs.readdir(binDir);
for (const file of binFiles) {
const filePath = path.join(binDir, file);
const stats = await fs.stat(filePath);
if (stats.isFile()) {
await fs.chmod(filePath, 0o755);
}
}
}
}
// For Python, set permissions for all files in bin directory
if (this.runtimeInfo.type === "python") {
const binDir = path.join(this.runtimeInfo.targetDir, "bin");
if (await fs.pathExists(binDir)) {
const binFiles = await fs.readdir(binDir);
for (const file of binFiles) {
const filePath = path.join(binDir, file);
const stats = await fs.stat(filePath);
if (stats.isFile()) {
await fs.chmod(filePath, 0o755);
}
}
}
}
}
const markerFile = path.join(this.runtimeInfo.targetDir, `${this.runtimeInfo.type}_${this.runtimeInfo.platform}_${this.runtimeInfo.arch}`);
await fs.writeFile(markerFile, this.runtimeInfo.version);
console.log(this.options.cleanup);
if (this.options.cleanup && this.runtimeInfo.type === "node") {
const cleanupConfig = typeof this.options.cleanup === "boolean"
? { removeDocs: true, removeDevFiles: true, removeSourceMaps: true }
: this.options.cleanup;
await this.cleanupRuntime(cleanupConfig);
}
await fs.remove(tempDir);
console.log(`${this.runtimeInfo.type} ${this.runtimeInfo.version} successfully installed to ${this.runtimeInfo.targetDir}`);
}
catch (error) {
console.error("Installation failed:", error);
throw error;
}
}
}