From 0a40ce6f61952eeee640a8940ea8050407c06cb5 Mon Sep 17 00:00:00 2001
From: Fabio Niephaus <fabio.niephaus@oracle.com>
Date: Wed, 18 Sep 2024 17:17:11 +0200
Subject: [PATCH] Add support for Oracle GraalVM (#501)

* Add support for Oracle GraalVM

* Add support for EA builds of Oracle GraalVM
---
 .github/workflows/e2e-versions.yml            |  22 ++-
 README.md                                     |   2 +
 .../distributors/graalvm-installer.test.ts    | 152 +++++++++++++++
 dist/setup/index.js                           | 170 +++++++++++++++++
 docs/advanced-usage.md                        |  16 ++
 src/distributions/distribution-factory.ts     |   6 +-
 src/distributions/graalvm/installer.ts        | 173 ++++++++++++++++++
 src/distributions/graalvm/models.ts           |  14 ++
 8 files changed, 550 insertions(+), 5 deletions(-)
 create mode 100644 __tests__/distributors/graalvm-installer.test.ts
 create mode 100644 src/distributions/graalvm/installer.ts
 create mode 100644 src/distributions/graalvm/models.ts

diff --git a/.github/workflows/e2e-versions.yml b/.github/workflows/e2e-versions.yml
index 9e88442..045dee1 100644
--- a/.github/workflows/e2e-versions.yml
+++ b/.github/workflows/e2e-versions.yml
@@ -45,11 +45,22 @@ jobs:
             version: 17
           - distribution: oracle
             os: windows-latest
-            version: 20
+            version: 21
           - distribution: oracle
             os: ubuntu-latest
-            version: 20
-
+            version: 21
+          - distribution: graalvm
+            os: macos-latest
+            version: 17
+          - distribution: graalvm
+            os: windows-latest
+            version: 21
+          - distribution: graalvm
+            os: ubuntu-latest
+            version: 21
+          - distribution: graalvm
+            os: ubuntu-latest
+            version: '24-ea'
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -79,7 +90,10 @@ jobs:
         include:
           - distribution: oracle
             os: ubuntu-latest
-            version: '20.0.1'
+            version: '21.0.4'
+          - distribution: graalvm
+            os: ubuntu-latest
+            version: '21.0.4'
           - distribution: dragonwell
             os: ubuntu-latest
             version: '11.0'
diff --git a/README.md b/README.md
index 48992b2..aaf9cce 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,7 @@ Currently, the following distributions are supported:
 | `oracle` | Oracle JDK | [Link](https://www.oracle.com/java/technologies/downloads/) | [Link](https://java.com/freeuselicense)
 | `dragonwell` | Alibaba Dragonwell JDK | [Link](https://dragonwell-jdk.io/) | [Link](https://www.aliyun.com/product/dragonwell/)
 | `sapmachine` | SAP SapMachine JDK/JRE | [Link](https://sapmachine.io/) | [Link](https://github.com/SAP/SapMachine/blob/sapmachine/LICENSE)
+| `graalvm` | Oracle GraalVM | [Link](https://www.graalvm.org/) | [Link](https://www.oracle.com/downloads/licenses/graal-free-license.html)
 
 **NOTE:** The different distributors can provide discrepant list of available versions / supported configurations. Please refer to the official documentation to see the list of supported versions.
 
@@ -259,6 +260,7 @@ In the example above multiple JDKs are installed for the same job. The result af
   - [Oracle](docs/advanced-usage.md#Oracle)
   - [Alibaba Dragonwell](docs/advanced-usage.md#Alibaba-Dragonwell)
   - [SapMachine](docs/advanced-usage.md#SapMachine)
+  - [GraalVM](docs/advanced-usage.md#GraalVM)
 - [Installing custom Java package type](docs/advanced-usage.md#Installing-custom-Java-package-type)
 - [Installing custom Java architecture](docs/advanced-usage.md#Installing-custom-Java-architecture)
 - [Installing custom Java distribution from local file](docs/advanced-usage.md#Installing-Java-from-local-file)
diff --git a/__tests__/distributors/graalvm-installer.test.ts b/__tests__/distributors/graalvm-installer.test.ts
new file mode 100644
index 0000000..ae2db43
--- /dev/null
+++ b/__tests__/distributors/graalvm-installer.test.ts
@@ -0,0 +1,152 @@
+import {GraalVMDistribution} from '../../src/distributions/graalvm/installer';
+import os from 'os';
+import * as core from '@actions/core';
+import {getDownloadArchiveExtension} from '../../src/util';
+import {HttpClient} from '@actions/http-client';
+
+describe('findPackageForDownload', () => {
+  let distribution: GraalVMDistribution;
+  let spyDebug: jest.SpyInstance;
+  let spyHttpClient: jest.SpyInstance;
+
+  beforeEach(() => {
+    distribution = new GraalVMDistribution({
+      version: '',
+      architecture: 'x64',
+      packageType: 'jdk',
+      checkLatest: false
+    });
+
+    spyDebug = jest.spyOn(core, 'debug');
+    spyDebug.mockImplementation(() => {});
+  });
+
+  it.each([
+    [
+      '21',
+      '21',
+      'https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_{{OS_TYPE}}-x64_bin.{{ARCHIVE_TYPE}}'
+    ],
+    [
+      '21.0.4',
+      '21.0.4',
+      'https://download.oracle.com/graalvm/21/archive/graalvm-jdk-21.0.4_{{OS_TYPE}}-x64_bin.{{ARCHIVE_TYPE}}'
+    ],
+    [
+      '17',
+      '17',
+      'https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_{{OS_TYPE}}-x64_bin.{{ARCHIVE_TYPE}}'
+    ],
+    [
+      '17.0.12',
+      '17.0.12',
+      'https://download.oracle.com/graalvm/17/archive/graalvm-jdk-17.0.12_{{OS_TYPE}}-x64_bin.{{ARCHIVE_TYPE}}'
+    ]
+  ])('version is %s -> %s', async (input, expectedVersion, expectedUrl) => {
+    /* Needed only for this particular test because /latest/ urls tend to change */
+    spyHttpClient = jest.spyOn(HttpClient.prototype, 'head');
+    spyHttpClient.mockReturnValue(
+      Promise.resolve({
+        message: {
+          statusCode: 200
+        }
+      })
+    );
+
+    const result = await distribution['findPackageForDownload'](input);
+
+    jest.restoreAllMocks();
+
+    expect(result.version).toBe(expectedVersion);
+    const osType = distribution.getPlatform();
+    const archiveType = getDownloadArchiveExtension();
+    const url = expectedUrl
+      .replace('{{OS_TYPE}}', osType)
+      .replace('{{ARCHIVE_TYPE}}', archiveType);
+    expect(result.url).toBe(url);
+  });
+
+  it.each([
+    [
+      '24-ea',
+      /^https:\/\/github\.com\/graalvm\/oracle-graalvm-ea-builds\/releases\/download\/jdk-24\.0\.0-ea\./
+    ]
+  ])('version is %s -> %s', async (version, expectedUrlPrefix) => {
+    /* Needed only for this particular test because /latest/ urls tend to change */
+    spyHttpClient = jest.spyOn(HttpClient.prototype, 'head');
+    spyHttpClient.mockReturnValue(
+      Promise.resolve({
+        message: {
+          statusCode: 200
+        }
+      })
+    );
+
+    const eaDistro = new GraalVMDistribution({
+      version,
+      architecture: '', // to get default value
+      packageType: 'jdk',
+      checkLatest: false
+    });
+
+    const versionWithoutEA = version.split('-')[0];
+    const result = await eaDistro['findPackageForDownload'](versionWithoutEA);
+
+    jest.restoreAllMocks();
+
+    expect(result.url).toEqual(expect.stringMatching(expectedUrlPrefix));
+  });
+
+  it.each([
+    ['amd64', 'x64'],
+    ['arm64', 'aarch64']
+  ])(
+    'defaults to os.arch(): %s mapped to distro arch: %s',
+    async (osArch: string, distroArch: string) => {
+      jest.spyOn(os, 'arch').mockReturnValue(osArch);
+      jest.spyOn(os, 'platform').mockReturnValue('linux');
+
+      const version = '21';
+      const distro = new GraalVMDistribution({
+        version,
+        architecture: '', // to get default value
+        packageType: 'jdk',
+        checkLatest: false
+      });
+
+      const osType = distribution.getPlatform();
+      if (osType === 'windows' && distroArch == 'aarch64') {
+        return; // skip, aarch64 is not available for Windows
+      }
+      const archiveType = getDownloadArchiveExtension();
+      const result = await distro['findPackageForDownload'](version);
+      const expectedUrl = `https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_${osType}-${distroArch}_bin.${archiveType}`;
+
+      expect(result.url).toBe(expectedUrl);
+    }
+  );
+
+  it('should throw an error', async () => {
+    await expect(distribution['findPackageForDownload']('8')).rejects.toThrow(
+      /GraalVM is only supported for JDK 17 and later/
+    );
+    await expect(distribution['findPackageForDownload']('11')).rejects.toThrow(
+      /GraalVM is only supported for JDK 17 and later/
+    );
+    await expect(distribution['findPackageForDownload']('18')).rejects.toThrow(
+      /Could not find GraalVM for SemVer */
+    );
+
+    const unavailableEADistro = new GraalVMDistribution({
+      version: '17-ea',
+      architecture: '', // to get default value
+      packageType: 'jdk',
+      checkLatest: false
+    });
+    await expect(
+      unavailableEADistro['findPackageForDownload']('17')
+    ).rejects.toThrow(
+      /No GraalVM EA build found\. Are you sure java-version: '17-ea' is correct\?/
+    );
+  });
+});
diff --git a/dist/setup/index.js b/dist/setup/index.js
index 18942fe..28a917e 100644
--- a/dist/setup/index.js
+++ b/dist/setup/index.js
@@ -124050,6 +124050,7 @@ const installer_8 = __nccwpck_require__(34750);
 const installer_9 = __nccwpck_require__(64298);
 const installer_10 = __nccwpck_require__(16132);
 const installer_11 = __nccwpck_require__(52869);
+const installer_12 = __nccwpck_require__(55644);
 var JavaDistribution;
 (function (JavaDistribution) {
     JavaDistribution["Adopt"] = "adopt";
@@ -124065,6 +124066,7 @@ var JavaDistribution;
     JavaDistribution["Oracle"] = "oracle";
     JavaDistribution["Dragonwell"] = "dragonwell";
     JavaDistribution["SapMachine"] = "sapmachine";
+    JavaDistribution["GraalVM"] = "graalvm";
 })(JavaDistribution || (JavaDistribution = {}));
 function getJavaDistribution(distributionName, installerOptions, jdkFile) {
     switch (distributionName) {
@@ -124093,6 +124095,8 @@ function getJavaDistribution(distributionName, installerOptions, jdkFile) {
             return new installer_10.DragonwellDistribution(installerOptions);
         case JavaDistribution.SapMachine:
             return new installer_11.SapMachineDistribution(installerOptions);
+        case JavaDistribution.GraalVM:
+            return new installer_12.GraalVMDistribution(installerOptions);
         default:
             return null;
     }
@@ -124310,6 +124314,172 @@ class DragonwellDistribution extends base_installer_1.JavaBase {
 exports.DragonwellDistribution = DragonwellDistribution;
 
 
+/***/ }),
+
+/***/ 55644:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    var desc = Object.getOwnPropertyDescriptor(m, k);
+    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+      desc = { enumerable: true, get: function() { return m[k]; } };
+    }
+    Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+    Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+    o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+    if (mod && mod.__esModule) return mod;
+    var result = {};
+    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+    __setModuleDefault(result, mod);
+    return result;
+};
+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 __importDefault = (this && this.__importDefault) || function (mod) {
+    return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.GraalVMDistribution = void 0;
+const core = __importStar(__nccwpck_require__(42186));
+const tc = __importStar(__nccwpck_require__(27784));
+const fs_1 = __importDefault(__nccwpck_require__(57147));
+const path_1 = __importDefault(__nccwpck_require__(71017));
+const base_installer_1 = __nccwpck_require__(59741);
+const util_1 = __nccwpck_require__(92629);
+const http_client_1 = __nccwpck_require__(96255);
+const GRAALVM_DL_BASE = 'https://download.oracle.com/graalvm';
+const IS_WINDOWS = process.platform === 'win32';
+const GRAALVM_PLATFORM = IS_WINDOWS ? 'windows' : process.platform;
+class GraalVMDistribution extends base_installer_1.JavaBase {
+    constructor(installerOptions) {
+        super('GraalVM', installerOptions);
+    }
+    downloadTool(javaRelease) {
+        return __awaiter(this, void 0, void 0, function* () {
+            core.info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`);
+            const javaArchivePath = yield tc.downloadTool(javaRelease.url);
+            core.info(`Extracting Java archive...`);
+            const extension = (0, util_1.getDownloadArchiveExtension)();
+            const extractedJavaPath = yield (0, util_1.extractJdkFile)(javaArchivePath, extension);
+            const archiveName = fs_1.default.readdirSync(extractedJavaPath)[0];
+            const archivePath = path_1.default.join(extractedJavaPath, archiveName);
+            const version = this.getToolcacheVersionName(javaRelease.version);
+            const javaPath = yield tc.cacheDir(archivePath, this.toolcacheFolderName, version, this.architecture);
+            return { version: javaRelease.version, path: javaPath };
+        });
+    }
+    findPackageForDownload(range) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const arch = this.distributionArchitecture();
+            if (arch !== 'x64' && arch !== 'aarch64') {
+                throw new Error(`Unsupported architecture: ${this.architecture}`);
+            }
+            if (!this.stable) {
+                return this.findEABuildDownloadUrl(`${range}-ea`);
+            }
+            if (this.packageType !== 'jdk') {
+                throw new Error('GraalVM provides only the `jdk` package type');
+            }
+            const platform = this.getPlatform();
+            const extension = (0, util_1.getDownloadArchiveExtension)();
+            let major;
+            let fileUrl;
+            if (range.includes('.')) {
+                major = range.split('.')[0];
+                fileUrl = `${GRAALVM_DL_BASE}/${major}/archive/graalvm-jdk-${range}_${platform}-${arch}_bin.${extension}`;
+            }
+            else {
+                major = range;
+                fileUrl = `${GRAALVM_DL_BASE}/${range}/latest/graalvm-jdk-${range}_${platform}-${arch}_bin.${extension}`;
+            }
+            if (parseInt(major) < 17) {
+                throw new Error('GraalVM is only supported for JDK 17 and later');
+            }
+            const response = yield this.http.head(fileUrl);
+            if (response.message.statusCode === http_client_1.HttpCodes.NotFound) {
+                throw new Error(`Could not find GraalVM for SemVer ${range}`);
+            }
+            if (response.message.statusCode !== http_client_1.HttpCodes.OK) {
+                throw new Error(`Http request for GraalVM failed with status code: ${response.message.statusCode}`);
+            }
+            return { url: fileUrl, version: range };
+        });
+    }
+    findEABuildDownloadUrl(javaEaVersion) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const versions = yield this.fetchEAJson(javaEaVersion);
+            const latestVersion = versions.find(v => v.latest);
+            if (!latestVersion) {
+                throw new Error(`Unable to find latest version for '${javaEaVersion}'`);
+            }
+            const arch = this.distributionArchitecture();
+            const file = latestVersion.files.find(f => f.arch === arch && f.platform === GRAALVM_PLATFORM);
+            if (!file || !file.filename.startsWith('graalvm-jdk-')) {
+                throw new Error(`Unable to find file metadata for '${javaEaVersion}'`);
+            }
+            return {
+                url: `${latestVersion.download_base_url}${file.filename}`,
+                version: latestVersion.version
+            };
+        });
+    }
+    fetchEAJson(javaEaVersion) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const owner = 'graalvm';
+            const repository = 'oracle-graalvm-ea-builds';
+            const branch = 'main';
+            const filePath = `versions/${javaEaVersion}.json`;
+            const url = `https://api.github.com/repos/${owner}/${repository}/contents/${filePath}?ref=${branch}`;
+            const headers = (0, util_1.getGitHubHttpHeaders)();
+            core.debug(`Trying to fetch available version info for GraalVM EA builds from '${url}'`);
+            let fetchedJson;
+            try {
+                fetchedJson = (yield this.http.getJson(url, headers))
+                    .result;
+            }
+            catch (err) {
+                throw Error(`Fetching version info for GraalVM EA builds from '${url}' failed with the error: ${err.message}`);
+            }
+            if (fetchedJson === null) {
+                throw Error(`No GraalVM EA build found. Are you sure java-version: '${javaEaVersion}' is correct?`);
+            }
+            return fetchedJson;
+        });
+    }
+    getPlatform(platform = process.platform) {
+        switch (platform) {
+            case 'darwin':
+                return 'macos';
+            case 'win32':
+                return 'windows';
+            case 'linux':
+                return 'linux';
+            default:
+                throw new Error(`Platform '${platform}' is not supported. Supported platforms: 'linux', 'macos', 'windows'`);
+        }
+    }
+}
+exports.GraalVMDistribution = GraalVMDistribution;
+
+
 /***/ }),
 
 /***/ 40883:
diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md
index 248b4be..efc443b 100644
--- a/docs/advanced-usage.md
+++ b/docs/advanced-usage.md
@@ -9,6 +9,7 @@
   - [Oracle](#Oracle)
   - [Alibaba Dragonwell](#Alibaba-Dragonwell)
   - [SapMachine](#SapMachine)
+  - [GraalVM](#GraalVM)
 - [Installing custom Java package type](#Installing-custom-Java-package-type)
 - [Installing custom Java architecture](#Installing-custom-Java-architecture)
 - [Installing custom Java distribution from local file](#Installing-Java-from-local-file)
@@ -155,6 +156,21 @@ steps:
 - run: java -cp java HelloWorldApp
 ```
 
+### GraalVM
+**NOTE:** Oracle GraalVM is only available for JDK 17 and later.
+
+```yaml
+steps:
+- uses: actions/checkout@v4
+- uses: actions/setup-java@v4
+  with:
+    distribution: 'graalvm'
+    java-version: '21'
+- run: |
+    java -cp java HelloWorldApp
+    native-image -cp java HelloWorldApp
+```
+
 ## Installing custom Java package type
 ```yaml
 steps:
diff --git a/src/distributions/distribution-factory.ts b/src/distributions/distribution-factory.ts
index fb673dc..563c9a8 100644
--- a/src/distributions/distribution-factory.ts
+++ b/src/distributions/distribution-factory.ts
@@ -11,6 +11,7 @@ import {CorrettoDistribution} from './corretto/installer';
 import {OracleDistribution} from './oracle/installer';
 import {DragonwellDistribution} from './dragonwell/installer';
 import {SapMachineDistribution} from './sapmachine/installer';
+import {GraalVMDistribution} from './graalvm/installer';
 
 enum JavaDistribution {
   Adopt = 'adopt',
@@ -25,7 +26,8 @@ enum JavaDistribution {
   Corretto = 'corretto',
   Oracle = 'oracle',
   Dragonwell = 'dragonwell',
-  SapMachine = 'sapmachine'
+  SapMachine = 'sapmachine',
+  GraalVM = 'graalvm'
 }
 
 export function getJavaDistribution(
@@ -68,6 +70,8 @@ export function getJavaDistribution(
       return new DragonwellDistribution(installerOptions);
     case JavaDistribution.SapMachine:
       return new SapMachineDistribution(installerOptions);
+    case JavaDistribution.GraalVM:
+      return new GraalVMDistribution(installerOptions);
     default:
       return null;
   }
diff --git a/src/distributions/graalvm/installer.ts b/src/distributions/graalvm/installer.ts
new file mode 100644
index 0000000..37dbe32
--- /dev/null
+++ b/src/distributions/graalvm/installer.ts
@@ -0,0 +1,173 @@
+import * as core from '@actions/core';
+import * as tc from '@actions/tool-cache';
+
+import fs from 'fs';
+import path from 'path';
+
+import {JavaBase} from '../base-installer';
+import {
+  JavaDownloadRelease,
+  JavaInstallerOptions,
+  JavaInstallerResults
+} from '../base-models';
+import {
+  extractJdkFile,
+  getDownloadArchiveExtension,
+  getGitHubHttpHeaders
+} from '../../util';
+import {HttpCodes} from '@actions/http-client';
+import {GraalVMEAVersion} from './models';
+
+const GRAALVM_DL_BASE = 'https://download.oracle.com/graalvm';
+const IS_WINDOWS = process.platform === 'win32';
+const GRAALVM_PLATFORM = IS_WINDOWS ? 'windows' : process.platform;
+
+export class GraalVMDistribution extends JavaBase {
+  constructor(installerOptions: JavaInstallerOptions) {
+    super('GraalVM', installerOptions);
+  }
+
+  protected async downloadTool(
+    javaRelease: JavaDownloadRelease
+  ): Promise<JavaInstallerResults> {
+    core.info(
+      `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`
+    );
+    const javaArchivePath = await tc.downloadTool(javaRelease.url);
+
+    core.info(`Extracting Java archive...`);
+    const extension = getDownloadArchiveExtension();
+
+    const extractedJavaPath = await extractJdkFile(javaArchivePath, extension);
+
+    const archiveName = fs.readdirSync(extractedJavaPath)[0];
+    const archivePath = path.join(extractedJavaPath, archiveName);
+    const version = this.getToolcacheVersionName(javaRelease.version);
+
+    const javaPath = await tc.cacheDir(
+      archivePath,
+      this.toolcacheFolderName,
+      version,
+      this.architecture
+    );
+
+    return {version: javaRelease.version, path: javaPath};
+  }
+
+  protected async findPackageForDownload(
+    range: string
+  ): Promise<JavaDownloadRelease> {
+    const arch = this.distributionArchitecture();
+    if (arch !== 'x64' && arch !== 'aarch64') {
+      throw new Error(`Unsupported architecture: ${this.architecture}`);
+    }
+
+    if (!this.stable) {
+      return this.findEABuildDownloadUrl(`${range}-ea`);
+    }
+
+    if (this.packageType !== 'jdk') {
+      throw new Error('GraalVM provides only the `jdk` package type');
+    }
+
+    const platform = this.getPlatform();
+    const extension = getDownloadArchiveExtension();
+    let major;
+    let fileUrl;
+    if (range.includes('.')) {
+      major = range.split('.')[0];
+      fileUrl = `${GRAALVM_DL_BASE}/${major}/archive/graalvm-jdk-${range}_${platform}-${arch}_bin.${extension}`;
+    } else {
+      major = range;
+      fileUrl = `${GRAALVM_DL_BASE}/${range}/latest/graalvm-jdk-${range}_${platform}-${arch}_bin.${extension}`;
+    }
+
+    if (parseInt(major) < 17) {
+      throw new Error('GraalVM is only supported for JDK 17 and later');
+    }
+
+    const response = await this.http.head(fileUrl);
+
+    if (response.message.statusCode === HttpCodes.NotFound) {
+      throw new Error(`Could not find GraalVM for SemVer ${range}`);
+    }
+
+    if (response.message.statusCode !== HttpCodes.OK) {
+      throw new Error(
+        `Http request for GraalVM failed with status code: ${response.message.statusCode}`
+      );
+    }
+
+    return {url: fileUrl, version: range};
+  }
+
+  private async findEABuildDownloadUrl(
+    javaEaVersion: string
+  ): Promise<JavaDownloadRelease> {
+    const versions = await this.fetchEAJson(javaEaVersion);
+    const latestVersion = versions.find(v => v.latest);
+    if (!latestVersion) {
+      throw new Error(`Unable to find latest version for '${javaEaVersion}'`);
+    }
+    const arch = this.distributionArchitecture();
+    const file = latestVersion.files.find(
+      f => f.arch === arch && f.platform === GRAALVM_PLATFORM
+    );
+    if (!file || !file.filename.startsWith('graalvm-jdk-')) {
+      throw new Error(`Unable to find file metadata for '${javaEaVersion}'`);
+    }
+    return {
+      url: `${latestVersion.download_base_url}${file.filename}`,
+      version: latestVersion.version
+    };
+  }
+
+  private async fetchEAJson(
+    javaEaVersion: string
+  ): Promise<GraalVMEAVersion[]> {
+    const owner = 'graalvm';
+    const repository = 'oracle-graalvm-ea-builds';
+    const branch = 'main';
+    const filePath = `versions/${javaEaVersion}.json`;
+
+    const url = `https://api.github.com/repos/${owner}/${repository}/contents/${filePath}?ref=${branch}`;
+
+    const headers = getGitHubHttpHeaders();
+
+    core.debug(
+      `Trying to fetch available version info for GraalVM EA builds from '${url}'`
+    );
+    let fetchedJson;
+    try {
+      fetchedJson = (await this.http.getJson<GraalVMEAVersion[]>(url, headers))
+        .result;
+    } catch (err) {
+      throw Error(
+        `Fetching version info for GraalVM EA builds from '${url}' failed with the error: ${
+          (err as Error).message
+        }`
+      );
+    }
+    if (fetchedJson === null) {
+      throw Error(
+        `No GraalVM EA build found. Are you sure java-version: '${javaEaVersion}' is correct?`
+      );
+    }
+    return fetchedJson;
+  }
+
+  public getPlatform(platform: NodeJS.Platform = process.platform): OsVersions {
+    switch (platform) {
+      case 'darwin':
+        return 'macos';
+      case 'win32':
+        return 'windows';
+      case 'linux':
+        return 'linux';
+      default:
+        throw new Error(
+          `Platform '${platform}' is not supported. Supported platforms: 'linux', 'macos', 'windows'`
+        );
+    }
+  }
+}
diff --git a/src/distributions/graalvm/models.ts b/src/distributions/graalvm/models.ts
new file mode 100644
index 0000000..2ae1e4d
--- /dev/null
+++ b/src/distributions/graalvm/models.ts
@@ -0,0 +1,14 @@
+export type OsVersions = 'linux' | 'macos' | 'windows';
+
+export interface GraalVMEAFile {
+  filename: string;
+  arch: 'aarch64' | 'x64';
+  platform: 'darwin' | 'linux' | 'windows';
+}
+
+export interface GraalVMEAVersion {
+  version: string;
+  latest?: boolean;
+  download_base_url: string;
+  files: GraalVMEAFile[];
+}