import * as cache from "@actions/cache"; import * as core from "@actions/core"; import * as exec from "@actions/exec"; import * as glob from "@actions/glob"; import * as io from "@actions/io"; import fs from "fs"; import path from "path"; import { getCacheConfig, getCmdOutput, isValidEvent, paths, stateKey } from "./common"; async function run() { if (!isValidEvent()) { return; } try { const start = Date.now(); const { paths: savePaths, key } = await getCacheConfig(); if (core.getState(stateKey) === key) { core.info(`Cache up-to-date.`); return; } // TODO: remove this once https://github.com/actions/toolkit/pull/553 lands await macOsWorkaround(); const registryName = await getRegistryName(); const packages = await getPackages(); await cleanRegistry(registryName, packages); await cleanGit(packages); await cleanTarget(packages); core.info(`Saving paths:\n ${savePaths.join("\n ")}`); core.info(`Using key "${key}".`); try { await cache.saveCache(savePaths, key); } catch (e) { core.info(`[warning] ${e.message}`); } const duration = Math.round((Date.now() - start) / 1000); if (duration) { core.info(`Took ${duration}s.`); } } catch (e) { core.info(`[warning] ${e.message}`); } } run(); interface PackageDefinition { name: string; version: string; path: string; targets: Array; } type Packages = Array; interface Meta { packages: Array<{ name: string; version: string; manifest_path: string; targets: Array<{ kind: Array; name: string }>; }>; } async function getRegistryName(): Promise { const globber = await glob.create(`${paths.index}/**/.last-updated`, { followSymbolicLinks: false }); const files = await globber.glob(); if (files.length > 1) { core.warning(`got multiple registries: "${files.join('", "')}"`); } const first = files.shift()!; return path.basename(path.dirname(first)); } async function getPackages(): Promise { const cwd = process.cwd(); const meta: Meta = JSON.parse(await getCmdOutput("cargo", ["metadata", "--all-features", "--format-version", "1"])); return meta.packages .filter((p) => !p.manifest_path.startsWith(cwd)) .map((p) => { const targets = p.targets.filter((t) => t.kind[0] === "lib").map((t) => t.name); return { name: p.name, version: p.version, targets, path: path.dirname(p.manifest_path) }; }); } async function cleanRegistry(registryName: string, packages: Packages) { await io.rmRF(path.join(paths.index, registryName, ".cache")); const pkgSet = new Set(packages.map((p) => `${p.name}-${p.version}.crate`)); const dir = await fs.promises.opendir(path.join(paths.cache, registryName)); for await (const dirent of dir) { if (dirent.isFile() && !pkgSet.has(dirent.name)) { await rm(dir.path, dirent); } } } async function cleanGit(packages: Packages) { const coPath = path.join(paths.git, "checkouts"); const dbPath = path.join(paths.git, "db"); const repos = new Map>(); for (const p of packages) { if (!p.path.startsWith(coPath)) { continue; } const [repo, ref] = p.path.slice(coPath.length + 1).split(path.sep); const refs = repos.get(repo); if (refs) { refs.add(ref); } else { repos.set(repo, new Set([ref])); } } // we have to keep both the clone, and the checkout, removing either will // trigger a rebuild let dir: fs.Dir; // clean the db dir = await fs.promises.opendir(dbPath); for await (const dirent of dir) { if (!repos.has(dirent.name)) { await rm(dir.path, dirent); } } // clean the checkouts dir = await fs.promises.opendir(coPath); for await (const dirent of dir) { const refs = repos.get(dirent.name); if (!refs) { await rm(dir.path, dirent); continue; } if (!dirent.isDirectory()) { continue; } const refsDir = await fs.promises.opendir(path.join(dir.path, dirent.name)); for await (const dirent of refsDir) { if (!refs.has(dirent.name)) { await rm(refsDir.path, dirent); } } } } async function cleanTarget(packages: Packages) { await fs.promises.unlink("./target/.rustc_info.json"); await io.rmRF("./target/debug/examples"); await io.rmRF("./target/debug/incremental"); let dir: fs.Dir; // remove all *files* from debug dir = await fs.promises.opendir("./target/debug"); for await (const dirent of dir) { if (dirent.isFile()) { await rm(dir.path, dirent); } } const keepPkg = new Set(packages.map((p) => p.name)); await rmExcept("./target/debug/build", keepPkg); await rmExcept("./target/debug/.fingerprint", keepPkg); const keepDeps = new Set( packages.flatMap((p) => { const names = []; for (const n of [p.name, ...p.targets]) { const name = n.replace(/-/g, "_"); names.push(name, `lib${name}`); } return names; }), ); await rmExcept("./target/debug/deps", keepDeps); } const oneWeek = 7 * 24 * 3600 * 1000; async function rmExcept(dirName: string, keepPrefix: Set) { const dir = await fs.promises.opendir(dirName); for await (const dirent of dir) { let name = dirent.name; const idx = name.lastIndexOf("-"); if (idx !== -1) { name = name.slice(0, idx); } const fileName = path.join(dir.path, dirent.name); const { mtime } = await fs.promises.stat(fileName); if (!keepPrefix.has(name) || Date.now() - mtime.getTime() > oneWeek) { await rm(dir.path, dirent); } } } async function rm(parent: string, dirent: fs.Dirent) { const fileName = path.join(parent, dirent.name); core.debug(`deleting "${fileName}"`); if (dirent.isFile()) { await fs.promises.unlink(fileName); } else if (dirent.isDirectory()) { await io.rmRF(fileName); } } async function macOsWorkaround() { try { // Workaround for https://github.com/actions/cache/issues/403 // Also see https://github.com/rust-lang/cargo/issues/8603 await exec.exec("sudo", ["/usr/sbin/purge"], { silent: true }); } catch {} }