Download pip and avoid venv in embedded Python
This commit is contained in:
Родитель
6306ce5200
Коммит
2bfef9e406
|
@ -7,8 +7,8 @@ import * as yauzl from 'yauzl';
|
|||
|
||||
import { mkdir, winmlDataFoler } from '../persistence/appData';
|
||||
|
||||
const venv = path.join(winmlDataFoler, 'venv');
|
||||
export const localPython = path.join(winmlDataFoler, 'python/python');
|
||||
const venv = mkdir(winmlDataFoler, 'venv');
|
||||
export const localPython = path.join(venv, 'Scripts/python');
|
||||
|
||||
function filterPythonBinaries(binaries: string[]) {
|
||||
// Look for binaries with venv support
|
||||
|
@ -24,64 +24,106 @@ function filterPythonBinaries(binaries: string[]) {
|
|||
}
|
||||
|
||||
export function getPythonBinaries() {
|
||||
const binaries = filterPythonBinaries([localPython, 'python3', 'py', 'python']) as Array<string | null>;
|
||||
if (process.platform === 'win32' && binaries[0] !== localPython) {
|
||||
const binaries = filterPythonBinaries(['python3', 'py', 'python']) as Array<string | null>;
|
||||
if (process.platform === 'win32') {
|
||||
binaries.push(null); // use null to represent local, embedded version
|
||||
}
|
||||
return [...new Set(binaries)];
|
||||
}
|
||||
|
||||
function unzip(buffer: Buffer, directory: string, success: () => void, errorCallback: (err: Error) => void) {
|
||||
yauzl.fromBuffer(buffer, (err, zipfile) => {
|
||||
if (err) {
|
||||
errorCallback(err);
|
||||
return;
|
||||
}
|
||||
zipfile!.once('end', success);
|
||||
zipfile!.once('error', errorCallback);
|
||||
zipfile!.on('entry', entry => {
|
||||
zipfile!.openReadStream(entry, (error, readStream) => {
|
||||
if (error) {
|
||||
errorCallback(error);
|
||||
return;
|
||||
}
|
||||
readStream!.once('error', errorCallback);
|
||||
readStream!.pipe(fs.createWriteStream(path.join(directory, entry.fileName)));
|
||||
async function unzip(buffer: Buffer, directory: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
yauzl.fromBuffer(buffer, (err, zipfile) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
zipfile!.once('end', resolve);
|
||||
zipfile!.once('error', reject);
|
||||
zipfile!.on('entry', entry => {
|
||||
zipfile!.openReadStream(entry, (error, readStream) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
readStream!.once('error', reject);
|
||||
readStream!.pipe(fs.createWriteStream(path.join(directory, entry.fileName)));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function downloadPython(success: () => void, error: (err: Error) => void) {
|
||||
if (process.platform !== 'win32') {
|
||||
throw new Error('Unsupported platform');
|
||||
}
|
||||
const pythonUrl = 'https://www.python.org/ftp/python/3.7.0/python-3.7.0-embed-amd64.zip';
|
||||
const pythonFolder = mkdir(winmlDataFoler, 'python');
|
||||
const data: Buffer[] = [];
|
||||
https.get(pythonUrl, response => {
|
||||
response
|
||||
.on('data', (x: string) => data.push(Buffer.from(x, 'binary')))
|
||||
.on('end', () => unzip(Buffer.concat(data), pythonFolder, success, error))
|
||||
.on('error', error);
|
||||
// Change the PTH to discover modules from Lib/site-packages, so that pip modules can be found
|
||||
const pythonPth = `Lib/site-packages
|
||||
python36.zip
|
||||
.
|
||||
# Uncomment to run site.main() automatically
|
||||
import site`;
|
||||
|
||||
export async function downloadPython() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (process.platform !== 'win32') {
|
||||
reject('Unsupported platform');
|
||||
}
|
||||
const pythonUrl = 'https://www.python.org/ftp/python/3.7.0/python-3.7.0-embed-amd64.zip';
|
||||
const pythonFolder = mkdir(venv, 'Scripts');
|
||||
const data: Buffer[] = [];
|
||||
https.get(pythonUrl, response => {
|
||||
response
|
||||
.on('data', (x: string) => data.push(Buffer.from(x, 'binary')))
|
||||
.on('end', async () => {
|
||||
try {
|
||||
await unzip(Buffer.concat(data), pythonFolder);
|
||||
fs.writeFileSync(path.join(pythonFolder, 'python37._pth'), pythonPth);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
})
|
||||
.on('error', reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getVenvPython() {
|
||||
// Get Python installed in local venv
|
||||
return filterPythonBinaries([path.join(venv, 'Scripts/python'), path.join(venv, 'bin/python')])[0];
|
||||
for (const binary of [localPython, path.join(venv, 'bin/python')]) {
|
||||
if (fs.existsSync(binary)) {
|
||||
return binary;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export async function installPip() {
|
||||
// Python embedded distribution for Windows doesn't have pip
|
||||
return new Promise((resolve, reject) => {
|
||||
const data: Buffer[] = [];
|
||||
https.get('https://bootstrap.pypa.io/get-pip.py', response => {
|
||||
response
|
||||
.on('data', (x: string) => data.push(Buffer.from(x, 'binary')))
|
||||
.on('end', async () => {
|
||||
try {
|
||||
const getPip = path.join(venv, 'get-pip.py');
|
||||
fs.writeFileSync(getPip, Buffer.concat(data));
|
||||
await python(getPip);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
})
|
||||
.on('error', reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function python(...command: string[]) {
|
||||
return await promisify(execFile)(getVenvPython()!, ['-m', ...command]);
|
||||
return await promisify(execFile)(getVenvPython()!, [...command]);
|
||||
}
|
||||
|
||||
export async function pip(...command: string[]) {
|
||||
return await python('pip', ...command);
|
||||
return await python('-m', 'pip', ...command);
|
||||
}
|
||||
|
||||
export async function installVenv(targetPython: string) {
|
||||
await promisify(execFile)(targetPython, ['-m', 'venv', venv]);
|
||||
await pip('install', '-U', 'pip');
|
||||
await pip('install', '-r', path.join(__filename, 'requirements.txt'));
|
||||
}
|
||||
|
|
|
@ -2,17 +2,19 @@ import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
|
|||
import { ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react/lib/ChoiceGroup';
|
||||
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
|
||||
import { winmlDataFoler } from '../../persistence/appData';
|
||||
import { downloadPython, getPythonBinaries, getVenvPython, installVenv, localPython } from '../../python/python';
|
||||
import { downloadPython, getPythonBinaries, getVenvPython, installVenv, pip } from '../../python/python';
|
||||
|
||||
import './View.css';
|
||||
|
||||
enum InstallationStep {
|
||||
NotInstalling,
|
||||
Downloading,
|
||||
Installing,
|
||||
CreatingVenv,
|
||||
InstallingRequirements,
|
||||
}
|
||||
|
||||
interface IComponentState {
|
||||
|
@ -21,7 +23,7 @@ interface IComponentState {
|
|||
}
|
||||
|
||||
export default class ConvertView extends React.Component<{}, IComponentState> {
|
||||
private venvPython: string;
|
||||
private venvPython: string | undefined;
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
@ -47,8 +49,10 @@ export default class ConvertView extends React.Component<{}, IComponentState> {
|
|||
switch (this.state.installationStep) {
|
||||
case InstallationStep.Downloading:
|
||||
return <Spinner label="Downloading Python..." />;
|
||||
case InstallationStep.Installing:
|
||||
return <Spinner label="Downloading and installing converter..." />;
|
||||
case InstallationStep.CreatingVenv:
|
||||
return <Spinner label="Creating virtual environment..." />;
|
||||
case InstallationStep.InstallingRequirements:
|
||||
return <Spinner label="Downloading and installing requirements..." />;
|
||||
}
|
||||
this.venvPython = this.venvPython || getVenvPython();
|
||||
if (!this.venvPython) {
|
||||
|
@ -60,12 +64,19 @@ export default class ConvertView extends React.Component<{}, IComponentState> {
|
|||
private pythonChooser = () => {
|
||||
const binaries = getPythonBinaries();
|
||||
const options = binaries.map((key) => key ? { key, text: key } : { key: '__download', text: 'Download a new Python binary to be used exclusively by the WinML Dashboard' });
|
||||
const onChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption) => {
|
||||
if (option.key === '__download') {
|
||||
this.setState({ installationStep: InstallationStep.Downloading });
|
||||
downloadPython(() => this.installVenv(localPython), (err) => this.setState({ error: err }));
|
||||
} else {
|
||||
this.installVenv(option.key);
|
||||
const onChange = async (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption) => {
|
||||
try {
|
||||
if (option.key === '__download') {
|
||||
this.setState({ installationStep: InstallationStep.Downloading });
|
||||
await downloadPython();
|
||||
} else {
|
||||
this.setState({ installationStep: InstallationStep.CreatingVenv });
|
||||
await installVenv(option.key);
|
||||
}
|
||||
await this.installRequirements();
|
||||
this.setState({ installationStep: InstallationStep.NotInstalling });
|
||||
} catch (error) {
|
||||
this.setState({ error, installationStep: InstallationStep.NotInstalling });
|
||||
}
|
||||
}
|
||||
return (
|
||||
|
@ -77,14 +88,9 @@ export default class ConvertView extends React.Component<{}, IComponentState> {
|
|||
);
|
||||
}
|
||||
|
||||
private installVenv = async (targetPython: string) => {
|
||||
this.setState({ installationStep: InstallationStep.Installing });
|
||||
try {
|
||||
await installVenv(targetPython);
|
||||
this.setState({ installationStep: InstallationStep.NotInstalling });
|
||||
} catch (error) {
|
||||
this.setState({ error, installationStep: InstallationStep.NotInstalling });
|
||||
}
|
||||
private installRequirements = async () => {
|
||||
this.setState({ installationStep: InstallationStep.InstallingRequirements });
|
||||
await pip('install', '-r', path.join(__filename, 'requirements.txt'));
|
||||
}
|
||||
|
||||
private convert = () => {
|
||||
|
|
Загрузка…
Ссылка в новой задаче