Add ability to download specific version of Python from releases

This commit is contained in:
MaksimZhukov 2020-04-24 20:29:24 +03:00
parent 985150d1f6
commit 81b3a118a7
9 changed files with 4063 additions and 3618 deletions

57
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,57 @@
name: Validate 'setup-python'
on:
push:
pull_request:
schedule:
- cron: 0 0 * * *
jobs:
default-version:
name: Setup default version
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest, ubuntu-16.04, ubuntu-18.04]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: setup default python
uses: ./
- name: Validate version
run: python --version
- name: Run simple python code
run: python -c 'import math; print(math.factorial(5))'
setup-versions-from-manifest:
name: Setup ${{ matrix.python }} ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest, ubuntu-16.04, ubuntu-18.04]
python: [3.5.3, 3.6.7, 3.7.5, 3.8.1]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: setup-python ${{ matrix.python }}
uses: ./
with:
python-version: ${{ matrix.python }}
- name: Validate version
run: |
$pythonVersion = (python --version)
if ("Python ${{ matrix.python }}" -ne "$pythonVersion"){
Write-Host "The current version is $pythonVersion; expected version is ${{ matrix.python }}"
exit 1
}
$pythonVersion
shell: pwsh
- name: Run simple code
run: python -c 'import math; print(math.factorial(5))'

View File

@ -0,0 +1,13 @@
{
"version": "1.2.3",
"stable": true,
"release_url": "https://github.com/actions/sometool/releases/tag/1.2.3-20200402.6",
"files": [
{
"filename": "sometool-1.2.3-linux-x64.tar.gz",
"arch": "x64",
"platform": "linux",
"download_url": "https://github.com/actions/sometool/releases/tag/1.2.3-20200402.6/sometool-1.2.3-linux-x64.tar.gz"
}
]
}

View File

@ -18,19 +18,48 @@ const tempDir = path.join(
process.env['RUNNER_TOOL_CACHE'] = toolDir;
process.env['RUNNER_TEMP'] = tempDir;
import * as tc from '@actions/tool-cache';
import * as finder from '../src/find-python';
import * as installer from '../src/install-python';
const pythonRelease = require('./data/python-release.json');
describe('Finder tests', () => {
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
it('Finds Python if it is installed', async () => {
const pythonDir: string = path.join(toolDir, 'Python', '3.0.0', 'x64');
await io.mkdirP(pythonDir);
fs.writeFileSync(`${pythonDir}.complete`, 'hello');
// This will throw if it doesn't find it in the cache (because no such version exists)
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
await finder.findPythonVersion('3.x', 'x64');
});
it('Finds Python if it is not installed, but exists in the manifest', async () => {
const findSpy: jest.SpyInstance = jest.spyOn(
installer,
'findReleaseFromManifest'
);
findSpy.mockImplementation(() => <tc.IToolRelease>pythonRelease);
const installSpy: jest.SpyInstance = jest.spyOn(
installer,
'installCpythonFromRelease'
);
installSpy.mockImplementation(async () => {
const pythonDir: string = path.join(toolDir, 'Python', '1.2.3', 'x64');
await io.mkdirP(pythonDir);
fs.writeFileSync(`${pythonDir}.complete`, 'hello');
});
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
await finder.findPythonVersion('1.2.3', 'x64');
});
it('Errors if Python is not installed', async () => {
// This will throw if it doesn't find it in the cache (because no such version exists)
// This will throw if it doesn't find it in the cache and in the manifest (because no such version exists)
let thrown = false;
try {
await finder.findPythonVersion('3.300000', 'x64');

View File

@ -9,6 +9,11 @@ inputs:
architecture:
description: 'The target architecture (x86, x64) of the Python interpreter.'
default: 'x64'
token:
description: >
Personal access token (PAT) used to fetch the manifest file from master of repository
[Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
default: ${{ github.token }}
outputs:
python-version:
description: "The installed python version. Useful when given a version range as input."

7417
dist/index.js vendored

File diff suppressed because it is too large Load Diff

14
package-lock.json generated
View File

@ -19,9 +19,9 @@
}
},
"@actions/http-client": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.6.tgz",
"integrity": "sha512-LGmio4w98UyGX33b/W6V6Nx/sQHRXZ859YlMkn36wPsXPB82u8xTVlA/Dq2DXrm6lEq9RVmisRJa1c+HETAIJA==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz",
"integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==",
"dev": true,
"requires": {
"tunnel": "0.0.6"
@ -34,14 +34,14 @@
"dev": true
},
"@actions/tool-cache": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-1.3.3.tgz",
"integrity": "sha512-AFVyTLcIxusDVI1gMhbZW8m/On7YNJG+xYaxorV+qic+f7lO7h37aT2mfzxqAq7mwHxtP1YlVFNrXe9QDf/bPg==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-1.5.2.tgz",
"integrity": "sha512-40A1St0GEo+QvHV1YRjStEoQcKKMaip+zNXPgGHcjYXCdZ7cl1LGlwOpsVVqwk+6ue/shFTS76KC1A02mVVCQA==",
"dev": true,
"requires": {
"@actions/core": "^1.2.0",
"@actions/exec": "^1.0.0",
"@actions/http-client": "^1.0.3",
"@actions/http-client": "^1.0.8",
"@actions/io": "^1.0.1",
"semver": "^6.1.0",
"uuid": "^3.3.2"

View File

@ -28,7 +28,7 @@
},
"devDependencies": {
"@actions/io": "^1.0.2",
"@actions/tool-cache": "^1.3.3",
"@actions/tool-cache": "^1.5.2",
"@types/jest": "^25.1.4",
"@types/node": "^12.12.31",
"@types/semver": "^7.1.0",

View File

@ -3,6 +3,8 @@ import * as path from 'path';
import * as semver from 'semver';
import * as installer from './install-python';
let cacheDirectory = process.env['RUNNER_TOOLSDIRECTORY'] || '';
if (!cacheDirectory) {
@ -92,29 +94,31 @@ 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) {
// Fail and list available versions
const x86Versions = tc
.findAllVersions('Python', 'x86')
.map(s => `${s} (x86)`)
.join(os.EOL);
core.info(`Version ${semanticVersionSpec} is not found in local cache`);
const foundRelease = await installer.findReleaseFromManifest(
semanticVersionSpec,
architecture
);
const x64Versions = tc
.findAllVersions('Python', 'x64')
.map(s => `${s} (x64)`)
.join(os.EOL);
if (foundRelease && foundRelease.files && foundRelease.files.length > 0) {
core.info(`Version ${semanticVersionSpec} is available for downloading`);
await installer.installCpythonFromRelease(foundRelease);
installDir = tc.find('Python', semanticVersionSpec, architecture);
}
}
if (!installDir) {
throw new Error(
[
`Version ${version} with arch ${architecture} not found`,
'Available versions:',
x86Versions,
x64Versions
`The list of all available versions can be found here: ${installer.MANIFEST_URL}`
].join(os.EOL)
);
}

64
src/install-python.ts Normal file
View File

@ -0,0 +1,64 @@
import * as path from 'path';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import {ExecOptions} from '@actions/exec/lib/interfaces';
const AUTH_TOKEN = core.getInput('token');
const MANIFEST_REPO_OWNER = 'actions';
const MANIFEST_REPO_NAME = 'python-versions';
export const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/master/versions-manifest.json`;
const IS_WINDOWS = process.platform === 'win32';
const IS_LINUX = process.platform === 'linux';
export async function findReleaseFromManifest(
semanticVersionSpec: string,
architecture: string
): Promise<tc.IToolRelease | undefined> {
const manifest: tc.IToolRelease[] = await tc.getManifestFromRepo(
MANIFEST_REPO_OWNER,
MANIFEST_REPO_NAME,
AUTH_TOKEN
);
return await tc.findFromManifest(
semanticVersionSpec,
true,
manifest,
architecture
);
}
export async function installCpythonFromRelease(release: tc.IToolRelease) {
const downloadUrl = release.files[0].download_url;
core.info(`Download from "${downloadUrl}"`);
const pythonPath = await tc.downloadTool(downloadUrl, undefined, AUTH_TOKEN);
const fileName = path.basename(pythonPath, '.zip');
core.info('Extract downloaded archive');
let pythonExtractedFolder;
if (IS_WINDOWS) {
pythonExtractedFolder = await tc.extractZip(pythonPath, `./${fileName}`);
} else {
pythonExtractedFolder = await tc.extractTar(pythonPath, `./${fileName}`);
}
const options: ExecOptions = {
cwd: pythonExtractedFolder,
silent: true,
listeners: {
stdout: (data: Buffer) => {
core.debug(data.toString().trim());
}
}
};
core.info('Execute installation script');
if (IS_WINDOWS) {
await exec.exec('powershell', ['./setup.ps1'], options);
} else if (IS_LINUX) {
await exec.exec('sudo', ['bash', './setup.sh'], options);
} else {
await exec.exec('bash', ['./setup.sh'], options);
}
}