mirror of
https://github.com/Swatinem/rust-cache
synced 2025-04-10 22:57:07 +00:00
300 lines
9.1 KiB
TypeScript
300 lines
9.1 KiB
TypeScript
import * as core from "@actions/core";
|
|
import * as io from "@actions/io";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
import { CARGO_HOME } from "./config";
|
|
import { Packages } from "./workspace";
|
|
|
|
export async function cleanTargetDir(targetDir: string, packages: Packages, checkTimestamp = false) {
|
|
core.debug(`cleaning target directory "${targetDir}"`);
|
|
|
|
// remove all *files* from the profile directory
|
|
let dir = await fs.promises.opendir(targetDir);
|
|
for await (const dirent of dir) {
|
|
if (dirent.isDirectory()) {
|
|
let dirName = path.join(dir.path, dirent.name);
|
|
// is it a profile dir, or a nested target dir?
|
|
let isNestedTarget =
|
|
(await exists(path.join(dirName, "CACHEDIR.TAG"))) || (await exists(path.join(dirName, ".rustc_info.json")));
|
|
|
|
try {
|
|
if (isNestedTarget) {
|
|
await cleanTargetDir(dirName, packages, checkTimestamp);
|
|
} else {
|
|
await cleanProfileTarget(dirName, packages, checkTimestamp);
|
|
}
|
|
} catch {}
|
|
} else if (dirent.name !== "CACHEDIR.TAG") {
|
|
await rm(dir.path, dirent);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function cleanProfileTarget(profileDir: string, packages: Packages, checkTimestamp = false) {
|
|
core.debug(`cleaning profile directory "${profileDir}"`);
|
|
|
|
let keepProfile = new Set(["build", ".fingerprint", "deps"]);
|
|
await rmExcept(profileDir, keepProfile);
|
|
|
|
const keepPkg = new Set(packages.map((p) => p.name));
|
|
await rmExcept(path.join(profileDir, "build"), keepPkg, checkTimestamp);
|
|
await rmExcept(path.join(profileDir, ".fingerprint"), keepPkg, checkTimestamp);
|
|
|
|
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(path.join(profileDir, "deps"), keepDeps, checkTimestamp);
|
|
}
|
|
|
|
export async function getCargoBins(): Promise<Set<string>> {
|
|
const bins = new Set<string>();
|
|
try {
|
|
const { installs }: { installs: { [key: string]: { bins: Array<string> } } } = JSON.parse(
|
|
await fs.promises.readFile(path.join(CARGO_HOME, ".crates2.json"), "utf8"),
|
|
);
|
|
for (const pkg of Object.values(installs)) {
|
|
for (const bin of pkg.bins) {
|
|
bins.add(bin);
|
|
}
|
|
}
|
|
} catch {}
|
|
return bins;
|
|
}
|
|
|
|
/**
|
|
* Clean the cargo bin directory, removing the binaries that existed
|
|
* when the action started, as they were not created by the build.
|
|
*
|
|
* @param oldBins The binaries that existed when the action started.
|
|
*/
|
|
export async function cleanBin(oldBins: Array<string>) {
|
|
const bins = await getCargoBins();
|
|
|
|
for (const bin of oldBins) {
|
|
bins.delete(bin);
|
|
}
|
|
|
|
const dir = await fs.promises.opendir(path.join(CARGO_HOME, "bin"));
|
|
for await (const dirent of dir) {
|
|
if (dirent.isFile() && !bins.has(dirent.name)) {
|
|
await rm(dir.path, dirent);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function cleanRegistry(packages: Packages, crates = true) {
|
|
// remove `.cargo/credentials.toml`
|
|
try {
|
|
const credentials = path.join(CARGO_HOME, ".cargo", "credentials.toml");
|
|
core.debug(`deleting "${credentials}"`);
|
|
await fs.promises.unlink(credentials);
|
|
} catch {}
|
|
|
|
// `.cargo/registry/index`
|
|
let pkgSet = new Set(packages.map((p) => p.name));
|
|
const indexDir = await fs.promises.opendir(path.join(CARGO_HOME, "registry", "index"));
|
|
for await (const dirent of indexDir) {
|
|
if (dirent.isDirectory()) {
|
|
// eg `.cargo/registry/index/github.com-1ecc6299db9ec823`
|
|
// or `.cargo/registry/index/index.crates.io-e139d0d48fed7772`
|
|
const dirPath = path.join(indexDir.path, dirent.name);
|
|
|
|
// for a git registry, we can remove `.cache`, as cargo will recreate it from git
|
|
if (await exists(path.join(dirPath, ".git"))) {
|
|
await rmRF(path.join(dirPath, ".cache"));
|
|
} else {
|
|
await cleanRegistryIndexCache(dirPath, pkgSet);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!crates) {
|
|
core.debug("skipping registry cache and src cleanup");
|
|
return;
|
|
}
|
|
|
|
// `.cargo/registry/src`
|
|
// Cargo usually re-creates these from the `.crate` cache below,
|
|
// but for some reason that does not work for `-sys` crates that check timestamps
|
|
// to decide if rebuilds are necessary.
|
|
pkgSet = new Set(packages.filter((p) => p.name.endsWith("-sys")).map((p) => `${p.name}-${p.version}`));
|
|
const srcDir = await fs.promises.opendir(path.join(CARGO_HOME, "registry", "src"));
|
|
for await (const dirent of srcDir) {
|
|
if (dirent.isDirectory()) {
|
|
// eg `.cargo/registry/src/github.com-1ecc6299db9ec823`
|
|
// or `.cargo/registry/src/index.crates.io-e139d0d48fed7772`
|
|
const dir = await fs.promises.opendir(path.join(srcDir.path, dirent.name));
|
|
for await (const dirent of dir) {
|
|
if (dirent.isDirectory() && !pkgSet.has(dirent.name)) {
|
|
await rmRF(path.join(dir.path, dirent.name));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// `.cargo/registry/cache`
|
|
pkgSet = new Set(packages.map((p) => `${p.name}-${p.version}.crate`));
|
|
const cacheDir = await fs.promises.opendir(path.join(CARGO_HOME, "registry", "cache"));
|
|
for await (const dirent of cacheDir) {
|
|
if (dirent.isDirectory()) {
|
|
// eg `.cargo/registry/cache/github.com-1ecc6299db9ec823`
|
|
// or `.cargo/registry/cache/index.crates.io-e139d0d48fed7772`
|
|
const dir = await fs.promises.opendir(path.join(cacheDir.path, dirent.name));
|
|
for await (const dirent of dir) {
|
|
// here we check that the downloaded `.crate` matches one from our dependencies
|
|
if (dirent.isFile() && !pkgSet.has(dirent.name)) {
|
|
await rm(dir.path, dirent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Recursively walks and cleans the index `.cache`
|
|
async function cleanRegistryIndexCache(dirName: string, keepPkg: Set<string>) {
|
|
let dirIsEmpty = true;
|
|
const cacheDir = await fs.promises.opendir(dirName);
|
|
for await (const dirent of cacheDir) {
|
|
if (dirent.isDirectory()) {
|
|
if (await cleanRegistryIndexCache(path.join(dirName, dirent.name), keepPkg)) {
|
|
await rm(dirName, dirent);
|
|
} else {
|
|
dirIsEmpty &&= false;
|
|
}
|
|
} else {
|
|
if (keepPkg.has(dirent.name)) {
|
|
dirIsEmpty &&= false;
|
|
} else {
|
|
await rm(dirName, dirent);
|
|
}
|
|
}
|
|
}
|
|
return dirIsEmpty;
|
|
}
|
|
|
|
export async function cleanGit(packages: Packages) {
|
|
const coPath = path.join(CARGO_HOME, "git", "checkouts");
|
|
const dbPath = path.join(CARGO_HOME, "git", "db");
|
|
const repos = new Map<string, Set<string>>();
|
|
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
|
|
|
|
// clean the db
|
|
try {
|
|
let dir = await fs.promises.opendir(dbPath);
|
|
for await (const dirent of dir) {
|
|
if (!repos.has(dirent.name)) {
|
|
await rm(dir.path, dirent);
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
// clean the checkouts
|
|
try {
|
|
let 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);
|
|
}
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
const ONE_WEEK = 7 * 24 * 3600 * 1000;
|
|
|
|
/**
|
|
* Removes all files or directories in `dirName` matching some criteria.
|
|
*
|
|
* When the `checkTimestamp` flag is set, this will also remove anything older
|
|
* than one week.
|
|
*
|
|
* Otherwise, it will remove everything that does not match any string in the
|
|
* `keepPrefix` set.
|
|
* The matching strips and trailing `-$hash` suffix.
|
|
*/
|
|
async function rmExcept(dirName: string, keepPrefix: Set<string>, checkTimestamp = false) {
|
|
const dir = await fs.promises.opendir(dirName);
|
|
for await (const dirent of dir) {
|
|
if (checkTimestamp) {
|
|
const fileName = path.join(dir.path, dirent.name);
|
|
const { mtime } = await fs.promises.stat(fileName);
|
|
const isOutdated = Date.now() - mtime.getTime() > ONE_WEEK;
|
|
|
|
if (isOutdated) {
|
|
await rm(dir.path, dirent);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let name = dirent.name;
|
|
|
|
// strip the trailing hash
|
|
const idx = name.lastIndexOf("-");
|
|
if (idx !== -1) {
|
|
name = name.slice(0, idx);
|
|
}
|
|
|
|
if (!keepPrefix.has(name)) {
|
|
await rm(dir.path, dirent);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function rm(parent: string, dirent: fs.Dirent) {
|
|
try {
|
|
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);
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
async function rmRF(dirName: string) {
|
|
core.debug(`deleting "${dirName}"`);
|
|
await io.rmRF(dirName);
|
|
}
|
|
|
|
async function exists(path: string) {
|
|
try {
|
|
await fs.promises.access(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|