Reimplement Python package installation.

This commit is contained in:
bobo.yang 2023-08-21 11:52:00 +08:00
parent 0e8e1a6e2c
commit e6b48a9681
9 changed files with 561 additions and 0 deletions

View File

@ -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<string> {
// 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;
}

View File

@ -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<string> {
// 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<string> {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
resolve('');
} else {
resolve(stdout.trim());
}
});
});
}
function checkPathExists(path: string): Promise<boolean> {
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<string> {
// 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<void> {
return new Promise<void>((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<string> {
// 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;
}

View File

@ -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();
}

View File

@ -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<string> {
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<string>((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(''); // 下载失败时返回空字符串
});
});
});
}

View File

@ -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<string> {
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 '';
}
}

View File

@ -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<string> {
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 '';
}
}

View File

@ -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<boolean> {
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);
}
});
});
}

View File

@ -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<boolean> {
return new Promise<boolean>((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<string> {
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<string>((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`));
}
}
});
});
}

View File

@ -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.