From d3f7e79d54d32edb0f7efc5a16f998db44cbda45 Mon Sep 17 00:00:00 2001
From: SerVB <servbul@yandex.ru>
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:
-//      <sys.prefix>/bin (by default /usr/local/bin, but not on hosted agents -- see the `else`)
-//      (--user) ~/.local/bin
-//   Windows:
-//      <Python installation dir>\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/<semantic version>/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:
+//      <sys.prefix>/bin (by default /usr/local/bin, but not on hosted agents -- see the `else`)
+//      (--user) ~/.local/bin
+//   Windows:
+//      <Python installation dir>\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/<semantic version>/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<string> {
+  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<string> {
+  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