| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,378 @@ | ||
| import fs from 'fs'; | ||
|
|
||
| import {HttpClient} from '@actions/http-client'; | ||
| import * as ifm from '@actions/http-client/lib/interfaces'; | ||
| import * as tc from '@actions/tool-cache'; | ||
| import * as exec from '@actions/exec'; | ||
| import * as core from '@actions/core'; | ||
|
|
||
| import * as path from 'path'; | ||
| import * as semver from 'semver'; | ||
|
|
||
| import * as finder from '../src/find-graalpy'; | ||
| import {IGraalPyManifestRelease, IS_WINDOWS} from '../src/utils'; | ||
|
|
||
| import manifestData from './data/graalpy.json'; | ||
|
|
||
| const architecture = 'x64'; | ||
|
|
||
| const toolDir = path.join(__dirname, 'runner', 'tools'); | ||
| const tempDir = path.join(__dirname, 'runner', 'temp'); | ||
|
|
||
| /* GraalPy doesn't have a windows release yet */ | ||
| const describeSkipOnWindows = IS_WINDOWS ? describe.skip : describe; | ||
|
|
||
| describe('parseGraalPyVersion', () => { | ||
| it.each([ | ||
| ['graalpy-23', '23'], | ||
| ['graalpy-23.0', '23.0'], | ||
| ['graalpy23.0', '23.0'] | ||
| ])('%s -> %s', (input, expected) => { | ||
| expect(finder.parseGraalPyVersion(input)).toEqual(expected); | ||
| }); | ||
|
|
||
| it.each(['', 'graalpy-', 'graalpy', 'p', 'notgraalpy-'])( | ||
| 'throw on invalid input "%s"', | ||
| input => { | ||
| expect(() => finder.parseGraalPyVersion(input)).toThrow( | ||
| "Invalid 'version' property for GraalPy. GraalPy version should be specified as 'graalpy<python-version>' or 'graalpy-<python-version>'. See README for examples and documentation." | ||
| ); | ||
| } | ||
| ); | ||
| }); | ||
|
|
||
| describe('findGraalPyToolCache', () => { | ||
| const actualGraalPyVersion = '23.0.0'; | ||
| const graalpyPath = path.join('GraalPy', actualGraalPyVersion, architecture); | ||
| let tcFind: jest.SpyInstance; | ||
| let infoSpy: jest.SpyInstance; | ||
| let warningSpy: jest.SpyInstance; | ||
| let debugSpy: jest.SpyInstance; | ||
| let addPathSpy: jest.SpyInstance; | ||
| let exportVariableSpy: jest.SpyInstance; | ||
| let setOutputSpy: jest.SpyInstance; | ||
|
|
||
| beforeEach(() => { | ||
| tcFind = jest.spyOn(tc, 'find'); | ||
| tcFind.mockImplementation((toolname: string, pythonVersion: string) => { | ||
| const semverVersion = new semver.Range(pythonVersion); | ||
| return semver.satisfies(actualGraalPyVersion, semverVersion) | ||
| ? graalpyPath | ||
| : ''; | ||
| }); | ||
|
|
||
| infoSpy = jest.spyOn(core, 'info'); | ||
| infoSpy.mockImplementation(() => null); | ||
|
|
||
| warningSpy = jest.spyOn(core, 'warning'); | ||
| warningSpy.mockImplementation(() => null); | ||
|
|
||
| debugSpy = jest.spyOn(core, 'debug'); | ||
| debugSpy.mockImplementation(() => null); | ||
|
|
||
| addPathSpy = jest.spyOn(core, 'addPath'); | ||
| addPathSpy.mockImplementation(() => null); | ||
|
|
||
| exportVariableSpy = jest.spyOn(core, 'exportVariable'); | ||
| exportVariableSpy.mockImplementation(() => null); | ||
|
|
||
| setOutputSpy = jest.spyOn(core, 'setOutput'); | ||
| setOutputSpy.mockImplementation(() => null); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| jest.resetAllMocks(); | ||
| jest.clearAllMocks(); | ||
| jest.restoreAllMocks(); | ||
| }); | ||
|
|
||
| it('GraalPy exists on the path and versions are satisfied', () => { | ||
| expect(finder.findGraalPyToolCache('23.0.0', architecture)).toEqual({ | ||
| installDir: graalpyPath, | ||
| resolvedGraalPyVersion: actualGraalPyVersion | ||
| }); | ||
| }); | ||
|
|
||
| it('GraalPy exists on the path and versions are satisfied with semver', () => { | ||
| expect(finder.findGraalPyToolCache('23.0', architecture)).toEqual({ | ||
| installDir: graalpyPath, | ||
| resolvedGraalPyVersion: actualGraalPyVersion | ||
| }); | ||
| }); | ||
|
|
||
| it("GraalPy exists on the path, but version doesn't match", () => { | ||
| expect(finder.findGraalPyToolCache('22.3', architecture)).toEqual({ | ||
| installDir: '', | ||
| resolvedGraalPyVersion: '' | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describeSkipOnWindows('findGraalPyVersion', () => { | ||
| let getBooleanInputSpy: jest.SpyInstance; | ||
| let warningSpy: jest.SpyInstance; | ||
| let debugSpy: jest.SpyInstance; | ||
| let infoSpy: jest.SpyInstance; | ||
| let addPathSpy: jest.SpyInstance; | ||
| let exportVariableSpy: jest.SpyInstance; | ||
| let setOutputSpy: jest.SpyInstance; | ||
| let tcFind: jest.SpyInstance; | ||
| let spyExtractZip: jest.SpyInstance; | ||
| let spyExtractTar: jest.SpyInstance; | ||
| let spyHttpClient: jest.SpyInstance; | ||
| let spyExistsSync: jest.SpyInstance; | ||
| let spyExec: jest.SpyInstance; | ||
| let spySymlinkSync: jest.SpyInstance; | ||
| let spyDownloadTool: jest.SpyInstance; | ||
| let spyFsReadDir: jest.SpyInstance; | ||
| let spyCacheDir: jest.SpyInstance; | ||
| let spyChmodSync: jest.SpyInstance; | ||
| let spyCoreAddPath: jest.SpyInstance; | ||
| let spyCoreExportVariable: jest.SpyInstance; | ||
| const env = process.env; | ||
|
|
||
| beforeEach(() => { | ||
| getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput'); | ||
| getBooleanInputSpy.mockImplementation(() => false); | ||
|
|
||
| infoSpy = jest.spyOn(core, 'info'); | ||
| infoSpy.mockImplementation(() => {}); | ||
|
|
||
| warningSpy = jest.spyOn(core, 'warning'); | ||
| warningSpy.mockImplementation(() => null); | ||
|
|
||
| debugSpy = jest.spyOn(core, 'debug'); | ||
| debugSpy.mockImplementation(() => null); | ||
|
|
||
| addPathSpy = jest.spyOn(core, 'addPath'); | ||
| addPathSpy.mockImplementation(() => null); | ||
|
|
||
| exportVariableSpy = jest.spyOn(core, 'exportVariable'); | ||
| exportVariableSpy.mockImplementation(() => null); | ||
|
|
||
| setOutputSpy = jest.spyOn(core, 'setOutput'); | ||
| setOutputSpy.mockImplementation(() => null); | ||
|
|
||
| jest.resetModules(); | ||
| process.env = {...env}; | ||
| tcFind = jest.spyOn(tc, 'find'); | ||
| tcFind.mockImplementation((tool: string, version: string) => { | ||
| const semverRange = new semver.Range(version); | ||
| let graalpyPath = ''; | ||
| if (semver.satisfies('23.0.0', semverRange)) { | ||
| graalpyPath = path.join(toolDir, 'GraalPy', '23.0.0', architecture); | ||
| } | ||
| return graalpyPath; | ||
| }); | ||
|
|
||
| spyDownloadTool = jest.spyOn(tc, 'downloadTool'); | ||
| spyDownloadTool.mockImplementation(() => path.join(tempDir, 'GraalPy')); | ||
|
|
||
| spyExtractZip = jest.spyOn(tc, 'extractZip'); | ||
| spyExtractZip.mockImplementation(() => tempDir); | ||
|
|
||
| spyExtractTar = jest.spyOn(tc, 'extractTar'); | ||
| spyExtractTar.mockImplementation(() => tempDir); | ||
|
|
||
| spyFsReadDir = jest.spyOn(fs, 'readdirSync'); | ||
| spyFsReadDir.mockImplementation((directory: string) => ['GraalPyTest']); | ||
|
|
||
| spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); | ||
| spyHttpClient.mockImplementation( | ||
| async (): Promise<ifm.TypedResponse<IGraalPyManifestRelease[]>> => { | ||
| const result = JSON.stringify(manifestData); | ||
| return { | ||
| statusCode: 200, | ||
| headers: {}, | ||
| result: JSON.parse(result) as IGraalPyManifestRelease[] | ||
| }; | ||
| } | ||
| ); | ||
|
|
||
| spyExec = jest.spyOn(exec, 'exec'); | ||
| spyExec.mockImplementation(() => undefined); | ||
|
|
||
| spySymlinkSync = jest.spyOn(fs, 'symlinkSync'); | ||
| spySymlinkSync.mockImplementation(() => undefined); | ||
|
|
||
| spyExistsSync = jest.spyOn(fs, 'existsSync'); | ||
| spyExistsSync.mockReturnValue(true); | ||
|
|
||
| spyCoreAddPath = jest.spyOn(core, 'addPath'); | ||
|
|
||
| spyCoreExportVariable = jest.spyOn(core, 'exportVariable'); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| jest.resetAllMocks(); | ||
| jest.clearAllMocks(); | ||
| jest.restoreAllMocks(); | ||
| process.env = env; | ||
| }); | ||
|
|
||
| it('found GraalPy in toolcache', async () => { | ||
| await expect( | ||
| finder.findGraalPyVersion( | ||
| 'graalpy-23.0', | ||
| architecture, | ||
| true, | ||
| false, | ||
| false | ||
| ) | ||
| ).resolves.toEqual('23.0.0'); | ||
| expect(spyCoreAddPath).toHaveBeenCalled(); | ||
| expect(spyCoreExportVariable).toHaveBeenCalledWith( | ||
| 'pythonLocation', | ||
| expect.anything() | ||
| ); | ||
| expect(spyCoreExportVariable).toHaveBeenCalledWith( | ||
| 'PKG_CONFIG_PATH', | ||
| expect.anything() | ||
| ); | ||
| }); | ||
|
|
||
| it('throw on invalid input format', async () => { | ||
| await expect( | ||
| finder.findGraalPyVersion('graalpy-x23', architecture, true, false, false) | ||
| ).rejects.toThrow(); | ||
| }); | ||
|
|
||
| it('found and install successfully', async () => { | ||
| spyCacheDir = jest.spyOn(tc, 'cacheDir'); | ||
| spyCacheDir.mockImplementation(() => | ||
| path.join(toolDir, 'GraalPy', '23.0.0', architecture) | ||
| ); | ||
| spyChmodSync = jest.spyOn(fs, 'chmodSync'); | ||
| spyChmodSync.mockImplementation(() => undefined); | ||
| await expect( | ||
| finder.findGraalPyVersion( | ||
| 'graalpy-23.0.0', | ||
| architecture, | ||
| true, | ||
| false, | ||
| false | ||
| ) | ||
| ).resolves.toEqual('23.0.0'); | ||
| expect(spyCoreAddPath).toHaveBeenCalled(); | ||
| expect(spyCoreExportVariable).toHaveBeenCalledWith( | ||
| 'pythonLocation', | ||
| expect.anything() | ||
| ); | ||
| expect(spyCoreExportVariable).toHaveBeenCalledWith( | ||
| 'PKG_CONFIG_PATH', | ||
| expect.anything() | ||
| ); | ||
| }); | ||
|
|
||
| it('found and install successfully without environment update', async () => { | ||
| spyCacheDir = jest.spyOn(tc, 'cacheDir'); | ||
| spyCacheDir.mockImplementation(() => | ||
| path.join(toolDir, 'GraalPy', '23.0.0', architecture) | ||
| ); | ||
| spyChmodSync = jest.spyOn(fs, 'chmodSync'); | ||
| spyChmodSync.mockImplementation(() => undefined); | ||
| await expect( | ||
| finder.findGraalPyVersion( | ||
| 'graalpy-23.0.0', | ||
| architecture, | ||
| false, | ||
| false, | ||
| false | ||
| ) | ||
| ).resolves.toEqual('23.0.0'); | ||
| expect(spyCoreAddPath).not.toHaveBeenCalled(); | ||
| expect(spyCoreExportVariable).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('throw if release is not found', async () => { | ||
| await expect( | ||
| finder.findGraalPyVersion( | ||
| 'graalpy-19.0.0', | ||
| architecture, | ||
| true, | ||
| false, | ||
| false | ||
| ) | ||
| ).rejects.toThrow( | ||
| `GraalPy version 19.0.0 with arch ${architecture} not found` | ||
| ); | ||
| }); | ||
|
|
||
| it('check-latest enabled version found and used from toolcache', async () => { | ||
| await expect( | ||
| finder.findGraalPyVersion( | ||
| 'graalpy-23.0.0', | ||
| architecture, | ||
| false, | ||
| true, | ||
| false | ||
| ) | ||
| ).resolves.toEqual('23.0.0'); | ||
|
|
||
| expect(infoSpy).toHaveBeenCalledWith('Resolved as GraalPy 23.0.0'); | ||
| }); | ||
|
|
||
| it('check-latest enabled version found and install successfully', async () => { | ||
| spyCacheDir = jest.spyOn(tc, 'cacheDir'); | ||
| spyCacheDir.mockImplementation(() => | ||
| path.join(toolDir, 'GraalPy', '23.0.0', architecture) | ||
| ); | ||
| spyChmodSync = jest.spyOn(fs, 'chmodSync'); | ||
| spyChmodSync.mockImplementation(() => undefined); | ||
| await expect( | ||
| finder.findGraalPyVersion( | ||
| 'graalpy-23.0.0', | ||
| architecture, | ||
| false, | ||
| true, | ||
| false | ||
| ) | ||
| ).resolves.toEqual('23.0.0'); | ||
| expect(infoSpy).toHaveBeenCalledWith('Resolved as GraalPy 23.0.0'); | ||
| }); | ||
|
|
||
| it('check-latest enabled version is not found and used from toolcache', async () => { | ||
| tcFind.mockImplementationOnce((tool: string, version: string) => { | ||
| const semverRange = new semver.Range(version); | ||
| let graalpyPath = ''; | ||
| if (semver.satisfies('22.3.4', semverRange)) { | ||
| graalpyPath = path.join(toolDir, 'GraalPy', '22.3.4', architecture); | ||
| } | ||
| return graalpyPath; | ||
| }); | ||
| await expect( | ||
| finder.findGraalPyVersion( | ||
| 'graalpy-22.3.4', | ||
| architecture, | ||
| false, | ||
| true, | ||
| false | ||
| ) | ||
| ).resolves.toEqual('22.3.4'); | ||
|
|
||
| expect(infoSpy).toHaveBeenCalledWith( | ||
| 'Failed to resolve GraalPy 22.3.4 from manifest' | ||
| ); | ||
| }); | ||
|
|
||
| it('found and install successfully, pre-release fallback', async () => { | ||
| spyCacheDir = jest.spyOn(tc, 'cacheDir'); | ||
| spyCacheDir.mockImplementation(() => | ||
| path.join(toolDir, 'GraalPy', '23.1', architecture) | ||
| ); | ||
| spyChmodSync = jest.spyOn(fs, 'chmodSync'); | ||
| spyChmodSync.mockImplementation(() => undefined); | ||
| await expect( | ||
| finder.findGraalPyVersion( | ||
| 'graalpy23.1', | ||
| architecture, | ||
| false, | ||
| false, | ||
| false | ||
| ) | ||
| ).rejects.toThrow(); | ||
| await expect( | ||
| finder.findGraalPyVersion('graalpy23.1', architecture, false, false, true) | ||
| ).resolves.toEqual('23.1.0-a.1'); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,256 @@ | ||
| import fs from 'fs'; | ||
|
|
||
| import {HttpClient} from '@actions/http-client'; | ||
| import * as ifm from '@actions/http-client/lib/interfaces'; | ||
| import * as tc from '@actions/tool-cache'; | ||
| import * as exec from '@actions/exec'; | ||
| import * as core from '@actions/core'; | ||
| import * as path from 'path'; | ||
|
|
||
| import * as installer from '../src/install-graalpy'; | ||
| import { | ||
| IGraalPyManifestRelease, | ||
| IGraalPyManifestAsset, | ||
| IS_WINDOWS | ||
| } from '../src/utils'; | ||
|
|
||
| import manifestData from './data/graalpy.json'; | ||
|
|
||
| const architecture = 'x64'; | ||
|
|
||
| const toolDir = path.join(__dirname, 'runner', 'tools'); | ||
| const tempDir = path.join(__dirname, 'runner', 'temp'); | ||
|
|
||
| /* GraalPy doesn't have a windows release yet */ | ||
| const describeSkipOnWindows = IS_WINDOWS ? describe.skip : describe; | ||
|
|
||
| describe('graalpyVersionToSemantic', () => { | ||
| it.each([ | ||
| ['23.0.0a1', '23.0.0a1'], | ||
| ['23.0.0', '23.0.0'], | ||
| ['23.0.x', '23.0.x'], | ||
| ['23.x', '23.x'] | ||
| ])('%s -> %s', (input, expected) => { | ||
| expect(installer.graalPyTagToVersion(input)).toEqual(expected); | ||
| }); | ||
| }); | ||
|
|
||
| describeSkipOnWindows('findRelease', () => { | ||
| const result = JSON.stringify(manifestData); | ||
| const releases = JSON.parse(result) as IGraalPyManifestRelease[]; | ||
| const extension = 'tar.gz'; | ||
| const arch = installer.toGraalPyArchitecture(architecture); | ||
| const platform = installer.toGraalPyPlatform(process.platform); | ||
| const extensionName = `${platform}-${arch}.${extension}`; | ||
| const files: IGraalPyManifestAsset = { | ||
| name: `graalpython-23.0.0-${extensionName}`, | ||
| browser_download_url: `https://github.com/oracle/graalpython/releases/download/graal-23.0.0/graalpython-23.0.0-${extensionName}` | ||
| }; | ||
| const filesRC1: IGraalPyManifestAsset = { | ||
| name: `graalpython-23.1.0a1-${extensionName}`, | ||
| browser_download_url: `https://github.com/oracle/graalpython/releases/download/graal-23.1.0a1/graalpython-23.1.0a1-${extensionName}` | ||
| }; | ||
|
|
||
| let warningSpy: jest.SpyInstance; | ||
| let debugSpy: jest.SpyInstance; | ||
| let infoSpy: jest.SpyInstance; | ||
|
|
||
| beforeEach(() => { | ||
| infoSpy = jest.spyOn(core, 'info'); | ||
| infoSpy.mockImplementation(() => {}); | ||
|
|
||
| warningSpy = jest.spyOn(core, 'warning'); | ||
| warningSpy.mockImplementation(() => null); | ||
|
|
||
| debugSpy = jest.spyOn(core, 'debug'); | ||
| debugSpy.mockImplementation(() => null); | ||
| }); | ||
|
|
||
| it("GraalPy version doesn't match", () => { | ||
| const graalpyVersion = '12.0.0'; | ||
| expect( | ||
| installer.findRelease(releases, graalpyVersion, architecture, false) | ||
| ).toEqual(null); | ||
| }); | ||
|
|
||
| it('GraalPy version matches', () => { | ||
| const graalpyVersion = '23.0.0'; | ||
| expect( | ||
| installer.findRelease(releases, graalpyVersion, architecture, false) | ||
| ).toMatchObject({ | ||
| foundAsset: files, | ||
| resolvedGraalPyVersion: graalpyVersion | ||
| }); | ||
| }); | ||
|
|
||
| it('Preview version of GraalPy is found', () => { | ||
| const graalpyVersion = installer.graalPyTagToVersion('vm-23.1.0a1'); | ||
| expect( | ||
| installer.findRelease(releases, graalpyVersion, architecture, false) | ||
| ).toMatchObject({ | ||
| foundAsset: { | ||
| name: `graalpython-23.1.0a1-${extensionName}`, | ||
| browser_download_url: `https://github.com/oracle/graalpython/releases/download/graal-23.1.0a1/graalpython-23.1.0a1-${extensionName}` | ||
| }, | ||
| resolvedGraalPyVersion: '23.1.0-a.1' | ||
| }); | ||
| }); | ||
|
|
||
| it('Latest GraalPy is found', () => { | ||
| const graalpyVersion = 'x'; | ||
| expect( | ||
| installer.findRelease(releases, graalpyVersion, architecture, false) | ||
| ).toMatchObject({ | ||
| foundAsset: files, | ||
| resolvedGraalPyVersion: '23.0.0' | ||
| }); | ||
| }); | ||
|
|
||
| it('GraalPy version matches semver (pre-release)', () => { | ||
| const graalpyVersion = '23.1.x'; | ||
| expect( | ||
| installer.findRelease(releases, graalpyVersion, architecture, false) | ||
| ).toBeNull(); | ||
| expect( | ||
| installer.findRelease(releases, graalpyVersion, architecture, true) | ||
| ).toMatchObject({ | ||
| foundAsset: filesRC1, | ||
| resolvedGraalPyVersion: '23.1.0-a.1' | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describeSkipOnWindows('installGraalPy', () => { | ||
| let tcFind: jest.SpyInstance; | ||
| let warningSpy: jest.SpyInstance; | ||
| let debugSpy: jest.SpyInstance; | ||
| let infoSpy: jest.SpyInstance; | ||
| let spyExtractZip: jest.SpyInstance; | ||
| let spyExtractTar: jest.SpyInstance; | ||
| let spyFsReadDir: jest.SpyInstance; | ||
| let spyFsWriteFile: jest.SpyInstance; | ||
| let spyHttpClient: jest.SpyInstance; | ||
| let spyExistsSync: jest.SpyInstance; | ||
| let spyExec: jest.SpyInstance; | ||
| let spySymlinkSync: jest.SpyInstance; | ||
| let spyDownloadTool: jest.SpyInstance; | ||
| let spyCacheDir: jest.SpyInstance; | ||
| let spyChmodSync: jest.SpyInstance; | ||
|
|
||
| beforeEach(() => { | ||
| tcFind = jest.spyOn(tc, 'find'); | ||
| tcFind.mockImplementation(() => | ||
| path.join('GraalPy', '3.6.12', architecture) | ||
| ); | ||
|
|
||
| spyDownloadTool = jest.spyOn(tc, 'downloadTool'); | ||
| spyDownloadTool.mockImplementation(() => path.join(tempDir, 'GraalPy')); | ||
|
|
||
| spyExtractZip = jest.spyOn(tc, 'extractZip'); | ||
| spyExtractZip.mockImplementation(() => tempDir); | ||
|
|
||
| spyExtractTar = jest.spyOn(tc, 'extractTar'); | ||
| spyExtractTar.mockImplementation(() => tempDir); | ||
|
|
||
| infoSpy = jest.spyOn(core, 'info'); | ||
| infoSpy.mockImplementation(() => {}); | ||
|
|
||
| warningSpy = jest.spyOn(core, 'warning'); | ||
| warningSpy.mockImplementation(() => null); | ||
|
|
||
| debugSpy = jest.spyOn(core, 'debug'); | ||
| debugSpy.mockImplementation(() => null); | ||
|
|
||
| spyFsReadDir = jest.spyOn(fs, 'readdirSync'); | ||
| spyFsReadDir.mockImplementation(() => ['GraalPyTest']); | ||
|
|
||
| spyFsWriteFile = jest.spyOn(fs, 'writeFileSync'); | ||
| spyFsWriteFile.mockImplementation(() => undefined); | ||
|
|
||
| spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); | ||
| spyHttpClient.mockImplementation( | ||
| async (): Promise<ifm.TypedResponse<IGraalPyManifestRelease[]>> => { | ||
| const result = JSON.stringify(manifestData); | ||
| return { | ||
| statusCode: 200, | ||
| headers: {}, | ||
| result: JSON.parse(result) as IGraalPyManifestRelease[] | ||
| }; | ||
| } | ||
| ); | ||
|
|
||
| spyExec = jest.spyOn(exec, 'exec'); | ||
| spyExec.mockImplementation(() => undefined); | ||
|
|
||
| spySymlinkSync = jest.spyOn(fs, 'symlinkSync'); | ||
| spySymlinkSync.mockImplementation(() => undefined); | ||
|
|
||
| spyExistsSync = jest.spyOn(fs, 'existsSync'); | ||
| spyExistsSync.mockImplementation(() => false); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| jest.resetAllMocks(); | ||
| jest.clearAllMocks(); | ||
| jest.restoreAllMocks(); | ||
| }); | ||
|
|
||
| it('throw if release is not found', async () => { | ||
| await expect( | ||
| installer.installGraalPy('7.3.3', architecture, false, undefined) | ||
| ).rejects.toThrow( | ||
| `GraalPy version 7.3.3 with arch ${architecture} not found` | ||
| ); | ||
|
|
||
| expect(spyHttpClient).toHaveBeenCalled(); | ||
| expect(spyDownloadTool).not.toHaveBeenCalled(); | ||
| expect(spyExec).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('found and install GraalPy', async () => { | ||
| spyCacheDir = jest.spyOn(tc, 'cacheDir'); | ||
| spyCacheDir.mockImplementation(() => | ||
| path.join(toolDir, 'GraalPy', '21.3.0', architecture) | ||
| ); | ||
|
|
||
| spyChmodSync = jest.spyOn(fs, 'chmodSync'); | ||
| spyChmodSync.mockImplementation(() => undefined); | ||
|
|
||
| await expect( | ||
| installer.installGraalPy('21.x', architecture, false, undefined) | ||
| ).resolves.toEqual({ | ||
| installDir: path.join(toolDir, 'GraalPy', '21.3.0', architecture), | ||
| resolvedGraalPyVersion: '21.3.0' | ||
| }); | ||
|
|
||
| expect(spyHttpClient).toHaveBeenCalled(); | ||
| expect(spyDownloadTool).toHaveBeenCalled(); | ||
| expect(spyCacheDir).toHaveBeenCalled(); | ||
| expect(spyExec).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('found and install GraalPy, pre-release fallback', async () => { | ||
| spyCacheDir = jest.spyOn(tc, 'cacheDir'); | ||
| spyCacheDir.mockImplementation(() => | ||
| path.join(toolDir, 'GraalPy', '23.1.0', architecture) | ||
| ); | ||
|
|
||
| spyChmodSync = jest.spyOn(fs, 'chmodSync'); | ||
| spyChmodSync.mockImplementation(() => undefined); | ||
|
|
||
| await expect( | ||
| installer.installGraalPy('23.1.x', architecture, false, undefined) | ||
| ).rejects.toThrow(); | ||
| await expect( | ||
| installer.installGraalPy('23.1.x', architecture, true, undefined) | ||
| ).resolves.toEqual({ | ||
| installDir: path.join(toolDir, 'GraalPy', '23.1.0', architecture), | ||
| resolvedGraalPyVersion: '23.1.0-a.1' | ||
| }); | ||
|
|
||
| expect(spyHttpClient).toHaveBeenCalled(); | ||
| expect(spyDownloadTool).toHaveBeenCalled(); | ||
| expect(spyCacheDir).toHaveBeenCalled(); | ||
| expect(spyExec).toHaveBeenCalled(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| import * as path from 'path'; | ||
| import * as graalpyInstall from './install-graalpy'; | ||
| import { | ||
| IS_WINDOWS, | ||
| validateVersion, | ||
| IGraalPyManifestRelease, | ||
| getBinaryDirectory | ||
| } from './utils'; | ||
|
|
||
| import * as semver from 'semver'; | ||
| import * as core from '@actions/core'; | ||
| import * as tc from '@actions/tool-cache'; | ||
|
|
||
| export async function findGraalPyVersion( | ||
| versionSpec: string, | ||
| architecture: string, | ||
| updateEnvironment: boolean, | ||
| checkLatest: boolean, | ||
| allowPreReleases: boolean | ||
| ): Promise<string> { | ||
| let resolvedGraalPyVersion = ''; | ||
| let installDir: string | null; | ||
| let releases: IGraalPyManifestRelease[] | undefined; | ||
|
|
||
| let graalpyVersionSpec = parseGraalPyVersion(versionSpec); | ||
|
|
||
| if (checkLatest) { | ||
| releases = await graalpyInstall.getAvailableGraalPyVersions(); | ||
| if (releases && releases.length > 0) { | ||
| const releaseData = graalpyInstall.findRelease( | ||
| releases, | ||
| graalpyVersionSpec, | ||
| architecture, | ||
| false | ||
| ); | ||
|
|
||
| if (releaseData) { | ||
| core.info(`Resolved as GraalPy ${releaseData.resolvedGraalPyVersion}`); | ||
| graalpyVersionSpec = releaseData.resolvedGraalPyVersion; | ||
| } else { | ||
| core.info( | ||
| `Failed to resolve GraalPy ${graalpyVersionSpec} from manifest` | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| ({installDir, resolvedGraalPyVersion} = findGraalPyToolCache( | ||
| graalpyVersionSpec, | ||
| architecture | ||
| )); | ||
|
|
||
| if (!installDir) { | ||
| ({installDir, resolvedGraalPyVersion} = await graalpyInstall.installGraalPy( | ||
| graalpyVersionSpec, | ||
| architecture, | ||
| allowPreReleases, | ||
| releases | ||
| )); | ||
| } | ||
|
|
||
| const pipDir = IS_WINDOWS ? 'Scripts' : 'bin'; | ||
| const _binDir = path.join(installDir, pipDir); | ||
| const binaryExtension = IS_WINDOWS ? '.exe' : ''; | ||
| const pythonPath = path.join( | ||
| IS_WINDOWS ? installDir : _binDir, | ||
| `python${binaryExtension}` | ||
| ); | ||
| const pythonLocation = getBinaryDirectory(installDir); | ||
| if (updateEnvironment) { | ||
| core.exportVariable('pythonLocation', installDir); | ||
| // https://cmake.org/cmake/help/latest/module/FindPython.html#module:FindPython | ||
| core.exportVariable('Python_ROOT_DIR', installDir); | ||
| // https://cmake.org/cmake/help/latest/module/FindPython2.html#module:FindPython2 | ||
| core.exportVariable('Python2_ROOT_DIR', installDir); | ||
| // https://cmake.org/cmake/help/latest/module/FindPython3.html#module:FindPython3 | ||
| core.exportVariable('Python3_ROOT_DIR', installDir); | ||
| core.exportVariable('PKG_CONFIG_PATH', pythonLocation + '/lib/pkgconfig'); | ||
| core.addPath(pythonLocation); | ||
| core.addPath(_binDir); | ||
| } | ||
| core.setOutput('python-version', 'graalpy' + resolvedGraalPyVersion); | ||
| core.setOutput('python-path', pythonPath); | ||
|
|
||
| return resolvedGraalPyVersion; | ||
| } | ||
|
|
||
| export function findGraalPyToolCache( | ||
| graalpyVersion: string, | ||
| architecture: string | ||
| ) { | ||
| let resolvedGraalPyVersion = ''; | ||
| let installDir: string | null = tc.find( | ||
| 'GraalPy', | ||
| graalpyVersion, | ||
| architecture | ||
| ); | ||
|
|
||
| if (installDir) { | ||
| // 'tc.find' finds tool based on Python version but we also need to check | ||
| // whether GraalPy version satisfies requested version. | ||
| resolvedGraalPyVersion = path.basename(path.dirname(installDir)); | ||
|
|
||
| const isGraalPyVersionSatisfies = semver.satisfies( | ||
| resolvedGraalPyVersion, | ||
| graalpyVersion | ||
| ); | ||
| if (!isGraalPyVersionSatisfies) { | ||
| installDir = null; | ||
| resolvedGraalPyVersion = ''; | ||
| } | ||
| } | ||
|
|
||
| if (!installDir) { | ||
| core.info( | ||
| `GraalPy version ${graalpyVersion} was not found in the local cache` | ||
| ); | ||
| } | ||
|
|
||
| return {installDir, resolvedGraalPyVersion}; | ||
| } | ||
|
|
||
| export function parseGraalPyVersion(versionSpec: string): string { | ||
| const versions = versionSpec.split('-').filter(item => !!item); | ||
|
|
||
| if (/^(graalpy)(.+)/.test(versions[0])) { | ||
| const version = versions[0].replace('graalpy', ''); | ||
| versions.splice(0, 1, 'graalpy', version); | ||
| } | ||
|
|
||
| if (versions.length < 2 || versions[0] != 'graalpy') { | ||
| throw new Error( | ||
| "Invalid 'version' property for GraalPy. GraalPy version should be specified as 'graalpy<python-version>' or 'graalpy-<python-version>'. See README for examples and documentation." | ||
| ); | ||
| } | ||
|
|
||
| const pythonVersion = versions[1]; | ||
|
|
||
| if (!validateVersion(pythonVersion)) { | ||
| throw new Error( | ||
| "Invalid 'version' property for GraalPy. GraalPy versions should satisfy SemVer notation. See README for examples and documentation." | ||
| ); | ||
| } | ||
|
|
||
| return pythonVersion; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| import * as os from 'os'; | ||
| import * as path from 'path'; | ||
| import * as core from '@actions/core'; | ||
| import * as tc from '@actions/tool-cache'; | ||
| import * as semver from 'semver'; | ||
| import * as httpm from '@actions/http-client'; | ||
| import * as ifm from '@actions/http-client/lib/interfaces'; | ||
| import * as exec from '@actions/exec'; | ||
|
|
||
| import fs from 'fs'; | ||
| import * as http from 'http'; | ||
|
|
||
| import { | ||
| IS_WINDOWS, | ||
| IGraalPyManifestRelease, | ||
| createSymlinkInFolder, | ||
| isNightlyKeyword, | ||
| getBinaryDirectory, | ||
| getNextPageUrl | ||
| } from './utils'; | ||
|
|
||
| const TOKEN = core.getInput('token'); | ||
| const AUTH = !TOKEN ? undefined : `token ${TOKEN}`; | ||
|
|
||
| export async function installGraalPy( | ||
| graalpyVersion: string, | ||
| architecture: string, | ||
| allowPreReleases: boolean, | ||
| releases: IGraalPyManifestRelease[] | undefined | ||
| ) { | ||
| let downloadDir; | ||
|
|
||
| releases = releases ?? (await getAvailableGraalPyVersions()); | ||
|
|
||
| if (!releases || !releases.length) { | ||
| throw new Error('No release was found in GraalPy version.json'); | ||
| } | ||
|
|
||
| let releaseData = findRelease(releases, graalpyVersion, architecture, false); | ||
|
|
||
| if (allowPreReleases && (!releaseData || !releaseData.foundAsset)) { | ||
| // check for pre-release | ||
| core.info( | ||
| [ | ||
| `Stable GraalPy version ${graalpyVersion} with arch ${architecture} not found`, | ||
| `Trying pre-release versions` | ||
| ].join(os.EOL) | ||
| ); | ||
| releaseData = findRelease(releases, graalpyVersion, architecture, true); | ||
| } | ||
|
|
||
| if (!releaseData || !releaseData.foundAsset) { | ||
| throw new Error( | ||
| `GraalPy version ${graalpyVersion} with arch ${architecture} not found` | ||
| ); | ||
| } | ||
|
|
||
| const {foundAsset, resolvedGraalPyVersion} = releaseData; | ||
| const downloadUrl = `${foundAsset.browser_download_url}`; | ||
|
|
||
| core.info(`Downloading GraalPy from "${downloadUrl}" ...`); | ||
|
|
||
| try { | ||
| const graalpyPath = await tc.downloadTool(downloadUrl, undefined, AUTH); | ||
|
|
||
| core.info('Extracting downloaded archive...'); | ||
| downloadDir = await tc.extractTar(graalpyPath); | ||
|
|
||
| // root folder in archive can have unpredictable name so just take the first folder | ||
| // downloadDir is unique folder under TEMP and can't contain any other folders | ||
| const archiveName = fs.readdirSync(downloadDir)[0]; | ||
|
|
||
| const toolDir = path.join(downloadDir, archiveName); | ||
| let installDir = toolDir; | ||
| if (!isNightlyKeyword(resolvedGraalPyVersion)) { | ||
| installDir = await tc.cacheDir( | ||
| toolDir, | ||
| 'GraalPy', | ||
| resolvedGraalPyVersion, | ||
| architecture | ||
| ); | ||
| } | ||
|
|
||
| const binaryPath = getBinaryDirectory(installDir); | ||
| await createGraalPySymlink(binaryPath, resolvedGraalPyVersion); | ||
| await installPip(binaryPath); | ||
|
|
||
| return {installDir, resolvedGraalPyVersion}; | ||
| } catch (err) { | ||
| if (err instanceof Error) { | ||
| // Rate limit? | ||
| if ( | ||
| err instanceof tc.HTTPError && | ||
| (err.httpStatusCode === 403 || err.httpStatusCode === 429) | ||
| ) { | ||
| core.info( | ||
| `Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded` | ||
| ); | ||
| } else { | ||
| core.info(err.message); | ||
| } | ||
| if (err.stack !== undefined) { | ||
| core.debug(err.stack); | ||
| } | ||
| } | ||
| throw err; | ||
| } | ||
| } | ||
|
|
||
| export async function getAvailableGraalPyVersions() { | ||
| const http: httpm.HttpClient = new httpm.HttpClient('tool-cache'); | ||
|
|
||
| const headers: http.OutgoingHttpHeaders = {}; | ||
| if (AUTH) { | ||
| headers.authorization = AUTH; | ||
| } | ||
|
|
||
| let url: string | null = | ||
| 'https://api.github.com/repos/oracle/graalpython/releases'; | ||
| const result: IGraalPyManifestRelease[] = []; | ||
| do { | ||
| const response: ifm.TypedResponse<IGraalPyManifestRelease[]> = | ||
| await http.getJson(url, headers); | ||
| if (!response.result) { | ||
| throw new Error( | ||
| `Unable to retrieve the list of available GraalPy versions from '${url}'` | ||
| ); | ||
| } | ||
| result.push(...response.result); | ||
| url = getNextPageUrl(response); | ||
| } while (url); | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| async function createGraalPySymlink( | ||
| graalpyBinaryPath: string, | ||
| graalpyVersion: string | ||
| ) { | ||
| const version = semver.coerce(graalpyVersion)!; | ||
| const pythonBinaryPostfix = semver.major(version); | ||
| const pythonMinor = semver.minor(version); | ||
| const graalpyMajorMinorBinaryPostfix = `${pythonBinaryPostfix}.${pythonMinor}`; | ||
| const binaryExtension = IS_WINDOWS ? '.exe' : ''; | ||
|
|
||
| core.info('Creating symlinks...'); | ||
| createSymlinkInFolder( | ||
| graalpyBinaryPath, | ||
| `graalpy${binaryExtension}`, | ||
| `python${pythonBinaryPostfix}${binaryExtension}`, | ||
| true | ||
| ); | ||
|
|
||
| createSymlinkInFolder( | ||
| graalpyBinaryPath, | ||
| `graalpy${binaryExtension}`, | ||
| `python${binaryExtension}`, | ||
| true | ||
| ); | ||
|
|
||
| createSymlinkInFolder( | ||
| graalpyBinaryPath, | ||
| `graalpy${binaryExtension}`, | ||
| `graalpy${graalpyMajorMinorBinaryPostfix}${binaryExtension}`, | ||
| true | ||
| ); | ||
| } | ||
|
|
||
| async function installPip(pythonLocation: string) { | ||
| core.info( | ||
| "Installing pip (GraalPy doesn't update pip because it uses a patched version of pip)" | ||
| ); | ||
| const pythonBinary = path.join(pythonLocation, 'python'); | ||
| await exec.exec(`${pythonBinary} -m ensurepip --default-pip`); | ||
| } | ||
|
|
||
| export function graalPyTagToVersion(tag: string) { | ||
| const versionPattern = /.*-(\d+\.\d+\.\d+(?:\.\d+)?)((?:a|b|rc))?(\d*)?/; | ||
| const match = tag.match(versionPattern); | ||
| if (match && match[2]) { | ||
| return `${match[1]}-${match[2]}.${match[3]}`; | ||
| } else if (match) { | ||
| return match[1]; | ||
| } else { | ||
| return tag.replace(/.*-/, ''); | ||
| } | ||
| } | ||
|
|
||
| export function findRelease( | ||
| releases: IGraalPyManifestRelease[], | ||
| graalpyVersion: string, | ||
| architecture: string, | ||
| includePrerelease: boolean | ||
| ) { | ||
| const options = {includePrerelease: includePrerelease}; | ||
| const filterReleases = releases.filter(item => { | ||
| const isVersionSatisfied = semver.satisfies( | ||
| graalPyTagToVersion(item.tag_name), | ||
| graalpyVersion, | ||
| options | ||
| ); | ||
| return ( | ||
| isVersionSatisfied && !!findAsset(item, architecture, process.platform) | ||
| ); | ||
| }); | ||
|
|
||
| if (!filterReleases.length) { | ||
| return null; | ||
| } | ||
|
|
||
| const sortedReleases = filterReleases.sort((previous, current) => | ||
| semver.compare( | ||
| semver.coerce(graalPyTagToVersion(current.tag_name))!, | ||
| semver.coerce(graalPyTagToVersion(previous.tag_name))! | ||
| ) | ||
| ); | ||
|
|
||
| const foundRelease = sortedReleases[0]; | ||
| const foundAsset = findAsset(foundRelease, architecture, process.platform); | ||
|
|
||
| return { | ||
| foundAsset, | ||
| resolvedGraalPyVersion: graalPyTagToVersion(foundRelease.tag_name) | ||
| }; | ||
| } | ||
|
|
||
| export function toGraalPyPlatform(platform: string) { | ||
| switch (platform) { | ||
| case 'win32': | ||
| return 'windows'; | ||
| case 'darwin': | ||
| return 'macos'; | ||
| } | ||
| return platform; | ||
| } | ||
|
|
||
| export function toGraalPyArchitecture(architecture: string) { | ||
| switch (architecture) { | ||
| case 'x64': | ||
| return 'amd64'; | ||
| case 'arm64': | ||
| return 'aarch64'; | ||
| } | ||
| return architecture; | ||
| } | ||
|
|
||
| export function findAsset( | ||
| item: IGraalPyManifestRelease, | ||
| architecture: string, | ||
| platform: string | ||
| ) { | ||
| const graalpyArch = toGraalPyArchitecture(architecture); | ||
| const graalpyPlatform = toGraalPyPlatform(platform); | ||
| const found = item.assets.filter( | ||
| file => | ||
| file.name.startsWith('graalpy') && | ||
| file.name.endsWith(`-${graalpyPlatform}-${graalpyArch}.tar.gz`) | ||
| ); | ||
| /* | ||
| In the future there could be more variants of GraalPy for a single release. Pick the shortest name, that one is the most likely to be the primary variant. | ||
| */ | ||
| found.sort((f1, f2) => f1.name.length - f2.name.length); | ||
| return found[0]; | ||
| } |