setup-bun/src/action.ts
Ashcon Partovi a93230df19
Various improvements and fixes to setup-bun (#40)
* Do not save cache on hit
* Support Windows (canary only)
* Support `registry-url` and `scope`
2023-11-17 15:58:17 -08:00

179 lines
4.7 KiB
TypeScript

import { homedir } from "node:os";
import { join } from "node:path";
import {
mkdirSync,
readdirSync,
symlinkSync,
renameSync,
copyFileSync,
} from "node:fs";
import { addPath, info, warning } from "@actions/core";
import { isFeatureAvailable, restoreCache, saveCache } from "@actions/cache";
import { downloadTool, extractZip } from "@actions/tool-cache";
import { getExecOutput } from "@actions/exec";
import { writeBunfig } from "./bunfig";
export type Input = {
customUrl?: string;
version?: string;
os?: string;
arch?: string;
avx2?: boolean;
profile?: boolean;
scope?: string;
registryUrl?: string;
};
export type Output = {
version: string;
revision: string;
cacheHit: boolean;
};
export default async (options: Input): Promise<Output> => {
const bunfigPath = join(process.cwd(), "bunfig.toml");
writeBunfig(bunfigPath, options);
const url = getDownloadUrl(options);
const cacheEnabled = isCacheEnabled(options);
const binPath = join(homedir(), ".bun", "bin");
try {
mkdirSync(binPath, { recursive: true });
} catch (error) {
if (error.code !== "EEXIST") {
throw error;
}
}
addPath(binPath);
const exe = (name: string) =>
process.platform === "win32" ? `${name}.exe` : name;
const bunPath = join(binPath, exe("bun"));
try {
symlinkSync(bunPath, join(binPath, exe("bunx")));
} catch (error) {
if (error.code !== "EEXIST") {
throw error;
}
}
let revision: string | undefined;
let cacheHit = false;
if (cacheEnabled) {
const cacheRestored = await restoreCache([bunPath], url);
if (cacheRestored) {
revision = await getRevision(bunPath);
if (revision) {
cacheHit = true;
info(`Using a cached version of Bun: ${revision}`);
} else {
warning(
`Found a cached version of Bun: ${revision} (but it appears to be corrupted?)`
);
}
}
}
if (!cacheHit) {
info(`Downloading a new version of Bun: ${url}`);
const zipPath = await downloadTool(url);
const extractedZipPath = await extractZip(zipPath);
const extractedBunPath = await extractBun(extractedZipPath);
try {
renameSync(extractedBunPath, bunPath);
} catch {
// If mv does not work, try to copy the file instead.
// For example: EXDEV: cross-device link not permitted
copyFileSync(extractedBunPath, bunPath);
}
revision = await getRevision(bunPath);
}
if (!revision) {
throw new Error(
"Downloaded a new version of Bun, but failed to check its version? Try again."
);
}
if (cacheEnabled && !cacheHit) {
try {
await saveCache([bunPath], url);
} catch (error) {
warning("Failed to save Bun to cache.");
}
}
const [version] = revision.split("+");
return {
version,
revision,
cacheHit,
};
};
function isCacheEnabled(options: Input): boolean {
const { customUrl, version } = options;
if (customUrl) {
return false;
}
if (!version || /latest|canary|action/i.test(version)) {
return false;
}
return isFeatureAvailable();
}
function getDownloadUrl(options: Input): string {
const { customUrl } = options;
if (customUrl) {
return customUrl;
}
const { version, os, arch, avx2, profile } = options;
const eversion = encodeURIComponent(version ?? "latest");
const eos = encodeURIComponent(os ?? process.platform);
const earch = encodeURIComponent(arch ?? process.arch);
const eavx2 = encodeURIComponent(avx2 ?? true);
const eprofile = encodeURIComponent(profile ?? false);
const { href } = new URL(
`${eversion}/${eos}/${earch}?avx2=${eavx2}&profile=${eprofile}`,
"https://bun.sh/download/"
);
return href;
}
async function extractBun(path: string): Promise<string> {
for (const entry of readdirSync(path, { withFileTypes: true })) {
const { name } = entry;
const entryPath = join(path, name);
if (entry.isFile()) {
if (name === "bun" || name === "bun.exe") {
return entryPath;
}
if (/^bun.*\.zip/.test(name)) {
const extractedPath = await extractZip(entryPath);
return extractBun(extractedPath);
}
}
if (/^bun/.test(name) && entry.isDirectory()) {
return extractBun(entryPath);
}
}
throw new Error("Could not find executable: bun");
}
async function getRevision(exe: string): Promise<string | undefined> {
const revision = await getExecOutput(exe, ["--revision"], {
ignoreReturnCode: true,
});
if (revision.exitCode === 0 && /^\d+\.\d+\.\d+/.test(revision.stdout)) {
return revision.stdout.trim();
}
const version = await getExecOutput(exe, ["--version"], {
ignoreReturnCode: true,
});
if (version.exitCode === 0 && /^\d+\.\d+\.\d+/.test(version.stdout)) {
return version.stdout.trim();
}
return undefined;
}