gerbil/src/main/modules/dependencies.ts

255 lines
6.1 KiB
TypeScript

import { access, readdir } from 'fs/promises';
import { homedir, release } from 'os';
import { join } from 'path';
import { platform, env as processEnv, versions, arch } from 'process';
import { execa } from 'execa';
import { app } from 'electron';
import { PRODUCT_NAME } from '@/constants';
async function executeCommand(
command: string,
args: string[],
env: Record<string, string | undefined>,
timeout = 5000
) {
try {
const { stdout } = await execa(command, args, {
env,
timeout,
reject: false,
});
return { success: true, output: stdout.trim() };
} catch {
return { success: false };
}
}
export async function getUvVersion() {
const env = await getUvEnvironment();
const result = await executeCommand('uv', ['--version'], env);
if (result.success && result.output) {
const version = result.output.match(/uv\s+(\d+\.\d+\.\d+)/i);
return version ? version[1] : result.output;
}
return null;
}
export async function getSystemNodeVersion() {
const env = await getNodeEnvironment();
const result = await executeCommand('node', ['--version'], env);
if (result.success && result.output) {
return result.output.replace(/^v/, '') || null;
}
return null;
}
export async function isUvAvailable() {
const env = await getUvEnvironment();
const result = await executeCommand('uv', ['--version'], env, 10000);
return result.success;
}
export async function getUvEnvironment() {
const env = { ...processEnv };
const uvPaths = [
join(homedir(), '.cargo', 'bin'),
join(homedir(), '.local', 'bin'),
];
const existingPaths: string[] = [];
for (const path of uvPaths) {
try {
await access(path);
existingPaths.push(path);
} catch {}
}
if (existingPaths.length > 0) {
const pathSeparator = platform === 'win32' ? ';' : ':';
env.PATH = `${existingPaths.join(pathSeparator)}${pathSeparator}${env.PATH}`;
}
if (platform === 'win32') {
env.PYTHONIOENCODING = 'utf-8';
env.PYTHONLEGACYWINDOWSSTDIO = '1';
env.PYTHONUTF8 = '1';
env.CHCP = '65001';
}
return env;
}
export async function isNpxAvailable() {
const env = await getNodeEnvironment();
const result = await executeCommand('npx', ['--version'], env);
return result.success;
}
export async function getNodeEnvironment() {
const env = { ...processEnv };
if (platform === 'win32') {
return env;
}
const versionManagerPaths = [
join(homedir(), '.local', 'share', 'fnm', 'node-versions'),
join(homedir(), '.nvm', 'versions', 'node'),
join(homedir(), '.volta', 'tools', 'image', 'node'),
join(homedir(), '.asdf', 'installs', 'nodejs'),
];
const systemPaths: string[] = [];
if (platform === 'darwin') {
systemPaths.push('/opt/homebrew/bin', '/usr/local/bin');
}
for (const systemPath of systemPaths) {
try {
await access(systemPath);
await tryAddPathToEnv(env, systemPath);
return env;
} catch {
continue;
}
}
for (const versionPath of versionManagerPaths) {
if (await tryVersionManagerPath(versionPath, env)) {
return env;
}
}
return env;
}
async function tryVersionManagerPath(
basePath: string,
env: Record<string, string | undefined>
) {
try {
await access(basePath);
const versions = await readdir(basePath);
if (versions.length > 0) {
const latestVersion = versions.sort().pop();
if (latestVersion) {
const binSubPath = basePath.includes('fnm')
? join('installation', 'bin')
: 'bin';
const nodeBinPath = join(basePath, latestVersion, binSubPath);
try {
await access(nodeBinPath);
return tryAddPathToEnv(env, nodeBinPath);
} catch {
return false;
}
}
}
} catch {
return false;
}
return false;
}
let aurVersionCache: string | null = null;
export async function getAURVersion() {
if (platform !== 'linux') {
return null;
}
if (aurVersionCache !== null) {
return aurVersionCache;
}
const packageName = PRODUCT_NAME.toLowerCase();
try {
const { stdout } = await execa('pacman', ['-Q', packageName], {
timeout: 1000,
reject: false,
});
const trimmed = stdout.trim();
if (trimmed.length > 0) {
aurVersionCache = trimmed.replace(`${packageName} `, '');
return aurVersionCache;
}
} catch {}
aurVersionCache = null;
return null;
}
export async function isAURInstallation() {
const aurPackageVersion = await getAURVersion();
return aurPackageVersion !== null;
}
export async function getVersionInfo() {
const [nodeJsSystemVersion, uvVersion, aurPackageVersion] = await Promise.all(
[getSystemNodeVersion(), getUvVersion(), getAURVersion()]
);
return {
appVersion: app.getVersion(),
electronVersion: versions.electron,
nodeVersion: versions.node,
chromeVersion: versions.chrome,
v8Version: versions.v8,
osVersion: release(),
platform,
arch,
nodeJsSystemVersion,
uvVersion,
aurPackageVersion,
};
}
function tryAddPathToEnv(
env: Record<string, string | undefined>,
path: string
) {
const pathSeparator = platform === 'win32' ? ';' : ':';
if (!env.PATH?.includes(path)) {
env.PATH = `${path}${pathSeparator}${env.PATH}`;
return true;
}
return false;
}
let windowsPortableInstallationCache: boolean | null = null;
export function isWindowsPortableInstallation() {
if (platform !== 'win32') {
return false;
}
if (windowsPortableInstallationCache !== null) {
return windowsPortableInstallationCache;
}
try {
const execPath = app.getPath('exe');
const isInTemp =
execPath.toLowerCase().includes('\\temp\\') ||
execPath.toLowerCase().includes('\\tmp\\');
const isInProgramFiles = execPath.toLowerCase().includes('program files');
const isInAppDataPrograms = execPath
.toLowerCase()
.includes('appdata\\local\\programs');
windowsPortableInstallationCache =
isInTemp && !isInProgramFiles && !isInAppDataPrograms;
return windowsPortableInstallationCache;
} catch {
windowsPortableInstallationCache = false;
return false;
}
}