diff --git a/LICENSE.dependencies.txt b/LICENSE.dependencies.txt index 240688b5..8441676e 100644 --- a/LICENSE.dependencies.txt +++ b/LICENSE.dependencies.txt @@ -12,7 +12,7 @@ license-checker --summary --production ``` ``` -├─ MIT: 4 +├─ MIT: 5 ├─ ISC: 1 └─ Apache-2.0: 1 ``` \ No newline at end of file diff --git a/README.md b/README.md index 731b78cc..d77fc60c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The package will be usable from any environment (locally, GitHub Actions, etc). - [x] setup cmake - [x] setup ninja -- [ ] setup llvm +- [x] setup llvm - [ ] setup gcc/mingw - [ ] setup msvc - [ ] setup conan diff --git a/action.yml b/action.yml index 150a8661..2dceee2e 100644 --- a/action.yml +++ b/action.yml @@ -3,12 +3,12 @@ description: "Install all the tools required for building and testing C++/C proj author: "Amin Yahyaabadi" inputs: - compiler: - description: "The compiler and its version." - required: false architecture: description: "The CPU architecture" required: false + llvm: + description: "The llvm version to install" + required: false cmake: description: "The cmake version to install." default: "3.20.2" diff --git a/package.json b/package.json index 23caccde..a4126aab 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,12 @@ }, "prettier": "prettier-config-atomic", "dependencies": { + "@actions/exec": "^1.1.0", "@actions/core": "^1.5.0", "@actions/io": "^1.1.1", "@actions/tool-cache": "^1.7.1", - "semver": "^7.3.5", - "hasha": "^5.2.2" + "hasha": "^5.2.2", + "semver": "^7.3.5" }, "devDependencies": { "@types/jest": "^27.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77ecec8c..016a850b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,7 @@ importers: .: specifiers: '@actions/core': ^1.5.0 + '@actions/exec': ^1.1.0 '@actions/io': ^1.1.1 '@actions/tool-cache': ^1.7.1 '@types/jest': ^27.0.1 @@ -24,6 +25,7 @@ importers: typescript: ^4.4.3 dependencies: '@actions/core': 1.5.0 + '@actions/exec': 1.1.0 '@actions/io': 1.1.1 '@actions/tool-cache': 1.7.1 hasha: 5.2.2 @@ -8112,6 +8114,7 @@ packages: /type-fest/0.8.1: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + dev: false /typedarray-to-buffer/3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} diff --git a/src/llvm/__tests__/llvm.test.ts b/src/llvm/__tests__/llvm.test.ts new file mode 100644 index 00000000..bc18dddf --- /dev/null +++ b/src/llvm/__tests__/llvm.test.ts @@ -0,0 +1,31 @@ +import { getSpecificVersionAndUrl } from "../llvm" +import * as https from "https" + +jest.setTimeout(100000) + +function testUrl(version: string) { + const [specificVersion, url] = getSpecificVersionAndUrl(process.platform, version) + + const input = `Version: ${version} => ${specificVersion} \n URL: ${url}` + + return new Promise((resolve, reject) => { + https.get(url, (res) => { + const report = `${input}\nStatus: ${res.statusCode}\nContent-Length: ${res.headers["content-length"]}` + if (res.statusCode !== undefined && res.statusCode >= 200 && res.statusCode <= 399) { + resolve(report) + } else { + reject(new Error(`Failed to download LLVM and Clang binaries.\n${input}\n${report}`)) + } + }) + }) +} + +describe("setup-llvm", () => { + it("Finds valid LLVM URLs", async () => { + await Promise.all( + ["12.0.0", "12", "11", "11.0.0", "10", "10.0.0", "9.0.0", "8.0.0", "7.0.0", "6", "6.0.0", "5", "5.0.0", "4"].map( + (version) => testUrl(version) + ) + ) + }) +}) diff --git a/src/llvm/llvm.ts b/src/llvm/llvm.ts new file mode 100644 index 00000000..f4e9a38a --- /dev/null +++ b/src/llvm/llvm.ts @@ -0,0 +1,303 @@ +import * as core from "@actions/core" +import * as exec from "@actions/exec" +import * as io from "@actions/io" +import * as tc from "@actions/tool-cache" +import * as path from "path" +import semverLte from "semver/functions/lte" + +//================================================ +// Version +//================================================ + +/** + * Gets the specific and minimum LLVM versions that can be used to refer to the supplied specific LLVM versions (e.g., + * `3`, `3.5`, `3.5.2` for `3.5.2`). + */ +function getVersions(specific: string[]): Set { + const versions = new Set(specific) + + for (const version of specific) { + versions.add(/^\d+/.exec(version)![0]) + versions.add(/^\d+\.\d+/.exec(version)![0]) + } + + return versions +} + +/** The specific and minimum LLVM versions supported by this action. */ +const VERSIONS: Set = getVersions([ + "3.5.0", + "3.5.1", + "3.5.2", + "3.6.0", + "3.6.1", + "3.6.2", + "3.7.0", + "3.7.1", + "3.8.0", + "3.8.1", + "3.9.0", + "3.9.1", + "4.0.0", + "4.0.1", + "5.0.0", + "5.0.1", + "5.0.2", + "6.0.0", + "6.0.1", + "7.0.0", + "7.0.1", + "7.1.0", + "8.0.0", + "8.0.1", + "9.0.0", + "9.0.1", + "10.0.0", + "10.0.1", + "11.0.0", + "11.0.1", + "11.1.0", + "12.0.0", + "12.0.1", +]) + +/** + * Gets the specific LLVM versions supported by this action compatible with the supplied (specific or minimum) LLVM + * version in descending order of release (e.g., `5.0.2`, `5.0.1`, and `5.0.0` for `5`). + */ +function getSpecificVersions(version: string): string[] { + return Array.from(VERSIONS) + .filter((v) => /^\d+\.\d+\.\d+$/.test(v) && v.startsWith(version)) + .sort() + .reverse() +} + +//================================================ +// URL +//================================================ + +/** Gets a LLVM download URL for GitHub. */ +function getGitHubUrl(version: string, prefix: string, suffix: string): string { + const file = `${prefix}${version}${suffix}` + return `https://github.com/llvm/llvm-project/releases/download/llvmorg-${version}/${file}` +} + +/** Gets a LLVM download URL for https://releases.llvm.org. */ +function getReleaseUrl(version: string, prefix: string, suffix: string): string { + const file = `${prefix}${version}${suffix}` + return `https://releases.llvm.org/${version}/${file}` +} + +/** The LLVM versions that were never released for the Darwin platform. */ +const DARWIN_MISSING: Set = new Set([ + "3.5.1", + "3.6.1", + "3.6.2", + "3.7.1", + "3.8.1", + "3.9.1", + "6.0.1", + "7.0.1", + "7.1.0", + "8.0.1", + "11.0.1", + "11.1.0", + "12.0.1", +]) + +/** Gets an LLVM download URL for the Darwin platform. */ +function getDarwinUrl(version: string): string | null { + if (DARWIN_MISSING.has(version)) { + return null + } + + const darwin = version === "9.0.0" ? "-darwin-apple" : "-apple-darwin" + const prefix = "clang+llvm-" + const suffix = `-x86_64${darwin}.tar.xz` + if (semverLte(version, "9.0.1")) { + return getReleaseUrl(version, prefix, suffix) + } else { + return getGitHubUrl(version, prefix, suffix) + } +} + +/** + * The LLVM versions that should use the last RC version instead of the release version for the Linux (Ubuntu) platform. + * This is useful when there were binaries released for the Linux (Ubuntu) platform for the last RC version but not for + * the actual release version. + */ +const UBUNTU_RC: Map = new Map([["12.0.1", "12.0.1-rc4"]]) + +/** The (latest) Ubuntu versions for each LLVM version. */ +const UBUNTU: { [key: string]: string } = { + "3.5.0": "-ubuntu-14.04", + "3.5.1": "", + "3.5.2": "-ubuntu-14.04", + "3.6.0": "-ubuntu-14.04", + "3.6.1": "-ubuntu-14.04", + "3.6.2": "-ubuntu-14.04", + "3.7.0": "-ubuntu-14.04", + "3.7.1": "-ubuntu-14.04", + "3.8.0": "-ubuntu-16.04", + "3.8.1": "-ubuntu-16.04", + "3.9.0": "-ubuntu-16.04", + "3.9.1": "-ubuntu-16.04", + "4.0.0": "-ubuntu-16.04", + "5.0.0": "-ubuntu16.04", + "5.0.1": "-ubuntu-16.04", + "5.0.2": "-ubuntu-16.04", + "6.0.0": "-ubuntu-16.04", + "6.0.1": "-ubuntu-16.04", + "7.0.0": "-ubuntu-16.04", + "7.0.1": "-ubuntu-18.04", + "7.1.0": "-ubuntu-14.04", + "8.0.0": "-ubuntu-18.04", + "9.0.0": "-ubuntu-18.04", + "9.0.1": "-ubuntu-16.04", + "10.0.0": "-ubuntu-18.04", + "10.0.1": "-ubuntu-16.04", + "11.0.0": "-ubuntu-20.04", + "11.0.1": "-ubuntu-16.04", + "11.1.0": "-ubuntu-16.04", + "12.0.0": "-ubuntu-20.04", + "12.0.1-rc4": "-ubuntu-21.04", +} + +/** The latest supported LLVM version for the Linux (Ubuntu) platform. */ +const MAX_UBUNTU: string = "12.0.1-rc4" + +/** Gets an LLVM download URL for the Linux (Ubuntu) platform. */ +function getLinuxUrl(versionGiven: string): string { + let version = versionGiven + + const rc = UBUNTU_RC.get(version) + if (rc !== undefined) { + version = rc + } + + let ubuntu: string + // ubuntu-version is specified + if (version.includes("ubuntu")) { + ubuntu = version + } else if (version !== "") { + ubuntu = UBUNTU[version] + } else { + // default to the maximum vresion + ubuntu = UBUNTU[MAX_UBUNTU] + } + + const prefix = "clang+llvm-" + const suffix = `-x86_64-linux-gnu${ubuntu}.tar.xz` + if (semverLte(version, "9.0.1")) { + return getReleaseUrl(version, prefix, suffix) + } else { + return getGitHubUrl(version, prefix, suffix) + } +} + +/** The LLVM versions that were never released for the Windows platform. */ +const WIN32_MISSING: Set = new Set(["10.0.1"]) + +/** Gets an LLVM download URL for the Windows platform. */ +function getWin32Url(version: string): string | null { + if (WIN32_MISSING.has(version)) { + return null + } + + const prefix = "LLVM-" + const suffix = semverLte(version, "3.7.0") ? "-win32.exe" : "-win64.exe" + if (semverLte(version, "9.0.1")) { + return getReleaseUrl(version, prefix, suffix) + } else { + return getGitHubUrl(version, prefix, suffix) + } +} + +/** Gets an LLVM download URL. */ +function getUrl(platform: string, version: string): string | null { + switch (platform) { + case "darwin": + return getDarwinUrl(version) + case "linux": + return getLinuxUrl(version) + case "win32": + return getWin32Url(version) + default: + return null + } +} + +/** Gets the most recent specific LLVM version for which there is a valid download URL. */ +export function getSpecificVersionAndUrl(platform: string, version: string): [string, string] { + if (!VERSIONS.has(version)) { + throw new Error(`Unsupported target! (platform='${platform}', version='${version}')`) + } + + for (const specificVersion of getSpecificVersions(version)) { + const url = getUrl(platform, specificVersion) + if (url !== null) { + return [specificVersion, url] + } + } + + throw new Error(`Unsupported target! (platform='${platform}', version='${version}')`) +} + +//================================================ +// Action +//================================================ + +const DEFAULT_NIX_DIRECTORY = "./llvm" +const DEFAULT_WIN32_DIRECTORY = "C:/Program Files/LLVM" + +async function install(version: string, directory: string): Promise { + const platform = process.platform + const [specificVersion, url] = getSpecificVersionAndUrl(platform, version) + core.setOutput("version", specificVersion) + + core.info(`Installing LLVM and Clang ${version} (${specificVersion})...`) + core.info(`Downloading and extracting '${url}'...`) + const archive = await tc.downloadTool(url) + + let exit + if (platform === "win32") { + exit = await exec.exec("7z", ["x", archive, `-o${directory}`]) + } else { + await io.mkdirP(directory) + exit = await exec.exec("tar", ["xf", archive, "-C", directory, "--strip-components=1"]) + } + + if (exit !== 0) { + throw new Error("Could not extract LLVM and Clang binaries.") + } + + core.info(`Installed LLVM and Clang ${version} (${specificVersion})!`) + core.info(`Install location: ${directory}`) +} + +export async function setupLLVM(version: string, directoryGiven?: string, cached: boolean = false): Promise { + let directory = directoryGiven + if (directory === "" || directory === undefined) { + directory = process.platform === "win32" ? DEFAULT_WIN32_DIRECTORY : DEFAULT_NIX_DIRECTORY + } + + directory = path.resolve(directory) + + if (cached) { + core.info(`Using cached LLVM and Clang ${version}...`) + } else { + await install(version, directory) + } + + const bin = path.join(directory, "bin") + const lib = path.join(directory, "lib") + + core.addPath(bin) + + const ld = process.env.LD_LIBRARY_PATH ?? "" + const dyld = process.env.DYLD_LIBRARY_PATH ?? "" + + core.exportVariable("LLVM_PATH", directory) + core.exportVariable("LD_LIBRARY_PATH", `${lib}${path.delimiter}${ld}`) + core.exportVariable("DYLD_LIBRARY_PATH", `${lib}${path.delimiter}${dyld}`) +} diff --git a/src/main.ts b/src/main.ts index 32df0533..5e8a6c02 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,6 +25,12 @@ export async function main(): Promise { if (ninjaVersion !== undefined) { await setupNinja(ninjaVersion, setupCppDir) } + + // setup llvm + const llvmVersion = maybeGetInput("llvm") + if (llvmVersion !== undefined) { + await setupLLVM(llvmVersion, setupCppDir) + } } catch (err) { core.error(err as string | Error) core.setFailed("install-cpp failed")