cache-apt-pkgs-action/dist/action.js
2026-06-28 13:27:41 -07:00

265 lines
10 KiB
JavaScript

import * as cache from "@actions/cache";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createPackageManager, DefaultCommandRunner, } from "../../ts-apt/dist/index.js";
import { isAptListsFresh } from "./io.js";
import { readManifestAsCsv, writeManifest } from "./manifest.js";
import * as tar from "tar";
const FORCE_UPDATE_INCREMENT = "3";
const CACHE_DIRNAME = "cache-apt-pkgs";
const CACHE_PREFIX = "cache-apt-pkgs_";
export class ActionPackageName {
name;
version;
constructor(name, version) {
this.name = name;
this.version = version;
}
serialize() {
return this.version ? `${this.name}=${this.version}` : this.name;
}
}
function toPackageName(packageSpecifier) {
const [name, version] = packageSpecifier.split("=");
if (!name) {
throw new Error("Package name cannot be empty.");
}
return new ActionPackageName(name, version);
}
export function parseBoolean(value, fieldName) {
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
throw new Error(`${fieldName} value '${value}' must be either true or false.`);
}
export function normalizeInputPackages(inputPackages) {
return inputPackages
.replace(/[,\\]/g, " ")
.replace(/\s+/g, " ")
.trim()
.split(" ")
.map((part) => part.trim())
.filter(Boolean)
.sort((a, b) => a.localeCompare(b));
}
export class ActionRunner {
commandRunner;
tar;
logger;
constructor(commandRunner, tarModule, logger) {
this.commandRunner = commandRunner;
this.tar = tarModule;
this.logger = logger;
}
parseBoolean(value, fieldName) {
return parseBoolean(value, fieldName);
}
normalizeInputPackages(inputPackages) {
return normalizeInputPackages(inputPackages);
}
async resolvePackageVersion(packageManager, packageName) {
const packageInfo = await packageManager.getPackageInfo([
toPackageName(packageName),
]);
const version = packageInfo[0]?.version;
if (!version) {
throw new Error(`Unable to resolve package version for '${packageName}'.`);
}
return version;
}
async normalizePackagesWithVersions(packageManager, inputPackages) {
const raw = this.normalizeInputPackages(inputPackages);
const packages = await Promise.all(raw.map(async (pkg) => {
if (pkg.includes("=")) {
return pkg;
}
return `${pkg}=${await this.resolvePackageVersion(packageManager, pkg)}`;
}));
return packages.sort((a, b) => a.localeCompare(b));
}
validateEmptyPackages(behavior, packages) {
if (packages.length > 0) {
return;
}
if (behavior === "ignore") {
return;
}
if (behavior === "warn") {
process.stdout.write("::warning::Packages argument is empty.\n");
return;
}
throw new Error("Packages argument is empty.");
}
getCacheRoot() {
return path.join(os.homedir(), CACHE_DIRNAME);
}
async getCacheKey(normalizedPackages, version) {
const architecture = (await this.commandRunner.run("arch")).stdout.trim();
let value = `${normalizedPackages.join(" ")} @ ${version} ${FORCE_UPDATE_INCREMENT}`;
if (architecture !== "x86_64") {
value = `${value} ${architecture}`;
}
const hash = crypto.createHash("md5").update(value).digest("hex");
return `${CACHE_PREFIX}${hash}`;
}
findInstallScript(packageName, extension, root) {
const scriptsDir = path.join(root, "var", "lib", "dpkg", "info");
if (!fs.existsSync(scriptsDir)) {
return undefined;
}
const pattern = new RegExp(`^${packageName}(:.*)?\\.${extension}$`);
const matches = fs
.readdirSync(scriptsDir)
.filter((entry) => pattern.test(entry))
.sort((a, b) => a.localeCompare(b));
const candidate = matches[0];
if (!candidate) {
return undefined;
}
return path.join(scriptsDir, candidate);
}
tarRelativePath(filePath) {
return filePath.startsWith("/") ? filePath.slice(1) : filePath;
}
async buildFileListForPackage(packageManager, packageName) {
const files = (await packageManager.listInstalledFiles(toPackageName(packageName)))
.filter((filePath) => {
if (!fs.existsSync(filePath)) {
return false;
}
const stat = fs.lstatSync(filePath);
return stat.isFile() || stat.isSymbolicLink();
})
.map(this.tarRelativePath);
const preinst = this.findInstallScript(packageName, "preinst", "/");
const postinst = this.findInstallScript(packageName, "postinst", "/");
if (preinst) {
files.push(this.tarRelativePath(preinst));
}
if (postinst) {
files.push(this.tarRelativePath(postinst));
}
return Array.from(new Set(files)).sort((a, b) => a.localeCompare(b));
}
async updateAptLists(packageManager) {
if (isAptListsFresh()) {
return;
}
await packageManager.update();
}
packageSpecifierToName(packageSpecifier) {
return packageSpecifier.split("=")[0] ?? packageSpecifier;
}
async installAndCachePackages(cacheDir, packages, packageManager) {
await this.updateAptLists(packageManager);
writeManifest(path.join(cacheDir, "manifest_main.log"), packages);
const installedPackages = await packageManager.install(packages.map(toPackageName));
const manifestAll = [];
for (const pkg of installedPackages) {
const packageName = pkg.name;
const packageVersion = pkg.version;
if (!packageName || !packageVersion) {
continue;
}
const archivePath = path.join(cacheDir, `${packageName}=${packageVersion}.tar`);
if (!fs.existsSync(archivePath)) {
const filesToArchive = await this.buildFileListForPackage(packageManager, packageName);
await this.tar.create({
cwd: "/",
file: archivePath,
portable: false,
preservePaths: false,
follow: false,
noDirRecurse: false,
}, filesToArchive);
}
manifestAll.push(`${packageName}=${packageVersion}`);
}
writeManifest(path.join(cacheDir, "manifest_all.log"), manifestAll);
}
async restorePackages(cacheDir, executeInstallScripts) {
const archives = fs
.readdirSync(cacheDir)
.filter((entry) => entry.endsWith(".tar"))
.sort((a, b) => a.localeCompare(b));
for (const archive of archives) {
const archivePath = path.join(cacheDir, archive);
await this.tar.extract({
cwd: "/",
file: archivePath,
preservePaths: true,
});
if (!executeInstallScripts) {
continue;
}
const packageName = archive.split("=")[0] ?? "";
if (!packageName) {
continue;
}
const preinst = this.findInstallScript(packageName, "preinst", "/");
if (preinst) {
this.logger.info(`Running pre-install script for ${packageName}`);
await this.commandRunner.run("sudo", ["sh", "-x", preinst, "install"]);
}
const postinst = this.findInstallScript(packageName, "postinst", "/");
if (postinst) {
this.logger.info(`Running post-install script for ${packageName}`);
await this.commandRunner.run("sudo", [
"sh",
"-x",
postinst,
"configure",
]);
}
}
}
async runAction(inputs) {
if (/\s/.test(inputs.version)) {
throw new Error(`Version value '${inputs.version}' cannot contain spaces.`);
}
const packageInfoManager = await createPackageManager(false);
const normalizedPackages = await this.normalizePackagesWithVersions(packageInfoManager, inputs.packages);
this.validateEmptyPackages(inputs.emptyPackagesBehavior, normalizedPackages);
const cacheDir = this.getCacheRoot();
fs.mkdirSync(cacheDir, { recursive: true });
if (normalizedPackages.length === 0) {
writeManifest(path.join(cacheDir, "manifest_main.log"), []);
writeManifest(path.join(cacheDir, "manifest_all.log"), []);
return {
cacheHit: false,
packageVersionList: "",
allPackageVersionList: "",
};
}
const key = await this.getCacheKey(normalizedPackages, inputs.version);
fs.writeFileSync(path.join(cacheDir, "cache_key.md5"), key.replace(CACHE_PREFIX, ""), "utf8");
const restoredKey = await cache.restoreCache([cacheDir], key);
const cacheHit = restoredKey === key;
if (cacheHit) {
await this.restorePackages(cacheDir, inputs.executeInstallScripts);
}
else {
const installManager = await createPackageManager(true);
const installTargets = normalizedPackages.map((packageSpecifier) => this.packageSpecifierToName(packageSpecifier));
await this.installAndCachePackages(cacheDir, installTargets, installManager);
await cache.saveCache([cacheDir], key);
}
return {
cacheHit,
packageVersionList: readManifestAsCsv(path.join(cacheDir, "manifest_main.log")),
allPackageVersionList: readManifestAsCsv(path.join(cacheDir, "manifest_all.log")),
};
}
}
export async function runAction(inputs, logger) {
const packageManager = (await createPackageManager(true, logger, logger));
const commandRunner = new DefaultCommandRunner(logger, logger);
const actionRunner = new ActionRunner(commandRunner, tar, logger);
return await actionRunner.runAction(inputs);
}
//# sourceMappingURL=action.js.map