From f50e500f4e60f635215683706c42b7587917735f Mon Sep 17 00:00:00 2001 From: Merlin <158784988+merlinz01@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:30:41 -0400 Subject: [PATCH] Add cache-python input for caching Python installs --- README.md | 15 ++++++++++++++ action.yml | 3 +++ dist/save-cache/index.js | 40 ++++++++++++++++++++++++++++++++++---- dist/setup/index.js | 28 +++++++++++++++++++++++--- src/cache/restore-cache.ts | 11 +++++++++-- src/save-cache.ts | 18 ++++++++++++++++- src/utils/inputs.ts | 21 ++++++++++++++++++++ 7 files changed, 126 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index abd319e..b9f642a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Set up your GitHub Actions workflow with a specific version of [uv](https://docs - [Save cache](#save-cache) - [Local cache path](#local-cache-path) - [Disable cache pruning](#disable-cache-pruning) + - [Cache Python installs](#cache-python-installs) - [Ignore nothing to cache](#ignore-nothing-to-cache) - [GitHub authentication token](#github-authentication-token) - [UV_TOOL_DIR](#uv_tool_dir) @@ -355,6 +356,20 @@ input. prune-cache: false ``` +### Cache Python installs + +By default, the Python install dir (`uv python dir` / `UV_PYTHON_INSTALL_DIR`) is not cached, +for the same reason that the dependency cache is pruned. +If you want to cache Python installs along with your dependencies, set the `cache-python` input to `true`. + +```yaml +- name: Cache Python installs + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-python: true +``` + ### Ignore nothing to cache By default, the action will fail if caching is enabled but there is nothing to upload (the uv cache directory does not exist). diff --git a/action.yml b/action.yml index ea7133b..997ecb5 100644 --- a/action.yml +++ b/action.yml @@ -56,6 +56,9 @@ inputs: prune-cache: description: "Prune cache before saving." default: "true" + cache-python: + description: "Upload managed Python installations to the Github Actions cache." + default: "false" ignore-nothing-to-cache: description: "Ignore when nothing is found to cache." default: "false" diff --git a/dist/save-cache/index.js b/dist/save-cache/index.js index 5fc3dd8..873aedd 100644 --- a/dist/save-cache/index.js +++ b/dist/save-cache/index.js @@ -90593,8 +90593,12 @@ async function restoreCache() { } let matchedKey; core.info(`Trying to restore uv cache from GitHub Actions cache with key: ${cacheKey}`); + const cachePaths = [inputs_1.cacheLocalPath]; + if (inputs_1.cachePython) { + cachePaths.push(await (0, inputs_1.getUvPythonDir)()); + } try { - matchedKey = await cache.restoreCache([inputs_1.cacheLocalPath], cacheKey); + matchedKey = await cache.restoreCache(cachePaths, cacheKey); } catch (err) { const message = err.message; @@ -90620,7 +90624,8 @@ async function computeKeys() { const pythonVersion = await getPythonVersion(); const platform = await (0, platforms_1.getPlatform)(); const pruned = inputs_1.pruneCache ? "-pruned" : ""; - return `setup-uv-${CACHE_VERSION}-${(0, platforms_1.getArch)()}-${platform}-${pythonVersion}${pruned}${cacheDependencyPathHash}${suffix}`; + const python = inputs_1.cachePython ? "-py" : ""; + return `setup-uv-${CACHE_VERSION}-${(0, platforms_1.getArch)()}-${platform}-${pythonVersion}${pruned}${python}${cacheDependencyPathHash}${suffix}`; } async function getPythonVersion() { if (inputs_1.pythonVersion !== "") { @@ -90844,8 +90849,18 @@ async function saveCache() { if (!fs.existsSync(actualCachePath) && !inputs_1.ignoreNothingToCache) { throw new Error(`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 (inputs_1.cachePython) { + const pythonDir = await (0, inputs_1.getUvPythonDir)(); + core.info(`Including Python cache path: ${pythonDir}`); + if (!fs.existsSync(pythonDir) && !inputs_1.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([actualCachePath], cacheKey); + await cache.saveCache(cachePaths, cacheKey); core.info(`cache saved with the key: ${cacheKey}`); } catch (e) { @@ -90996,9 +91011,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = exports.pruneCache = exports.cacheDependencyGlob = exports.cacheLocalPath = exports.cacheSuffix = exports.saveCache = exports.restoreCache = exports.enableCache = exports.checkSum = exports.activateEnvironment = exports.pythonVersion = exports.versionFile = exports.version = exports.workingDirectory = void 0; +exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = exports.cachePython = exports.pruneCache = exports.cacheDependencyGlob = exports.cacheLocalPath = exports.cacheSuffix = exports.saveCache = exports.restoreCache = exports.enableCache = exports.checkSum = exports.activateEnvironment = exports.pythonVersion = exports.versionFile = exports.version = exports.workingDirectory = void 0; +exports.getUvPythonDir = getUvPythonDir; const node_path_1 = __importDefault(__nccwpck_require__(6760)); const core = __importStar(__nccwpck_require__(7484)); +const exec = __importStar(__nccwpck_require__(5236)); const config_file_1 = __nccwpck_require__(5465); exports.workingDirectory = core.getInput("working-directory"); exports.version = core.getInput("version"); @@ -91013,6 +91030,7 @@ exports.cacheSuffix = core.getInput("cache-suffix") || ""; exports.cacheLocalPath = getCacheLocalPath(); exports.cacheDependencyGlob = getCacheDependencyGlob(); exports.pruneCache = core.getInput("prune-cache") === "true"; +exports.cachePython = core.getInput("cache-python") === "true"; exports.ignoreNothingToCache = core.getInput("ignore-nothing-to-cache") === "true"; exports.ignoreEmptyWorkdir = core.getInput("ignore-empty-workdir") === "true"; exports.toolBinDir = getToolBinDir(); @@ -91106,6 +91124,20 @@ function getCacheDirFromConfig() { } return undefined; } +async function getUvPythonDir() { + if (process.env.UV_PYTHON_INSTALL_DIR !== undefined) { + core.info(`Using UV_PYTHON_INSTALL_DIR from environment: ${process.env.UV_PYTHON_INSTALL_DIR}`); + return process.env.UV_PYTHON_INSTALL_DIR; + } + core.info("Determining UV_PYTHON_INSTALL_DIR using `uv python dir`..."); + const result = await exec.getExecOutput("uv", ["python", "dir"]); + if (result.exitCode !== 0) { + throw new Error(`Failed to get uv python dir: ${result.stderr || result.stdout}`); + } + const dir = result.stdout.trim(); + core.info(`Determined UV_PYTHON_INSTALL_DIR: ${dir}`); + return dir; +} function getCacheDependencyGlob() { const cacheDependencyGlobInput = core.getInput("cache-dependency-glob"); if (cacheDependencyGlobInput !== "") { diff --git a/dist/setup/index.js b/dist/setup/index.js index f443a3e..67424dc 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -125151,8 +125151,12 @@ async function restoreCache() { } let matchedKey; core.info(`Trying to restore uv cache from GitHub Actions cache with key: ${cacheKey}`); + const cachePaths = [inputs_1.cacheLocalPath]; + if (inputs_1.cachePython) { + cachePaths.push(await (0, inputs_1.getUvPythonDir)()); + } try { - matchedKey = await cache.restoreCache([inputs_1.cacheLocalPath], cacheKey); + matchedKey = await cache.restoreCache(cachePaths, cacheKey); } catch (err) { const message = err.message; @@ -125178,7 +125182,8 @@ async function computeKeys() { const pythonVersion = await getPythonVersion(); const platform = await (0, platforms_1.getPlatform)(); const pruned = inputs_1.pruneCache ? "-pruned" : ""; - return `setup-uv-${CACHE_VERSION}-${(0, platforms_1.getArch)()}-${platform}-${pythonVersion}${pruned}${cacheDependencyPathHash}${suffix}`; + const python = inputs_1.cachePython ? "-py" : ""; + return `setup-uv-${CACHE_VERSION}-${(0, platforms_1.getArch)()}-${platform}-${pythonVersion}${pruned}${python}${cacheDependencyPathHash}${suffix}`; } async function getPythonVersion() { if (inputs_1.pythonVersion !== "") { @@ -129708,9 +129713,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = exports.pruneCache = exports.cacheDependencyGlob = exports.cacheLocalPath = exports.cacheSuffix = exports.saveCache = exports.restoreCache = exports.enableCache = exports.checkSum = exports.activateEnvironment = exports.pythonVersion = exports.versionFile = exports.version = exports.workingDirectory = void 0; +exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = exports.cachePython = exports.pruneCache = exports.cacheDependencyGlob = exports.cacheLocalPath = exports.cacheSuffix = exports.saveCache = exports.restoreCache = exports.enableCache = exports.checkSum = exports.activateEnvironment = exports.pythonVersion = exports.versionFile = exports.version = exports.workingDirectory = void 0; +exports.getUvPythonDir = getUvPythonDir; const node_path_1 = __importDefault(__nccwpck_require__(76760)); const core = __importStar(__nccwpck_require__(37484)); +const exec = __importStar(__nccwpck_require__(95236)); const config_file_1 = __nccwpck_require__(27846); exports.workingDirectory = core.getInput("working-directory"); exports.version = core.getInput("version"); @@ -129725,6 +129732,7 @@ exports.cacheSuffix = core.getInput("cache-suffix") || ""; exports.cacheLocalPath = getCacheLocalPath(); exports.cacheDependencyGlob = getCacheDependencyGlob(); exports.pruneCache = core.getInput("prune-cache") === "true"; +exports.cachePython = core.getInput("cache-python") === "true"; exports.ignoreNothingToCache = core.getInput("ignore-nothing-to-cache") === "true"; exports.ignoreEmptyWorkdir = core.getInput("ignore-empty-workdir") === "true"; exports.toolBinDir = getToolBinDir(); @@ -129818,6 +129826,20 @@ function getCacheDirFromConfig() { } return undefined; } +async function getUvPythonDir() { + if (process.env.UV_PYTHON_INSTALL_DIR !== undefined) { + core.info(`Using UV_PYTHON_INSTALL_DIR from environment: ${process.env.UV_PYTHON_INSTALL_DIR}`); + return process.env.UV_PYTHON_INSTALL_DIR; + } + core.info("Determining UV_PYTHON_INSTALL_DIR using `uv python dir`..."); + const result = await exec.getExecOutput("uv", ["python", "dir"]); + if (result.exitCode !== 0) { + throw new Error(`Failed to get uv python dir: ${result.stderr || result.stdout}`); + } + const dir = result.stdout.trim(); + core.info(`Determined UV_PYTHON_INSTALL_DIR: ${dir}`); + return dir; +} function getCacheDependencyGlob() { const cacheDependencyGlobInput = core.getInput("cache-dependency-glob"); if (cacheDependencyGlobInput !== "") { diff --git a/src/cache/restore-cache.ts b/src/cache/restore-cache.ts index fac9f55..3c32c04 100644 --- a/src/cache/restore-cache.ts +++ b/src/cache/restore-cache.ts @@ -5,7 +5,9 @@ import { hashFiles } from "../hash/hash-files"; import { cacheDependencyGlob, cacheLocalPath, + cachePython, cacheSuffix, + getUvPythonDir, pruneCache, pythonVersion as pythonVersionInput, restoreCache as shouldRestoreCache, @@ -30,8 +32,12 @@ export async function restoreCache(): Promise { core.info( `Trying to restore uv cache from GitHub Actions cache with key: ${cacheKey}`, ); + const cachePaths = [cacheLocalPath]; + if (cachePython) { + cachePaths.push(await getUvPythonDir()); + } try { - matchedKey = await cache.restoreCache([cacheLocalPath], cacheKey); + matchedKey = await cache.restoreCache(cachePaths, cacheKey); } catch (err) { const message = (err as Error).message; core.warning(message); @@ -62,7 +68,8 @@ async function computeKeys(): Promise { const pythonVersion = await getPythonVersion(); const platform = await getPlatform(); const pruned = pruneCache ? "-pruned" : ""; - return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${pythonVersion}${pruned}${cacheDependencyPathHash}${suffix}`; + const python = cachePython ? "-py" : ""; + return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${pythonVersion}${pruned}${python}${cacheDependencyPathHash}${suffix}`; } async function getPythonVersion(): Promise { diff --git a/src/save-cache.ts b/src/save-cache.ts index 45cc62a..2e9f684 100644 --- a/src/save-cache.ts +++ b/src/save-cache.ts @@ -10,7 +10,9 @@ import { import { STATE_UV_PATH, STATE_UV_VERSION } from "./utils/constants"; import { cacheLocalPath, + cachePython, enableCache, + getUvPythonDir, ignoreNothingToCache, pruneCache as shouldPruneCache, saveCache as shouldSaveCache, @@ -68,8 +70,22 @@ async function saveCache(): Promise { `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) { + const pythonDir = await getUvPythonDir(); + 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([actualCachePath], cacheKey); + await cache.saveCache(cachePaths, cacheKey); core.info(`cache saved with the key: ${cacheKey}`); } catch (e) { if ( diff --git a/src/utils/inputs.ts b/src/utils/inputs.ts index faf2f59..2d93953 100644 --- a/src/utils/inputs.ts +++ b/src/utils/inputs.ts @@ -1,5 +1,6 @@ import path from "node:path"; import * as core from "@actions/core"; +import * as exec from "@actions/exec"; import { getConfigValueFromTomlFile } from "./config-file"; export const workingDirectory = core.getInput("working-directory"); @@ -15,6 +16,7 @@ export const cacheSuffix = core.getInput("cache-suffix") || ""; export const cacheLocalPath = getCacheLocalPath(); export const cacheDependencyGlob = getCacheDependencyGlob(); export const pruneCache = core.getInput("prune-cache") === "true"; +export const cachePython = core.getInput("cache-python") === "true"; export const ignoreNothingToCache = core.getInput("ignore-nothing-to-cache") === "true"; export const ignoreEmptyWorkdir = @@ -123,6 +125,25 @@ function getCacheDirFromConfig(): string | undefined { return undefined; } +export async function getUvPythonDir(): Promise { + if (process.env.UV_PYTHON_INSTALL_DIR !== undefined) { + core.info( + `Using UV_PYTHON_INSTALL_DIR from environment: ${process.env.UV_PYTHON_INSTALL_DIR}`, + ); + return process.env.UV_PYTHON_INSTALL_DIR; + } + core.info("Determining UV_PYTHON_INSTALL_DIR using `uv python dir`..."); + const result = await exec.getExecOutput("uv", ["python", "dir"]); + if (result.exitCode !== 0) { + throw new Error( + `Failed to get uv python dir: ${result.stderr || result.stdout}`, + ); + } + const dir = result.stdout.trim(); + core.info(`Determined UV_PYTHON_INSTALL_DIR: ${dir}`); + return dir; +} + function getCacheDependencyGlob(): string { const cacheDependencyGlobInput = core.getInput("cache-dependency-glob"); if (cacheDependencyGlobInput !== "") {