From d3f7e79d54d32edb0f7efc5a16f998db44cbda45 Mon Sep 17 00:00:00 2001 From: SerVB Date: Tue, 14 Apr 2020 19:14:52 +0300 Subject: [PATCH] Support installation specified of Python version for Linux --- dist/index.js | 495 +++++++++++++++++++++++------------------ src/download-python.ts | 43 ++++ src/find-python.ts | 10 +- 3 files changed, 336 insertions(+), 212 deletions(-) create mode 100644 src/download-python.ts diff --git a/dist/index.js b/dist/index.js index a91c8a5d..50e0d832 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1250,6 +1250,73 @@ module.exports = SemVer module.exports = require("os"); +/***/ }), + +/***/ 98: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const exec = __importStar(__webpack_require__(986)); +function getVariable(variableName) { + return __awaiter(this, void 0, void 0, function* () { + let variableValue = ''; + const options = { + listeners: { + stdout: (data) => { + variableValue += data.toString(); + } + } + }; + yield exec.exec('bash', ['-c', `echo $${variableName}`], options); + return variableValue.trim(); + }); +} +function downloadLinuxCpython(version) { + return __awaiter(this, void 0, void 0, function* () { + const home = yield getVariable('HOME'); + yield exec.exec('bash', [ + '-c', + ` + set -e # Any command which returns non-zero exit code will cause this shell script to exit immediately + set -x # Activate debugging to show execution details: all commands will be printed before execution + + sudo apt-get install build-essential checkinstall + sudo apt-get install libreadline-gplv2-dev libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev + + cd $HOME + wget https://www.python.org/ftp/python/${version}/Python-${version}.tgz + + tar -xvf Python-${version}.tgz + cd Python-${version} + ./configure + make + sudo checkinstall -y + ` + ]); + return `${home}/Python-${version}`; + }); +} +exports.downloadLinuxCpython = downloadLinuxCpython; + + /***/ }), /***/ 120: @@ -2235,45 +2302,45 @@ const Range = __webpack_require__(124) /***/ (function(__unusedmodule, exports, __webpack_require__) { "use strict"; - -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; - result["default"] = mod; - return result; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const core = __importStar(__webpack_require__(470)); -const finder = __importStar(__webpack_require__(927)); -const path = __importStar(__webpack_require__(622)); -function run() { - return __awaiter(this, void 0, void 0, function* () { - try { - let version = core.getInput('python-version'); - if (version) { - const arch = core.getInput('architecture', { required: true }); - const installed = yield finder.findPythonVersion(version, arch); - core.info(`Successfully setup ${installed.impl} (${installed.version})`); - } - const matchersPath = path.join(__dirname, '..', '.github'); - core.info(`##[add-matcher]${path.join(matchersPath, 'python.json')}`); - } - catch (err) { - core.setFailed(err.message); - } - }); -} -run(); + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(__webpack_require__(470)); +const finder = __importStar(__webpack_require__(927)); +const path = __importStar(__webpack_require__(622)); +function run() { + return __awaiter(this, void 0, void 0, function* () { + try { + let version = core.getInput('python-version'); + if (version) { + const arch = core.getInput('architecture', { required: true }); + const installed = yield finder.findPythonVersion(version, arch); + core.info(`Successfully setup ${installed.impl} (${installed.version})`); + } + const matchersPath = path.join(__dirname, '..', '.github'); + core.info(`##[add-matcher]${path.join(matchersPath, 'python.json')}`); + } + catch (err) { + core.setFailed(err.message); + } + }); +} +run(); /***/ }), @@ -6258,178 +6325,184 @@ module.exports = lte /***/ (function(__unusedmodule, exports, __webpack_require__) { "use strict"; - -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; - result["default"] = mod; - return result; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const os = __importStar(__webpack_require__(87)); -const path = __importStar(__webpack_require__(622)); -const semver = __importStar(__webpack_require__(876)); -let cacheDirectory = process.env['RUNNER_TOOLSDIRECTORY'] || ''; -if (!cacheDirectory) { - let baseLocation; - if (process.platform === 'win32') { - // On windows use the USERPROFILE env variable - baseLocation = process.env['USERPROFILE'] || 'C:\\'; - } - else { - if (process.platform === 'darwin') { - baseLocation = '/Users'; - } - else { - baseLocation = '/home'; - } - } - cacheDirectory = path.join(baseLocation, 'actions', 'cache'); -} -const core = __importStar(__webpack_require__(470)); -const tc = __importStar(__webpack_require__(533)); -const IS_WINDOWS = process.platform === 'win32'; -// Python has "scripts" or "bin" directories where command-line tools that come with packages are installed. -// This is where pip is, along with anything that pip installs. -// There is a seperate directory for `pip install --user`. -// -// For reference, these directories are as follows: -// macOS / Linux: -// /bin (by default /usr/local/bin, but not on hosted agents -- see the `else`) -// (--user) ~/.local/bin -// Windows: -// \Scripts -// (--user) %APPDATA%\Python\PythonXY\Scripts -// See https://docs.python.org/3/library/sysconfig.html -function binDir(installDir) { - if (IS_WINDOWS) { - return path.join(installDir, 'Scripts'); - } - else { - return path.join(installDir, 'bin'); - } -} -// Note on the tool cache layout for PyPy: -// PyPy has its own versioning scheme that doesn't follow the Python versioning scheme. -// A particular version of PyPy may contain one or more versions of the Python interpreter. -// For example, PyPy 7.0 contains Python 2.7, 3.5, and 3.6-alpha. -// We only care about the Python version, so we don't use the PyPy version for the tool cache. -function usePyPy(majorVersion, architecture) { - const findPyPy = tc.find.bind(undefined, 'PyPy', majorVersion.toString()); - let installDir = findPyPy(architecture); - if (!installDir && IS_WINDOWS) { - // PyPy only precompiles binaries for x86, but the architecture parameter defaults to x64. - // On our Windows virtual environments, we only install an x86 version. - // Fall back to x86. - installDir = findPyPy('x86'); - } - if (!installDir) { - // PyPy not installed in $(Agent.ToolsDirectory) - throw new Error(`PyPy ${majorVersion} not found`); - } - // For PyPy, Windows uses 'bin', not 'Scripts'. - const _binDir = path.join(installDir, 'bin'); - // On Linux and macOS, the Python interpreter is in 'bin'. - // On Windows, it is in the installation root. - const pythonLocation = IS_WINDOWS ? installDir : _binDir; - core.exportVariable('pythonLocation', pythonLocation); - core.addPath(installDir); - core.addPath(_binDir); - const impl = 'pypy' + majorVersion.toString(); - core.setOutput('python-version', impl); - return { impl: impl, version: versionFromPath(installDir) }; -} -function useCpythonVersion(version, architecture) { - return __awaiter(this, void 0, void 0, function* () { - const desugaredVersionSpec = desugarDevVersion(version); - const semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec); - core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`); - const installDir = tc.find('Python', semanticVersionSpec, architecture); - if (!installDir) { - // Fail and list available versions - const x86Versions = tc - .findAllVersions('Python', 'x86') - .map(s => `${s} (x86)`) - .join(os.EOL); - const x64Versions = tc - .findAllVersions('Python', 'x64') - .map(s => `${s} (x64)`) - .join(os.EOL); - throw new Error([ - `Version ${version} with arch ${architecture} not found`, - 'Available versions:', - x86Versions, - x64Versions - ].join(os.EOL)); - } - core.exportVariable('pythonLocation', installDir); - core.addPath(installDir); - core.addPath(binDir(installDir)); - if (IS_WINDOWS) { - // Add --user directory - // `installDir` from tool cache should look like $RUNNER_TOOL_CACHE/Python//x64/ - // So if `findLocalTool` succeeded above, we must have a conformant `installDir` - const version = path.basename(path.dirname(installDir)); - const major = semver.major(version); - const minor = semver.minor(version); - const userScriptsDir = path.join(process.env['APPDATA'] || '', 'Python', `Python${major}${minor}`, 'Scripts'); - core.addPath(userScriptsDir); - } - // On Linux and macOS, pip will create the --user directory and add it to PATH as needed. - const installed = versionFromPath(installDir); - core.setOutput('python-version', installed); - return { impl: 'CPython', version: installed }; - }); -} -/** Convert versions like `3.8-dev` to a version like `>= 3.8.0-a0`. */ -function desugarDevVersion(versionSpec) { - if (versionSpec.endsWith('-dev')) { - const versionRoot = versionSpec.slice(0, -'-dev'.length); - return `>= ${versionRoot}.0-a0`; - } - else { - return versionSpec; - } -} -/** Extracts python version from install path from hosted tool cache as described in README.md */ -function versionFromPath(installDir) { - const parts = installDir.split(path.sep); - const idx = parts.findIndex(part => part === 'PyPy' || part === 'Python'); - return parts[idx + 1] || ''; -} -/** - * Python's prelease versions look like `3.7.0b2`. - * This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-b2`. - * If the version spec contains prerelease versions, we need to convert them to the semantic version equivalent. - */ -function pythonVersionToSemantic(versionSpec) { - const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)/g; - return versionSpec.replace(prereleaseVersion, '$1-$2'); -} -exports.pythonVersionToSemantic = pythonVersionToSemantic; -function findPythonVersion(version, architecture) { - return __awaiter(this, void 0, void 0, function* () { - switch (version.toUpperCase()) { - case 'PYPY2': - return usePyPy(2, architecture); - case 'PYPY3': - return usePyPy(3, architecture); - default: - return yield useCpythonVersion(version, architecture); - } - }); -} -exports.findPythonVersion = findPythonVersion; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const os = __importStar(__webpack_require__(87)); +const path = __importStar(__webpack_require__(622)); +const semver = __importStar(__webpack_require__(876)); +const downloader = __importStar(__webpack_require__(98)); +let cacheDirectory = process.env['RUNNER_TOOLSDIRECTORY'] || ''; +if (!cacheDirectory) { + let baseLocation; + if (process.platform === 'win32') { + // On windows use the USERPROFILE env variable + baseLocation = process.env['USERPROFILE'] || 'C:\\'; + } + else { + if (process.platform === 'darwin') { + baseLocation = '/Users'; + } + else { + baseLocation = '/home'; + } + } + cacheDirectory = path.join(baseLocation, 'actions', 'cache'); +} +const core = __importStar(__webpack_require__(470)); +const tc = __importStar(__webpack_require__(533)); +const IS_WINDOWS = process.platform === 'win32'; +const IS_LINUX = process.platform === 'linux'; +// Python has "scripts" or "bin" directories where command-line tools that come with packages are installed. +// This is where pip is, along with anything that pip installs. +// There is a seperate directory for `pip install --user`. +// +// For reference, these directories are as follows: +// macOS / Linux: +// /bin (by default /usr/local/bin, but not on hosted agents -- see the `else`) +// (--user) ~/.local/bin +// Windows: +// \Scripts +// (--user) %APPDATA%\Python\PythonXY\Scripts +// See https://docs.python.org/3/library/sysconfig.html +function binDir(installDir) { + if (IS_WINDOWS) { + return path.join(installDir, 'Scripts'); + } + else { + return path.join(installDir, 'bin'); + } +} +// Note on the tool cache layout for PyPy: +// PyPy has its own versioning scheme that doesn't follow the Python versioning scheme. +// A particular version of PyPy may contain one or more versions of the Python interpreter. +// For example, PyPy 7.0 contains Python 2.7, 3.5, and 3.6-alpha. +// We only care about the Python version, so we don't use the PyPy version for the tool cache. +function usePyPy(majorVersion, architecture) { + const findPyPy = tc.find.bind(undefined, 'PyPy', majorVersion.toString()); + let installDir = findPyPy(architecture); + if (!installDir && IS_WINDOWS) { + // PyPy only precompiles binaries for x86, but the architecture parameter defaults to x64. + // On our Windows virtual environments, we only install an x86 version. + // Fall back to x86. + installDir = findPyPy('x86'); + } + if (!installDir) { + // PyPy not installed in $(Agent.ToolsDirectory) + throw new Error(`PyPy ${majorVersion} not found`); + } + // For PyPy, Windows uses 'bin', not 'Scripts'. + const _binDir = path.join(installDir, 'bin'); + // On Linux and macOS, the Python interpreter is in 'bin'. + // On Windows, it is in the installation root. + const pythonLocation = IS_WINDOWS ? installDir : _binDir; + core.exportVariable('pythonLocation', pythonLocation); + core.addPath(installDir); + core.addPath(_binDir); + const impl = 'pypy' + majorVersion.toString(); + core.setOutput('python-version', impl); + return { impl: impl, version: versionFromPath(installDir) }; +} +function useCpythonVersion(version, architecture) { + return __awaiter(this, void 0, void 0, function* () { + const desugaredVersionSpec = desugarDevVersion(version); + const semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec); + core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`); + let installDir = tc.find('Python', semanticVersionSpec, architecture); + if (!installDir && IS_LINUX) { + core.info(`Can't find installed CPython ${version}; trying to download`); + installDir = yield downloader.downloadLinuxCpython(version); + } + if (!installDir) { + // Fail and list available versions + const x86Versions = tc + .findAllVersions('Python', 'x86') + .map(s => `${s} (x86)`) + .join(os.EOL); + const x64Versions = tc + .findAllVersions('Python', 'x64') + .map(s => `${s} (x64)`) + .join(os.EOL); + throw new Error([ + `Version ${version} with arch ${architecture} not found`, + 'Available versions:', + x86Versions, + x64Versions + ].join(os.EOL)); + } + core.exportVariable('pythonLocation', installDir); + core.addPath(installDir); + core.addPath(binDir(installDir)); + if (IS_WINDOWS) { + // Add --user directory + // `installDir` from tool cache should look like $RUNNER_TOOL_CACHE/Python//x64/ + // So if `findLocalTool` succeeded above, we must have a conformant `installDir` + const version = path.basename(path.dirname(installDir)); + const major = semver.major(version); + const minor = semver.minor(version); + const userScriptsDir = path.join(process.env['APPDATA'] || '', 'Python', `Python${major}${minor}`, 'Scripts'); + core.addPath(userScriptsDir); + } + // On Linux and macOS, pip will create the --user directory and add it to PATH as needed. + const installed = versionFromPath(installDir); + core.setOutput('python-version', installed); + return { impl: 'CPython', version: installed }; + }); +} +/** Convert versions like `3.8-dev` to a version like `>= 3.8.0-a0`. */ +function desugarDevVersion(versionSpec) { + if (versionSpec.endsWith('-dev')) { + const versionRoot = versionSpec.slice(0, -'-dev'.length); + return `>= ${versionRoot}.0-a0`; + } + else { + return versionSpec; + } +} +/** Extracts python version from install path from hosted tool cache as described in README.md */ +function versionFromPath(installDir) { + const parts = installDir.split(path.sep); + const idx = parts.findIndex(part => part === 'PyPy' || part === 'Python'); + return parts[idx + 1] || ''; +} +/** + * Python's prelease versions look like `3.7.0b2`. + * This is the one part of Python versioning that does not look like semantic versioning, which specifies `3.7.0-b2`. + * If the version spec contains prerelease versions, we need to convert them to the semantic version equivalent. + */ +function pythonVersionToSemantic(versionSpec) { + const prereleaseVersion = /(\d+\.\d+\.\d+)((?:a|b|rc)\d*)/g; + return versionSpec.replace(prereleaseVersion, '$1-$2'); +} +exports.pythonVersionToSemantic = pythonVersionToSemantic; +function findPythonVersion(version, architecture) { + return __awaiter(this, void 0, void 0, function* () { + switch (version.toUpperCase()) { + case 'PYPY2': + return usePyPy(2, architecture); + case 'PYPY3': + return usePyPy(3, architecture); + default: + return yield useCpythonVersion(version, architecture); + } + }); +} +exports.findPythonVersion = findPythonVersion; /***/ }), diff --git a/src/download-python.ts b/src/download-python.ts new file mode 100644 index 00000000..0453668a --- /dev/null +++ b/src/download-python.ts @@ -0,0 +1,43 @@ +import * as exec from '@actions/exec'; + +async function getVariable(variableName: string): Promise { + let variableValue = ''; + + const options = { + listeners: { + stdout: (data: Buffer) => { + variableValue += data.toString(); + } + } + }; + + await exec.exec('bash', ['-c', `echo $${variableName}`], options); + + return variableValue.trim(); +} + +export async function downloadLinuxCpython(version: string): Promise { + const home = await getVariable('HOME'); + + await exec.exec('bash', [ + '-c', + ` + set -e # Any command which returns non-zero exit code will cause this shell script to exit immediately + set -x # Activate debugging to show execution details: all commands will be printed before execution + + sudo apt-get install build-essential checkinstall + sudo apt-get install libreadline-gplv2-dev libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev + + cd $HOME + wget https://www.python.org/ftp/python/${version}/Python-${version}.tgz + + tar -xvf Python-${version}.tgz + cd Python-${version} + ./configure + make + sudo checkinstall -y + ` + ]); + + return `${home}/Python-${version}`; +} diff --git a/src/find-python.ts b/src/find-python.ts index 151eafe5..871c3cc3 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -3,6 +3,8 @@ import * as path from 'path'; import * as semver from 'semver'; +import * as downloader from './download-python'; + let cacheDirectory = process.env['RUNNER_TOOLSDIRECTORY'] || ''; if (!cacheDirectory) { @@ -24,6 +26,7 @@ import * as core from '@actions/core'; import * as tc from '@actions/tool-cache'; const IS_WINDOWS = process.platform === 'win32'; +const IS_LINUX = process.platform === 'linux'; // Python has "scripts" or "bin" directories where command-line tools that come with packages are installed. // This is where pip is, along with anything that pip installs. @@ -92,11 +95,16 @@ async function useCpythonVersion( const semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec); core.debug(`Semantic version spec of ${version} is ${semanticVersionSpec}`); - const installDir: string | null = tc.find( + let installDir: string | null = tc.find( 'Python', semanticVersionSpec, architecture ); + if (!installDir && IS_LINUX) { + core.info(`Can't find installed CPython ${version}; trying to download`); + installDir = await downloader.downloadLinuxCpython(version); + } + if (!installDir) { // Fail and list available versions const x86Versions = tc