mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-02-04 01:38:05 +00:00
add outputs python-version and python-cache-hit (#728)
This commit splits up the "normal" cache containing the dependencies and the "python" cache containing the python binaries. This will lead to a one-time invalidation of caches. Closes: #713
This commit is contained in:
committed by
GitHub
parent
11050edb83
commit
61cb8a9741
99
src/cache/restore-cache.ts
vendored
99
src/cache/restore-cache.ts
vendored
@@ -1,6 +1,5 @@
|
||||
import * as cache from "@actions/cache";
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import { hashFiles } from "../hash/hash-files";
|
||||
import {
|
||||
cacheDependencyGlob,
|
||||
@@ -9,52 +8,75 @@ import {
|
||||
cacheSuffix,
|
||||
pruneCache,
|
||||
pythonDir,
|
||||
pythonVersion as pythonVersionInput,
|
||||
restoreCache as shouldRestoreCache,
|
||||
workingDirectory,
|
||||
} from "../utils/inputs";
|
||||
import { getArch, getOSNameVersion, getPlatform } from "../utils/platforms";
|
||||
|
||||
export const STATE_CACHE_KEY = "cache-key";
|
||||
export const STATE_CACHE_MATCHED_KEY = "cache-matched-key";
|
||||
export const STATE_PYTHON_CACHE_MATCHED_KEY = "python-cache-matched-key";
|
||||
|
||||
const CACHE_VERSION = "2";
|
||||
|
||||
export async function restoreCache(): Promise<void> {
|
||||
const cacheKey = await computeKeys();
|
||||
export async function restoreCache(pythonVersion?: string): Promise<void> {
|
||||
const cacheKey = await computeKeys(pythonVersion);
|
||||
core.saveState(STATE_CACHE_KEY, cacheKey);
|
||||
core.setOutput("cache-key", cacheKey);
|
||||
|
||||
if (!shouldRestoreCache) {
|
||||
core.info("restore-cache is false. Skipping restore cache step.");
|
||||
core.setOutput("python-cache-hit", false);
|
||||
return;
|
||||
}
|
||||
|
||||
let matchedKey: string | undefined;
|
||||
core.info(
|
||||
`Trying to restore uv cache from GitHub Actions cache with key: ${cacheKey}`,
|
||||
);
|
||||
if (cacheLocalPath === undefined) {
|
||||
throw new Error(
|
||||
"cache-local-path is not set. Cannot restore cache without a valid cache path.",
|
||||
);
|
||||
}
|
||||
const cachePaths = [cacheLocalPath.path];
|
||||
|
||||
await restoreCacheFromKey(
|
||||
cacheKey,
|
||||
cacheLocalPath.path,
|
||||
STATE_CACHE_MATCHED_KEY,
|
||||
"cache-hit",
|
||||
);
|
||||
|
||||
if (cachePython) {
|
||||
cachePaths.push(pythonDir);
|
||||
await restoreCacheFromKey(
|
||||
`${cacheKey}-python`,
|
||||
pythonDir,
|
||||
STATE_PYTHON_CACHE_MATCHED_KEY,
|
||||
"python-cache-hit",
|
||||
);
|
||||
} else {
|
||||
core.setOutput("python-cache-hit", false);
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreCacheFromKey(
|
||||
cacheKey: string,
|
||||
cachePath: string,
|
||||
stateKey: string,
|
||||
outputKey: string,
|
||||
): Promise<void> {
|
||||
core.info(
|
||||
`Trying to restore cache from GitHub Actions cache with key: ${cacheKey}`,
|
||||
);
|
||||
let matchedKey: string | undefined;
|
||||
try {
|
||||
matchedKey = await cache.restoreCache(cachePaths, cacheKey);
|
||||
matchedKey = await cache.restoreCache([cachePath], cacheKey);
|
||||
} catch (err) {
|
||||
const message = (err as Error).message;
|
||||
core.warning(message);
|
||||
core.setOutput("cache-hit", false);
|
||||
core.setOutput(outputKey, false);
|
||||
return;
|
||||
}
|
||||
|
||||
handleMatchResult(matchedKey, cacheKey);
|
||||
handleMatchResult(matchedKey, cacheKey, stateKey, outputKey);
|
||||
}
|
||||
|
||||
async function computeKeys(): Promise<string> {
|
||||
async function computeKeys(pythonVersion?: string): Promise<string> {
|
||||
let cacheDependencyPathHash = "-";
|
||||
if (cacheDependencyGlob !== "") {
|
||||
core.info(
|
||||
@@ -71,58 +93,27 @@ async function computeKeys(): Promise<string> {
|
||||
cacheDependencyPathHash = "-no-dependency-glob";
|
||||
}
|
||||
const suffix = cacheSuffix ? `-${cacheSuffix}` : "";
|
||||
const pythonVersion = await getPythonVersion();
|
||||
const version = pythonVersion ?? "unknown";
|
||||
const platform = await getPlatform();
|
||||
const osNameVersion = getOSNameVersion();
|
||||
const pruned = pruneCache ? "-pruned" : "";
|
||||
const python = cachePython ? "-py" : "";
|
||||
return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${osNameVersion}-${pythonVersion}${pruned}${python}${cacheDependencyPathHash}${suffix}`;
|
||||
}
|
||||
|
||||
async function getPythonVersion(): Promise<string> {
|
||||
if (pythonVersionInput !== "") {
|
||||
return pythonVersionInput;
|
||||
}
|
||||
|
||||
let output = "";
|
||||
const options: exec.ExecOptions = {
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
output += data.toString();
|
||||
},
|
||||
},
|
||||
silent: !core.isDebug(),
|
||||
};
|
||||
|
||||
try {
|
||||
const execArgs = ["python", "find", "--directory", workingDirectory];
|
||||
await exec.exec("uv", execArgs, options);
|
||||
const pythonPath = output.trim();
|
||||
|
||||
output = "";
|
||||
await exec.exec(pythonPath, ["--version"], options);
|
||||
// output is like "Python 3.8.10"
|
||||
return output.split(" ")[1].trim();
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
core.debug(`Failed to get python version from uv. Error: ${err.message}`);
|
||||
return "unknown";
|
||||
}
|
||||
return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${osNameVersion}-${version}${pruned}${python}${cacheDependencyPathHash}${suffix}`;
|
||||
}
|
||||
|
||||
function handleMatchResult(
|
||||
matchedKey: string | undefined,
|
||||
primaryKey: string,
|
||||
stateKey: string,
|
||||
outputKey: string,
|
||||
): void {
|
||||
if (!matchedKey) {
|
||||
core.info(`No GitHub Actions cache found for key: ${primaryKey}`);
|
||||
core.setOutput("cache-hit", false);
|
||||
core.setOutput(outputKey, false);
|
||||
return;
|
||||
}
|
||||
|
||||
core.saveState(STATE_CACHE_MATCHED_KEY, matchedKey);
|
||||
core.info(
|
||||
`uv cache restored from GitHub Actions cache with key: ${matchedKey}`,
|
||||
);
|
||||
core.setOutput("cache-hit", true);
|
||||
core.saveState(stateKey, matchedKey);
|
||||
core.info(`cache restored from GitHub Actions cache with key: ${matchedKey}`);
|
||||
core.setOutput(outputKey, true);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as pep440 from "@renovatebot/pep440";
|
||||
import {
|
||||
STATE_CACHE_KEY,
|
||||
STATE_CACHE_MATCHED_KEY,
|
||||
STATE_PYTHON_CACHE_MATCHED_KEY,
|
||||
} from "./cache/restore-cache";
|
||||
import { STATE_UV_PATH, STATE_UV_VERSION } from "./utils/constants";
|
||||
import {
|
||||
@@ -52,63 +53,30 @@ async function saveCache(): Promise<void> {
|
||||
}
|
||||
if (matchedKey === cacheKey) {
|
||||
core.info(`Cache hit occurred on key ${cacheKey}, not saving cache.`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (shouldPruneCache) {
|
||||
await pruneCache();
|
||||
}
|
||||
|
||||
if (shouldPruneCache) {
|
||||
await pruneCache();
|
||||
}
|
||||
|
||||
if (cacheLocalPath === undefined) {
|
||||
throw new Error(
|
||||
"cache-local-path is not set. Cannot save cache without a valid cache path.",
|
||||
);
|
||||
}
|
||||
let actualCachePath = cacheLocalPath.path;
|
||||
if (
|
||||
process.env.UV_CACHE_DIR &&
|
||||
process.env.UV_CACHE_DIR !== cacheLocalPath.path
|
||||
) {
|
||||
core.warning(
|
||||
`The environment variable UV_CACHE_DIR has been changed to "${process.env.UV_CACHE_DIR}", by an action or step running after astral-sh/setup-uv. This can lead to unexpected behavior. If you expected this to happen set the cache-local-path input to "${process.env.UV_CACHE_DIR}" instead of "${cacheLocalPath.path}".`,
|
||||
);
|
||||
actualCachePath = process.env.UV_CACHE_DIR;
|
||||
}
|
||||
|
||||
core.info(`Saving cache path: ${actualCachePath}`);
|
||||
if (!fs.existsSync(actualCachePath) && !ignoreNothingToCache) {
|
||||
throw new Error(
|
||||
const actualCachePath = getUvCachePath();
|
||||
await saveCacheToKey(
|
||||
cacheKey,
|
||||
actualCachePath,
|
||||
STATE_CACHE_MATCHED_KEY,
|
||||
"uv cache",
|
||||
`Cache path ${actualCachePath} does not exist on disk. This likely indicates that there are no dependencies to cache. Consider disabling the cache input if it is not needed.`,
|
||||
);
|
||||
}
|
||||
|
||||
const cachePaths = [actualCachePath];
|
||||
if (cachePython) {
|
||||
core.info(`Including Python cache path: ${pythonDir}`);
|
||||
if (!fs.existsSync(pythonDir) && !ignoreNothingToCache) {
|
||||
throw new Error(
|
||||
`Python cache path ${pythonDir} does not exist on disk. This likely indicates that there are no dependencies to cache. Consider disabling the cache input if it is not needed.`,
|
||||
);
|
||||
}
|
||||
cachePaths.push(pythonDir);
|
||||
}
|
||||
|
||||
core.info(`Final cache paths: ${cachePaths.join(", ")}`);
|
||||
try {
|
||||
await cache.saveCache(cachePaths, cacheKey);
|
||||
core.info(`cache saved with the key: ${cacheKey}`);
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof Error &&
|
||||
e.message ===
|
||||
"Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved."
|
||||
) {
|
||||
core.info(
|
||||
"No cacheable paths were found. Ignoring because ignore-nothing-to-save is enabled.",
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
const pythonCacheKey = `${cacheKey}-python`;
|
||||
await saveCacheToKey(
|
||||
pythonCacheKey,
|
||||
pythonDir,
|
||||
STATE_PYTHON_CACHE_MATCHED_KEY,
|
||||
"Python cache",
|
||||
`Python cache path ${pythonDir} does not exist on disk. This likely indicates that there are no Python installations to cache. Consider disabling the cache input if it is not needed.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,4 +96,61 @@ async function pruneCache(): Promise<void> {
|
||||
await exec.exec(uvPath, execArgs, options);
|
||||
}
|
||||
|
||||
function getUvCachePath(): string {
|
||||
if (cacheLocalPath === undefined) {
|
||||
throw new Error(
|
||||
"cache-local-path is not set. Cannot save cache without a valid cache path.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
process.env.UV_CACHE_DIR &&
|
||||
process.env.UV_CACHE_DIR !== cacheLocalPath.path
|
||||
) {
|
||||
core.warning(
|
||||
`The environment variable UV_CACHE_DIR has been changed to "${process.env.UV_CACHE_DIR}", by an action or step running after astral-sh/setup-uv. This can lead to unexpected behavior. If you expected this to happen set the cache-local-path input to "${process.env.UV_CACHE_DIR}" instead of "${cacheLocalPath.path}".`,
|
||||
);
|
||||
return process.env.UV_CACHE_DIR;
|
||||
}
|
||||
return cacheLocalPath.path;
|
||||
}
|
||||
|
||||
async function saveCacheToKey(
|
||||
cacheKey: string,
|
||||
cachePath: string,
|
||||
stateKey: string,
|
||||
cacheName: string,
|
||||
pathNotExistErrorMessage: string,
|
||||
): Promise<void> {
|
||||
const matchedKey = core.getState(stateKey);
|
||||
|
||||
if (matchedKey === cacheKey) {
|
||||
core.info(
|
||||
`${cacheName} hit occurred on key ${cacheKey}, not saving cache.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Including ${cacheName} path: ${cachePath}`);
|
||||
if (!fs.existsSync(cachePath) && !ignoreNothingToCache) {
|
||||
throw new Error(pathNotExistErrorMessage);
|
||||
}
|
||||
|
||||
try {
|
||||
await cache.saveCache([cachePath], cacheKey);
|
||||
core.info(`${cacheName} saved with key: ${cacheKey}`);
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof Error &&
|
||||
e.message ===
|
||||
"Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved."
|
||||
) {
|
||||
core.info(
|
||||
`No cacheable ${cacheName} paths were found. Ignoring because ignore-nothing-to-save is enabled.`,
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
@@ -36,6 +36,37 @@ import {
|
||||
} from "./utils/platforms";
|
||||
import { getUvVersionFromFile } from "./version/resolve";
|
||||
|
||||
async function getPythonVersion(): Promise<string> {
|
||||
if (pythonVersion !== "") {
|
||||
return pythonVersion;
|
||||
}
|
||||
|
||||
let output = "";
|
||||
const options: exec.ExecOptions = {
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
output += data.toString();
|
||||
},
|
||||
},
|
||||
silent: !core.isDebug(),
|
||||
};
|
||||
|
||||
try {
|
||||
const execArgs = ["python", "find", "--directory", workingDirectory];
|
||||
await exec.exec("uv", execArgs, options);
|
||||
const pythonPath = output.trim();
|
||||
|
||||
output = "";
|
||||
await exec.exec(pythonPath, ["--version"], options);
|
||||
// output is like "Python 3.8.10"
|
||||
return output.split(" ")[1].trim();
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
core.debug(`Failed to get python version from uv. Error: ${err.message}`);
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
detectEmptyWorkdir();
|
||||
const platform = await getPlatform();
|
||||
@@ -63,8 +94,11 @@ async function run(): Promise<void> {
|
||||
core.saveState(STATE_UV_VERSION, setupResult.version);
|
||||
core.info(`Successfully installed uv version ${setupResult.version}`);
|
||||
|
||||
const pythonVersion = await getPythonVersion();
|
||||
core.setOutput("python-version", pythonVersion);
|
||||
|
||||
if (enableCache) {
|
||||
await restoreCache();
|
||||
await restoreCache(pythonVersion);
|
||||
}
|
||||
// https://github.com/nodejs/node/issues/56645#issuecomment-3077594952
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
Reference in New Issue
Block a user