diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..f61a2cc --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,19 @@ +import * as otypes from '@octokit/types' +import {homedir} from 'os' +import {join} from 'path' + +export const IS_MACOS = process.platform === 'darwin' +export const IS_WINDOWS = process.platform === 'win32' + +export const VERSION_LATEST = 'latest' +export const VERSION_NIGHTLY = 'nightly' +export const VERSION_TRUNK = 'trunk' + +export const GRAALVM_BASE = join(homedir(), '.graalvm') +export const GRAALVM_FILE_EXTENSION = IS_WINDOWS ? '.zip' : '.tar.gz' +export const GRAALVM_GH_USER = 'graalvm' +export const GRAALVM_PLATFORM = IS_WINDOWS ? 'windows' : process.platform +export const JDK_HOME_SUFFIX = IS_MACOS ? '/Contents/Home' : '' + +export type LatestReleaseResponse = + otypes.Endpoints['GET /repos/{owner}/{repo}/releases/latest']['response'] diff --git a/src/graalvm-trunk.ts b/src/graalvm-trunk.ts new file mode 100644 index 0000000..e5d7680 --- /dev/null +++ b/src/graalvm-trunk.ts @@ -0,0 +1,103 @@ +import * as c from './constants' +import * as core from '@actions/core' +import * as tc from '@actions/tool-cache' +import {SpawnSyncOptionsWithStringEncoding, spawnSync} from 'child_process' +import {mkdirP, mv} from '@actions/io' +import {findJavaHomeInSubfolder} from './utils' +import {join} from 'path' + +const GRAALVM_TRUNK_DL = + 'https://github.com/oracle/graal/archive/refs/heads/master.zip' +const GRAALVM_MX_DL = + 'https://github.com/graalvm/mx/archive/refs/heads/master.zip' +const DEFAULT_SUITES = '/compiler,/regex,/sdk,/tools,/truffle' +const GRAAL_REPO_DIR = join(c.GRAALVM_BASE, 'graal') +const VM_DIR = join(GRAAL_REPO_DIR, 'vm') +const MX_DIR = join(c.GRAALVM_BASE, 'mx') +const MX_EXEC = c.IS_WINDOWS ? 'mx.cmd' : 'mx' +const SPAWN_OPTIONS: SpawnSyncOptionsWithStringEncoding = { + cwd: VM_DIR, + encoding: 'utf8', + stdio: 'inherit' +} + +const COMPONENTS_TO_SUITE_NAME = new Map([ + ['espresso', '/espresso'], + ['js', '/graal-js'], + ['llvm-toolchain', '/sulong'], + ['native-image', '/substratevm'], + ['nodejs', '/graal-nodejs'], + ['python', 'graalpython'], + ['R', 'fastr'], + ['ruby', 'truffleruby'], + ['wasm', '/wasm'] +]) + +export async function setUpGraalVMTrunk( + javaVersion: string, + components: string[] +): Promise { + const jdkId = `labsjdk-ce-${javaVersion}` + + core.startGroup(`Downloading GraalVM sources, mx, and ${jdkId}...`) + + await tc.extractZip(await tc.downloadTool(GRAALVM_TRUNK_DL), c.GRAALVM_BASE) + await mv(join(c.GRAALVM_BASE, 'graal-master'), GRAAL_REPO_DIR) + + await tc.extractZip(await tc.downloadTool(GRAALVM_MX_DL), c.GRAALVM_BASE) + await mv(join(c.GRAALVM_BASE, 'mx-master'), MX_DIR) + core.addPath(MX_DIR) + core.debug(`"${MX_DIR}" added to $PATH`) + + const labsJDKDir = join(c.GRAALVM_BASE, 'labsjdk') + await mkdirP(labsJDKDir) + spawnSync( + MX_EXEC, + ['--java-home=', 'fetch-jdk', '--jdk-id', jdkId, '--to', labsJDKDir], + SPAWN_OPTIONS + ) + const labsJDKHome = findJavaHomeInSubfolder(labsJDKDir) + core.exportVariable('JAVA_HOME', labsJDKHome) + core.debug(`$JAVA_HOME set to "${labsJDKHome}"`) + + core.endGroup() + + const dynamicImports = toSuiteNames(components).join(',') + const mxArgs = [ + '--no-download-progress', // avoid cluttering the build log + '--disable-installables=true', // installables not needed + '--force-bash-launchers=true', // disable native launchers + '--exclude-components=LibGraal', // avoid building libgraal to save time + '--dynamicimports', + dynamicImports + ] + if (core.isDebug()) { + spawnSync(MX_EXEC, mxArgs.concat('graalvm-show'), SPAWN_OPTIONS) + } + const graalvmHome = spawnSync(MX_EXEC, mxArgs.concat(['graalvm-home']), { + ...SPAWN_OPTIONS, + stdio: 'pipe' + }) + core.startGroup('Building GraalVM CE from source...') + spawnSync(MX_EXEC, mxArgs.concat(['build']), SPAWN_OPTIONS) + core.endGroup() + const graalvmHomePath = graalvmHome.stdout.trim() + if (core.isDebug()) { + const cmd = c.IS_WINDOWS ? 'dir' : 'ls' + spawnSync(cmd, [graalvmHomePath], {stdio: 'inherit'}) + } + return graalvmHomePath +} + +function toSuiteNames(components: string[]): string[] { + const names = [DEFAULT_SUITES] + for (const component of components) { + const suiteName = COMPONENTS_TO_SUITE_NAME.get(component) + if (suiteName) { + names.push(suiteName) + } else { + throw new Error(`Unsupported component: ${component}`) + } + } + return names +} diff --git a/src/graalvm.ts b/src/graalvm.ts new file mode 100644 index 0000000..38a3489 --- /dev/null +++ b/src/graalvm.ts @@ -0,0 +1,51 @@ +import * as c from './constants' +import {downloadAndExtractJDK, getLatestRelease} from './utils' + +const GRAALVM_CE_DL_BASE = + 'https://github.com/graalvm/graalvm-ce-builds/releases/download' +const GRAALVM_REPO_NIGHTLY = 'graalvm-ce-dev-builds' +const GRAALVM_REPO_RELEASES = 'graalvm-ce-builds' +const GRAALVM_TAG_PREFIX = 'vm-' + +export async function setUpGraalVMLatest(javaVersion: string): Promise { + const latestRelease = await getLatestRelease(GRAALVM_REPO_RELEASES) + const tag_name = latestRelease.tag_name + if (tag_name.startsWith(GRAALVM_TAG_PREFIX)) { + const latestVersion = tag_name.substring( + GRAALVM_TAG_PREFIX.length, + tag_name.length + ) + return setUpGraalVMRelease(latestVersion, javaVersion) + } + throw new Error(`Could not find latest GraalVM release: ${tag_name}`) +} + +export async function setUpGraalVMNightly( + javaVersion: string +): Promise { + const latestNightly = await getLatestRelease(GRAALVM_REPO_NIGHTLY) + const graalVMIdentifier = determineGraalVMIdentifier('dev', javaVersion) + const expectedFileName = `${graalVMIdentifier}${c.GRAALVM_FILE_EXTENSION}` + for (const asset of latestNightly.assets) { + if (asset.name === expectedFileName) { + return downloadAndExtractJDK(asset.browser_download_url) + } + } + throw new Error('Could not find GraalVM nightly build') +} + +export async function setUpGraalVMRelease( + version: string, + javaVersion: string +): Promise { + const graalVMIdentifier = determineGraalVMIdentifier(version, javaVersion) + const downloadUrl = `${GRAALVM_CE_DL_BASE}/${GRAALVM_TAG_PREFIX}${version}/${graalVMIdentifier}${c.GRAALVM_FILE_EXTENSION}` + return downloadAndExtractJDK(downloadUrl) +} + +function determineGraalVMIdentifier( + version: string, + javaVersion: string +): string { + return `graalvm-ce-java${javaVersion}-${c.GRAALVM_PLATFORM}-amd64-${version}` +} diff --git a/src/main.ts b/src/main.ts index efee84f..637a4bb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,48 @@ +import * as c from './constants' import * as core from '@actions/core' +import * as graalvm from './graalvm' +import {join} from 'path' +import {mkdirP} from '@actions/io' +import {setUpGraalVMTrunk} from './graalvm-trunk' async function run(): Promise { try { + const graalvmVersion: string = core.getInput('version', {required: true}) + const javaVersion: string = core.getInput('java-version', {required: true}) + const componentsString: string = core.getInput('components') + const components: string[] = + componentsString.length > 0 ? componentsString.split(',') : [] + const setJavaHome = core.getInput('set-java-home') === 'true' + + await mkdirP(c.GRAALVM_BASE) + + // Download or build GraalVM let graalVMHome + switch (graalvmVersion) { + case c.VERSION_LATEST: + graalVMHome = await graalvm.setUpGraalVMLatest(javaVersion) + break + case c.VERSION_NIGHTLY: + graalVMHome = await graalvm.setUpGraalVMNightly(javaVersion) + break + case c.VERSION_TRUNK: + graalVMHome = await setUpGraalVMTrunk(javaVersion, components) + break + default: + graalVMHome = await graalvm.setUpGraalVMRelease( + graalvmVersion, + javaVersion + ) + break + } + + // Activate GraalVM + core.debug(`Activating GraalVM located at '${graalVMHome}'...`) core.exportVariable('GRAALVM_HOME', graalVMHome) + core.addPath(join(graalVMHome, 'bin')) + if (setJavaHome) { + core.exportVariable('JAVA_HOME', graalVMHome) + } } catch (error) { if (error instanceof Error) core.setFailed(error.message) } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..d70df3b --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,55 @@ +import * as c from './constants' +import * as core from '@actions/core' +import * as httpClient from '@actions/http-client' +import * as tc from '@actions/tool-cache' +import {Octokit} from '@octokit/core' +import {join} from 'path' +import {readdirSync} from 'fs' + +// Set up Octokit in the same way as @actions/github (see https://git.io/Jy9YP) +const baseUrl = process.env['GITHUB_API_URL'] || 'https://api.github.com' +const GitHub = Octokit.defaults({ + baseUrl, + request: { + agent: new httpClient.HttpClient().getAgent(baseUrl) + } +}) + +export async function getLatestRelease( + repo: string +): Promise { + const githubToken = core.getInput('github-token') + const options = githubToken.length > 0 ? {auth: githubToken} : {} + const octokit = new GitHub(options) + return ( + await octokit.request('GET /repos/{owner}/{repo}/releases/latest', { + owner: c.GRAALVM_GH_USER, + repo + }) + ).data +} + +export async function downloadAndExtractJDK( + downloadUrl: string +): Promise { + const downloadPath = await tc.downloadTool(downloadUrl) + if (downloadUrl.endsWith('.tar.gz')) { + await tc.extractTar(downloadPath, c.GRAALVM_BASE) + } else if (downloadUrl.endsWith('.zip')) { + await tc.extractZip(downloadPath, c.GRAALVM_BASE) + } else { + throw new Error(`Unexpected filetype downloaded: ${downloadUrl}`) + } + return findJavaHomeInSubfolder(c.GRAALVM_BASE) +} + +export function findJavaHomeInSubfolder(searchPath: string): string { + const baseContents = readdirSync(searchPath) + if (baseContents.length === 1) { + return join(searchPath, baseContents[0], c.JDK_HOME_SUFFIX) + } else { + throw new Error( + `Unexpected amount of directory items found: ${baseContents.length}` + ) + } +}