diff --git a/src/util/python_installer/app_install.ts b/src/util/python_installer/app_install.ts new file mode 100644 index 0000000..11bff89 --- /dev/null +++ b/src/util/python_installer/app_install.ts @@ -0,0 +1,60 @@ +/* + Install devchat + */ + +import { logger } from "../logger"; +import { installConda } from "./conda_install"; +import { installPackage } from "./package_install"; +import { installPython } from "./python_install"; + +// step 1. install conda +// step 2. create env with python 3.11.4 +// step 3. install devchat in the env + +export async function appInstall(pkgName: string, pythonVersion: string) : Promise { + // install conda + logger.channel()?.info('Install conda ...') + const condaCommand = await installConda(); + if (!condaCommand) { + logger.channel()?.error('Install conda failed'); + logger.channel()?.show(); + return ''; + } + + // create env with specify python + logger.channel()?.info('Create env ...'); + let pythonCommand = ''; + // try 3 times + for (let i = 0; i < 3; i++) { + pythonCommand = await installPython(condaCommand, pkgName, pythonVersion); + if (pythonCommand) { + break; + } + logger.channel()?.info(`Create env failed, try again: ${i + 1}`); + } + if (!pythonCommand) { + logger.channel()?.error('Create env failed'); + logger.channel()?.show(); + return ''; + } + logger.channel()?.info(`Create env success: ${pythonCommand}`); + + // install devchat in the env + logger.channel()?.info('Install python packages ...') + let isInstalled = false; + // try 3 times + for (let i = 0; i < 3; i++) { + isInstalled = await installPackage(pythonCommand, pkgName); + if (isInstalled) { + break; + } + logger.channel()?.info(`Install packages failed, try again: ${i + 1}`); + } + if (!isInstalled) { + logger.channel()?.error('Install packages failed'); + logger.channel()?.show(); + return ''; + } + + return pythonCommand; +} \ No newline at end of file diff --git a/src/util/python_installer/conda_install.ts b/src/util/python_installer/conda_install.ts new file mode 100644 index 0000000..a3c0e3b --- /dev/null +++ b/src/util/python_installer/conda_install.ts @@ -0,0 +1,180 @@ +/* + Install conda command + Install file from https://repo.anaconda.com/miniconda/ + */ + +import { logger } from "../logger"; +import { getCondaDownloadUrl } from "./conda_url"; +import { downloadFile } from "./https_download"; + +import { exec, spawn } from 'child_process'; +const fs = require('fs'); +const path = require('path'); + + +// Check whether conda has installed before installing conda. +// If "conda -V" runs ok, then conda has installed. +// If ~/.devchat/conda/bin/conda exists, then conda has installed. + +// is "conda -V" ok? then find conda command +// is ~/.devchat/conda/bin/conda exists? then return ~/.devchat/conda/bin/conda +// find conda command by: with different os use diffenc command: which conda | where conda +async function isCondaInstalled(): Promise { + // whether conda -V runs ok + const condaVersion = await runCommand('conda2 -V'); + if (condaVersion) { + // find conda command by: with different os use diffenc command: which conda | where conda + const os = process.platform; + const command = os === 'win32' ? 'where conda' : 'which conda'; + const condaCommand = await runCommand(command); + if (condaCommand) { + const condaCommandLines = condaCommand.split('\n'); + return condaCommandLines[0].trim(); + } + } + + // whether ~/.devchat/conda/bin/conda exists + const os = process.platform; + const userHome = os === 'win32' ? fs.realpathSync(process.env.USERPROFILE || '') : process.env.HOME; + const pathToConda = `${userHome}/.devchat/conda`; + const condaPath = os === 'win32' ? `${pathToConda}/Scripts/conda.exe` : `${pathToConda}/bin/conda`; + logger.channel()?.info(`checking conda path: ${condaPath}`); + const isCondaPathExists = fs.existsSync(condaPath); + if (isCondaPathExists) { + return condaPath; + } + + logger.channel()?.info(`conda path: ${condaPath} not exists`); + return ''; +} + +function runCommand(command: string): Promise { + return new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + resolve(''); + } else { + resolve(stdout.trim()); + } + }); + }); +} + +function checkPathExists(path: string): Promise { + return new Promise((resolve, reject) => { + exec(`test -e ${path}`, (error, stdout, stderr) => { + if (error) { + resolve(false); + } else { + resolve(true); + } + }); + }); +} + +// install file is an exe file or sh file +// according to different os, use different command to install conda, Installing in silent mode +// install conda to USER_HOME/.devchat/conda +// return: conda command path +async function installCondaByInstallFile(installFileUrl: string) : Promise { + // Determine the operating system + const os = process.platform; + + // Set the installation directory for conda + const userHome = os === 'win32' ? fs.realpathSync(process.env.USERPROFILE || '') : process.env.HOME; + const pathToConda = `${userHome}/.devchat/conda`; + + // Define the command to install conda based on the operating system + let command = ''; + if (os === 'win32') { + const winPathToConda = pathToConda.replace(/\//g, '\\'); + command = `start /wait ${installFileUrl} /InstallationType=JustMe /AddToPath=0 /RegisterPython=0 /S /D=${winPathToConda}`; + } else if (os === 'linux') { + command = `bash ${installFileUrl} -b -p ${pathToConda}`; + } else if (os === 'darwin') { + command = `bash ${installFileUrl} -b -p ${pathToConda}`; + } else { + throw new Error('Unsupported operating system'); + } + + // Execute the command to install conda + logger.channel()?.info(`install conda command: ${command}`); + try { + await executeCommand(command); + + // Return the path to the conda command + let condaCommandPath = ''; + if (os === 'win32') { + condaCommandPath = `${pathToConda}\\Scripts\\conda.exe`; + } else { + condaCommandPath = `${pathToConda}/bin/conda`; + } + + return condaCommandPath; + } catch(error) { + logger.channel()?.error(`install conda failed: ${error}`); + logger.channel()?.show(); + return ''; + } +} + +// Helper function to execute a command +function executeCommand(command: string): Promise { + return new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + logger.channel()?.error(`exec error: ${error}`); + logger.channel()?.show(); + reject(error); + } else { + if (stderr) { + logger.channel()?.error(`stderr: ${error}`); + logger.channel()?.show(); + } + if (stdout) { + logger.channel()?.info(`${stdout}`); + } + resolve(); + } + }); + }); +} + +export async function installConda() : Promise { + // step 1. check whether conda has installed + // step 2. download install file + // step 3. install conda by install file + + const condaCommand = await isCondaInstalled(); + if (condaCommand) { + logger.channel()?.info(`conda has installed: ${condaCommand}`); + return condaCommand; + } + + const downloadInstallFile = getCondaDownloadUrl(); + if (!downloadInstallFile) { + logger.channel()?.error(`get conda download url failed`); + logger.channel()?.show(); + return ''; + } + + logger.channel()?.info(`conda download url: ${downloadInstallFile}`); + let installFileLocal = ''; + // try 3 times + for (let i = 0; i < 3; i++) { + installFileLocal = await downloadFile(downloadInstallFile); + if (installFileLocal) { + break; + } + logger.channel()?.info(`download conda install file failed, try again ...`); + } + if (!installFileLocal) { + logger.channel()?.error(`download conda install file failed`); + logger.channel()?.show(); + return ''; + } + + logger.channel()?.info(`conda install file: ${installFileLocal}`); + const installedConda = await installCondaByInstallFile(installFileLocal); + return installedConda; +} \ No newline at end of file diff --git a/src/util/python_installer/conda_url.ts b/src/util/python_installer/conda_url.ts new file mode 100644 index 0000000..be53383 --- /dev/null +++ b/src/util/python_installer/conda_url.ts @@ -0,0 +1,54 @@ +/* + Get conda download url + */ + + import os from 'os'; +import { logger } from '../logger'; + + function getDownloadFileName(): string { + const platform = os.platform(); + const arch = os.arch(); + logger.channel()?.info(`Platform: ${platform}, Arch: ${arch}`); + + if (platform === "win32") { + if (arch === "x64") { + return "Miniconda3-latest-Windows-x86_64.exe"; + } else if (arch === "ia32") { + return "Miniconda3-latest-Windows-x86.exe"; + } else { + return "Miniconda3-latest-Windows-x86_64.exe"; + } + } else if (platform === "darwin") { + if (arch === "x64") { + return "Miniconda3-latest-MacOSX-x86_64.sh"; + } else if (arch === "arm64") { + return "Miniconda3-latest-MacOSX-arm64.sh"; + } else if (arch === "x86") { + return "Miniconda3-latest-MacOSX-x86.sh"; + } else { + return "Miniconda3-latest-MacOSX-arm64.sh"; + } + } else if (platform === "linux") { + if (arch === "x64") { + return "Miniconda3-latest-Linux-x86_64.sh"; + } else if (arch === "s390x") { + return "Miniconda3-latest-Linux-s390x.sh"; + } else if (arch === "ppc64le") { + return "Miniconda3-latest-Linux-ppc64le.sh"; + } else if (arch === "aarch64") { + return "Miniconda3-latest-Linux-aarch64.sh"; + } else if (arch === "x86") { + return "Miniconda3-latest-Linux-x86.sh"; + } else if (arch === "armv7l") { + return "Miniconda3-latest-Linux-armv7l.sh"; + } else { + return "Miniconda3-latest-Linux-x86_64.sh"; + } + } + + return ""; + } + +export function getCondaDownloadUrl(): string { + return 'https://repo.anaconda.com/miniconda/' + getDownloadFileName(); +} \ No newline at end of file diff --git a/src/util/python_installer/https_download.ts b/src/util/python_installer/https_download.ts new file mode 100644 index 0000000..be86a54 --- /dev/null +++ b/src/util/python_installer/https_download.ts @@ -0,0 +1,47 @@ +import * as fs from 'fs'; +import * as https from 'https'; +import * as os from 'os'; +import * as path from 'path'; +import { logger } from '../logger'; + +// download url to tmp directory +// return: local file path or empty string +export async function downloadFile(url: string): Promise { + const os = process.platform; + const tempDir = os === 'win32' ? fs.realpathSync(process.env.USERPROFILE || '') : process.env.HOME; + + const fileName = path.basename(url); // 从 URL 中提取文件名称 + const destination = path.join(tempDir!, fileName); // 构建文件路径 + + const file = fs.createWriteStream(destination); + let downloadedBytes = 0; + let totalBytes = 0; + let lastProgress = 0; + + return new Promise((resolve, reject) => { + https.get(url, (response) => { + totalBytes = parseInt(response.headers['content-length'] || '0', 10); + + response.on('data', (chunk) => { + downloadedBytes += chunk.length; + const progress = (downloadedBytes / totalBytes) * 100; + + if (progress - lastProgress >= 3) { + logger.channel()?.info(`Downloaded ${downloadedBytes} bytes (${progress.toFixed(2)}%)`); + lastProgress = progress; + } + }); + + response.pipe(file); + + file.on('finish', () => { + file.close(); + resolve(destination); // 修改为传递下载的文件路径 + }); + }).on('error', (error) => { + fs.unlink(destination, () => { + resolve(''); // 下载失败时返回空字符串 + }); + }); + }); +} \ No newline at end of file diff --git a/src/util/python_installer/install_askcode.ts b/src/util/python_installer/install_askcode.ts new file mode 100644 index 0000000..4ab64bb --- /dev/null +++ b/src/util/python_installer/install_askcode.ts @@ -0,0 +1,29 @@ +/* + Install DevChat with python=3.11.4 + */ + + import { logger } from "../logger"; + import { appInstall } from "./app_install" + + + // python version: 3.11.4 + // pkg name: devchat + // return: path to devchat, devchat is located in the same directory as python + export async function installAskCode(): Promise { + try { + logger.channel()?.info(`start installing AskCode with python=3.11.4 ...`); + const pythonCommand = await appInstall('devchat-ask', '3.11.4'); + if (!pythonCommand) { + logger.channel()?.error(`failed to install devchat-ask with python=3.11.4`); + logger.channel()?.show(); + return ''; + } + + logger.channel()?.info(`installed devchat-ask with python=3.11.4 at ${pythonCommand}`); + return pythonCommand; + } catch (error) { + logger.channel()?.error(`${error}`); + logger.channel()?.show(); + return ''; + } + } \ No newline at end of file diff --git a/src/util/python_installer/install_devchat.ts b/src/util/python_installer/install_devchat.ts new file mode 100644 index 0000000..300356e --- /dev/null +++ b/src/util/python_installer/install_devchat.ts @@ -0,0 +1,55 @@ +/* + Install DevChat with python=3.11.4 + */ + +import { logger } from "../logger"; +import { appInstall } from "./app_install" + +import * as path from 'path'; +import * as fs from 'fs'; + + +let isDevChatInstalling: boolean | undefined = undefined; + +export function isDevchatInstalling(): boolean { + if (isDevChatInstalling === true) { + return true; + } + return false; +} + +// python version: 3.11.4 +// pkg name: devchat +// return: path to devchat, devchat is located in the same directory as python +export async function installDevchat(): Promise { + try { + logger.channel()?.info(`start installing devchat with python=3.11.4 ...`); + isDevChatInstalling = true; + const pythonCommand = await appInstall('devchat', '3.11.4'); + if (!pythonCommand) { + logger.channel()?.error(`failed to install devchat with python=3.11.4`); + logger.channel()?.show(); + isDevChatInstalling = false; + return ''; + } + + // Get the directory of pythonCommand + const pythonDirectory = path.dirname(pythonCommand); + + // Get the path of devchat + let devchatPath = path.join(pythonDirectory, 'devchat'); + + // Check if devchatPath exists, if not, try with 'Scripts' subdirectory + if (!fs.existsSync(devchatPath)) { + devchatPath = path.join(pythonDirectory, 'Scripts', 'devchat'); + } + + isDevChatInstalling = false; + return devchatPath; + } catch (error) { + logger.channel()?.error(`${error}`); + logger.channel()?.show(); + isDevChatInstalling = false; + return ''; + } +} \ No newline at end of file diff --git a/src/util/python_installer/package_install.ts b/src/util/python_installer/package_install.ts new file mode 100644 index 0000000..ca27435 --- /dev/null +++ b/src/util/python_installer/package_install.ts @@ -0,0 +1,41 @@ +/* + Install specific version of package. e.g. devchat + */ + + +import { spawn } from 'child_process'; +import { logger } from '../logger'; + +// install specific version of package +// pythonCommand -m install pkgName +// if install success, return true +// else return false +export async function installPackage(pythonCommand: string, pkgName: string) : Promise { + return new Promise((resolve, reject) => { + const cmd = pythonCommand; + const args = ['-m', 'pip', 'install', pkgName]; + const child = spawn(cmd, args); + + child.stdout.on('data', (data) => { + logger.channel()?.info(`${data}`); + }); + + child.stderr.on('data', (data) => { + console.error(`stderr: ${data}`); + }); + + child.on('error', (error) => { + logger.channel()?.error(`exec error: ${error}`); + logger.channel()?.show(); + resolve(false); + }); + + child.on('close', (code) => { + if (code !== 0) { + resolve(false); + } else { + resolve(true); + } + }); + }); +} \ No newline at end of file diff --git a/src/util/python_installer/python_install.ts b/src/util/python_installer/python_install.ts new file mode 100644 index 0000000..5d2360c --- /dev/null +++ b/src/util/python_installer/python_install.ts @@ -0,0 +1,83 @@ +import { exec, spawn } from 'child_process'; +import * as path from 'path'; +import * as os from 'os'; +import { logger } from '../logger'; +const fs = require('fs'); + +// Check if the environment already exists +export async function checkEnvExists(condaCommandPath: string, envName: string): Promise { + return new Promise((resolve, reject) => { + const condaCommand = path.resolve(condaCommandPath); + const command = `${condaCommand} env list`; + exec(command, (error, stdout, stderr) => { + if (error) { + logger.channel()?.error(`Error checking environments`); + logger.channel()?.show(); + reject(false); + } else { + const envs = stdout.split('\n').map(line => line.split(' ')[0]); + resolve(envs.includes(envName)); + } + }); + }); +} + +// Install env with specific python version +// conda create -n {envName} python={pythonVersion} --yes +// return: python in env path +export async function installPython(condaCommandPath: string, envName: string, pythonVersion: string): Promise { + const envExists = await checkEnvExists(condaCommandPath, envName); + + const condaCommand = path.resolve(condaCommandPath); + const envPath = path.resolve(condaCommand, '..', '..', 'envs', envName); + let pythonPath; + let pythonPath2; + if (os.platform() === 'win32') { + pythonPath = path.join(envPath, 'Scripts', 'python.exe'); + pythonPath2 = path.join(envPath, 'python.exe'); + } else { + pythonPath = path.join(envPath, 'bin', 'python'); + } + + if (envExists) { + if (fs.existsSync(pythonPath)) { + return pythonPath; + } else if (pythonPath2 && fs.existsSync(pythonPath2)) { + return pythonPath2; + } + } + + return new Promise((resolve, reject) => { + const cmd = condaCommand; + const args = ['create', '-n', envName, `python=${pythonVersion}`, '--yes']; + const child = spawn(cmd, args); + + child.stdout.on('data', (data) => { + logger.channel()?.info(`${data}`); + }); + + child.stderr.on('data', (data) => { + console.error(`stderr: ${data}`); + }); + + child.on('error', (error) => { + logger.channel()?.error(`Error installing python ${pythonVersion} in env ${envName}`); + logger.channel()?.show(); + reject(''); + }); + + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(`Command exited with code ${code}`)); + } else { + if (fs.existsSync(pythonPath)) { + resolve(pythonPath); + } else if (pythonPath2 && fs.existsSync(pythonPath2)) { + resolve(pythonPath2); + } else { + reject(new Error(`No Python found`)); + } + } + }); + }); +} \ No newline at end of file diff --git a/src/util/python_installer/readme.md b/src/util/python_installer/readme.md new file mode 100644 index 0000000..bf3dcfa --- /dev/null +++ b/src/util/python_installer/readme.md @@ -0,0 +1,12 @@ +# Why conda? +Devchat-vscode support custom extension. Different extension may need different python version. + +pyenv is also a python version manager, but it always download source of python, then build it to binary. + +conda is a package manager, it can download binary of python, and install packages. + +# Where to install? +Install conda to $USER_PROFILE/.devchat + +Python will install inside $USER_PROFILE/.devchat/conda. +