mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-11-05 01:35:32 +00:00
Repair cached Python directory junctions on Windows (#1122)
With `cache-python: true`, a Windows runner can report a cache hit while extracting the cache incorrectly. When `@actions/cache`'s tar extracts uv's minor-version junction before the patch directory it points to, it creates a file symlink. Resolving that link later raises `[WinError 5] Access is denied`. This PR fixes things up after restoring the Python cache, recreating the junction if resolving the link fails with access denied and its recorded target is a matching, accessible CPython installation in the same directory. This is a temporary workaround until https://github.com/actions/toolkit/issues/2562 gets resolved and `@actions/cache` itself becomes resilient to these situations. I tested this by restoring the same archive on two fresh `windows-2022` runners. The original run passed because resolving the minor-version link raised the expected WinError 5. With the patch, `uv sync --offline --managed-python --no-python-downloads` created a virtual environment whose `home` in `pyvenv.cfg` resolved to the cached CPython 3.10.22 installation. [Original run](https://github.com/astral-sh/ruff/actions/runs/41469344610/job/125358623949#step:9:1) · [Patched run](https://github.com/astral-sh/ruff/actions/runs/41469344610/job/125358623835#step:10:1).
This commit is contained in:
+27
-16
@@ -985,14 +985,18 @@ jobs:
|
|||||||
run: rm -rf "$UV_PYTHON_INSTALL_DIR"
|
run: rm -rf "$UV_PYTHON_INSTALL_DIR"
|
||||||
|
|
||||||
test-cache-python-installs:
|
test-cache-python-installs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-2022]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Verify Python install dir is not populated
|
- name: Verify Python install dir is not populated
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ -d /home/runner/work/_temp/uv-python-dir ]; then
|
if [ -e "$RUNNER_TEMP/uv-python-dir" ]; then
|
||||||
echo "Python install dir should not exist"
|
echo "Python install dir should not exist"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -1005,21 +1009,26 @@ jobs:
|
|||||||
- run: uv sync --managed-python
|
- run: uv sync --managed-python
|
||||||
working-directory: __tests__/fixtures/uv-project
|
working-directory: __tests__/fixtures/uv-project
|
||||||
- name: Verify Python install dir exists
|
- name: Verify Python install dir exists
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ ! -d /home/runner/work/_temp/uv-python-dir ]; then
|
if [ ! -d "$UV_PYTHON_INSTALL_DIR" ]; then
|
||||||
echo "Python install dir should exist"
|
echo "Python install dir should exist"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
test-restore-python-installs:
|
test-restore-python-installs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
needs: test-cache-python-installs
|
needs: test-cache-python-installs
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-2022]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Verify Python install dir does not exist
|
- name: Verify Python install dir does not exist
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ -d /home/runner/work/_temp/uv-python-dir ]; then
|
if [ -e "$RUNNER_TEMP/uv-python-dir" ]; then
|
||||||
echo "Python install dir should not exist"
|
echo "Python install dir should not exist"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -1030,19 +1039,21 @@ jobs:
|
|||||||
enable-cache: true
|
enable-cache: true
|
||||||
cache-python: true
|
cache-python: true
|
||||||
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-cache-python-installs
|
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-cache-python-installs
|
||||||
- name: Verify Python install dir exists
|
- name: Verify the restored Python minor-version link resolves to the patch directory
|
||||||
run: |
|
shell: python
|
||||||
if [ ! -d /home/runner/work/_temp/uv-python-dir ]; then
|
|
||||||
echo "Python install dir should exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- name: Cache was hit
|
|
||||||
run: |
|
|
||||||
if [ "$CACHE_HIT" != "true" ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
env:
|
env:
|
||||||
CACHE_HIT: ${{ steps.restore.outputs.python-cache-hit }}
|
CACHE_HIT: ${{ steps.restore.outputs.python-cache-hit }}
|
||||||
|
run: |
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
assert os.environ["CACHE_HIT"] == "true", "expected a Python cache hit"
|
||||||
|
installs = Path(os.environ["UV_PYTHON_INSTALL_DIR"]).resolve()
|
||||||
|
assert installs.is_dir(), "Python install dir should exist"
|
||||||
|
patch_dir, = installs.glob("cpython-*.*.*-*")
|
||||||
|
minor_prefix = patch_dir.name.rsplit(".", 1)[0]
|
||||||
|
alias, = installs.glob(f"{minor_prefix}-*")
|
||||||
|
assert alias.resolve(strict=True) == patch_dir, alias
|
||||||
- run: uv sync --managed-python
|
- run: uv sync --managed-python
|
||||||
working-directory: __tests__/fixtures/uv-project
|
working-directory: __tests__/fixtures/uv-project
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
|
|||||||
- `venv`: Path to the activated venv if activate-environment is true.
|
- `venv`: Path to the activated venv if activate-environment is true.
|
||||||
- `python-version`: The Python version that was set.
|
- `python-version`: The Python version that was set.
|
||||||
- `python-runtime-id`: An opaque identifier reported by uv for the activated venv's Python runtime. Empty when `activate-environment` is false.
|
- `python-runtime-id`: An opaque identifier reported by uv for the activated venv's Python runtime. Empty when `activate-environment` is false.
|
||||||
- `python-cache-hit`: A boolean value to indicate the Python cache entry was found.
|
- `python-cache-hit`: A boolean indicating whether the Python cache was restored successfully.
|
||||||
|
|
||||||
### Python version
|
### Python version
|
||||||
|
|
||||||
|
|||||||
+37
@@ -1,3 +1,4 @@
|
|||||||
|
import { fileURLToPath } from "node:url";
|
||||||
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
||||||
import { createSetupInputs } from "../helpers/setup-inputs";
|
import { createSetupInputs } from "../helpers/setup-inputs";
|
||||||
|
|
||||||
@@ -48,6 +49,42 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("restoreCache", () => {
|
describe("restoreCache", () => {
|
||||||
|
it.each([
|
||||||
|
["win32", false],
|
||||||
|
["linux", true],
|
||||||
|
] as const)(
|
||||||
|
"keeps the matched Python cache key on %s when the install directory cannot be read",
|
||||||
|
async (platform, pythonCacheHit) => {
|
||||||
|
const inputs = createSetupInputs({
|
||||||
|
cachePython: true,
|
||||||
|
// A file at the install directory path makes readdir fail.
|
||||||
|
pythonDir: fileURLToPath(import.meta.url),
|
||||||
|
restoreCache: true,
|
||||||
|
});
|
||||||
|
mockRestoreCache
|
||||||
|
.mockReturnValueOnce("uv-key")
|
||||||
|
.mockReturnValueOnce("python-key");
|
||||||
|
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, "platform", { value: platform });
|
||||||
|
try {
|
||||||
|
await restoreCache(inputs);
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockSetOutput).toHaveBeenCalledWith("cache-hit", true);
|
||||||
|
expect(mockSetOutput).toHaveBeenCalledWith(
|
||||||
|
"python-cache-hit",
|
||||||
|
pythonCacheHit,
|
||||||
|
);
|
||||||
|
expect(mockSaveState).toHaveBeenCalledWith(
|
||||||
|
"python-cache-matched-key",
|
||||||
|
"python-key",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("encodes Python version ranges before adding them to the cache key", async () => {
|
it("encodes Python version ranges before adding them to the cache key", async () => {
|
||||||
await restoreCache(createSetupInputs(), ">3.10.11,<3.11");
|
await restoreCache(createSetupInputs(), ">3.10.11,<3.11");
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -113,7 +113,7 @@ outputs:
|
|||||||
python-runtime-id:
|
python-runtime-id:
|
||||||
description: "An opaque identifier reported by uv for the activated venv's Python runtime. Empty when activate-environment is false."
|
description: "An opaque identifier reported by uv for the activated venv's Python runtime. Empty when activate-environment is false."
|
||||||
python-cache-hit:
|
python-cache-hit:
|
||||||
description: "A boolean value to indicate the Python cache entry was found"
|
description: "A boolean indicating whether the Python cache was restored successfully."
|
||||||
runs:
|
runs:
|
||||||
using: "node24"
|
using: "node24"
|
||||||
main: "dist/setup/index.cjs"
|
main: "dist/setup/index.cjs"
|
||||||
|
|||||||
+77
-29
@@ -93966,6 +93966,53 @@ function getWindowsNameVersion() {
|
|||||||
return `windows-${match4[1]}`;
|
return `windows-${match4[1]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/cache/repair-python-junctions.ts
|
||||||
|
var import_promises = require("node:fs/promises");
|
||||||
|
var import_node_path = __toESM(require("node:path"), 1);
|
||||||
|
async function repairPythonJunctions(pythonDir) {
|
||||||
|
const entries = await (0, import_promises.readdir)(pythonDir, { withFileTypes: true });
|
||||||
|
const directories = new Set(
|
||||||
|
entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name.toLowerCase())
|
||||||
|
);
|
||||||
|
for (const entry of entries) {
|
||||||
|
const name = entry.name.toLowerCase();
|
||||||
|
if (!entry.isSymbolicLink() || !/^cpython-\d+\.\d+[+-].+$/.test(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const link = import_node_path.default.join(pythonDir, entry.name);
|
||||||
|
let target;
|
||||||
|
try {
|
||||||
|
target = import_node_path.default.resolve(pythonDir, await (0, import_promises.readlink)(link));
|
||||||
|
} catch (err) {
|
||||||
|
if (await resolvesWithAccessDenied(link)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const sibling = import_node_path.default.relative(pythonDir, target).toLowerCase();
|
||||||
|
const version3 = /^(cpython-\d+\.\d+)\.\d+(?:(?:a|b|rc)\d+)?([+-].+)$/.exec(
|
||||||
|
sibling
|
||||||
|
);
|
||||||
|
if (!version3 || version3[1] + version3[2] !== name || !directories.has(sibling)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!await resolvesWithAccessDenied(link)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await (0, import_promises.realpath)(target);
|
||||||
|
await (0, import_promises.unlink)(link);
|
||||||
|
await (0, import_promises.symlink)(target, link, "junction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function resolvesWithAccessDenied(link) {
|
||||||
|
try {
|
||||||
|
await (0, import_promises.realpath)(link);
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
return err.code === "EPERM";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// src/cache/restore-cache.ts
|
// src/cache/restore-cache.ts
|
||||||
var STATE_CACHE_KEY = "cache-key";
|
var STATE_CACHE_KEY = "cache-key";
|
||||||
var STATE_CACHE_MATCHED_KEY = "cache-matched-key";
|
var STATE_CACHE_MATCHED_KEY = "cache-matched-key";
|
||||||
@@ -93996,26 +94043,37 @@ async function restoreCache2(inputs, pythonVersion) {
|
|||||||
`${cacheKey}-python`,
|
`${cacheKey}-python`,
|
||||||
inputs.pythonDir,
|
inputs.pythonDir,
|
||||||
STATE_PYTHON_CACHE_MATCHED_KEY,
|
STATE_PYTHON_CACHE_MATCHED_KEY,
|
||||||
"python-cache-hit"
|
"python-cache-hit",
|
||||||
|
// TODO: Remove this repair once @actions/cache preserves Windows junctions.
|
||||||
|
// https://github.com/actions/toolkit/issues/2562
|
||||||
|
process.platform === "win32" ? repairPythonJunctions : void 0
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setOutput("python-cache-hit", false);
|
setOutput("python-cache-hit", false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function restoreCacheFromKey(cacheKey, cachePath, stateKey, outputKey) {
|
async function restoreCacheFromKey(cacheKey, cachePath, stateKey, outputKey, afterRestore) {
|
||||||
info2(
|
info2(
|
||||||
`Trying to restore cache from GitHub Actions cache with key: ${cacheKey}`
|
`Trying to restore cache from GitHub Actions cache with key: ${cacheKey}`
|
||||||
);
|
);
|
||||||
let matchedKey;
|
|
||||||
try {
|
try {
|
||||||
matchedKey = await restoreCache([cachePath], cacheKey);
|
const matchedKey = await restoreCache([cachePath], cacheKey);
|
||||||
|
if (!matchedKey) {
|
||||||
|
info2(`No GitHub Actions cache found for key: ${cacheKey}`);
|
||||||
|
setOutput(outputKey, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveState(stateKey, matchedKey);
|
||||||
|
await afterRestore?.(cachePath);
|
||||||
|
info2(
|
||||||
|
`cache restored from GitHub Actions cache with key: ${matchedKey}`
|
||||||
|
);
|
||||||
|
setOutput(outputKey, true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err.message;
|
const message = err.message;
|
||||||
warning2(message);
|
warning2(message);
|
||||||
setOutput(outputKey, false);
|
setOutput(outputKey, false);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
handleMatchResult(matchedKey, cacheKey, stateKey, outputKey);
|
|
||||||
}
|
}
|
||||||
async function computeKeys(inputs, pythonVersion) {
|
async function computeKeys(inputs, pythonVersion) {
|
||||||
let cacheDependencyPathHash = "-";
|
let cacheDependencyPathHash = "-";
|
||||||
@@ -94045,16 +94103,6 @@ async function computeKeys(inputs, pythonVersion) {
|
|||||||
const python = inputs.cachePython ? "-py" : "";
|
const python = inputs.cachePython ? "-py" : "";
|
||||||
return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform2}-${osNameVersion}-${version3}${pythonArchKey}${pruned}${python}${cacheDependencyPathHash}${suffix}`;
|
return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform2}-${osNameVersion}-${version3}${pythonArchKey}${pruned}${python}${cacheDependencyPathHash}${suffix}`;
|
||||||
}
|
}
|
||||||
function handleMatchResult(matchedKey, primaryKey, stateKey, outputKey) {
|
|
||||||
if (!matchedKey) {
|
|
||||||
info2(`No GitHub Actions cache found for key: ${primaryKey}`);
|
|
||||||
setOutput(outputKey, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saveState(stateKey, matchedKey);
|
|
||||||
info2(`cache restored from GitHub Actions cache with key: ${matchedKey}`);
|
|
||||||
setOutput(outputKey, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/download/download-version.ts
|
// src/download/download-version.ts
|
||||||
var import_node_fs7 = require("node:fs");
|
var import_node_fs7 = require("node:fs");
|
||||||
@@ -102164,7 +102212,7 @@ function getExtension(platform2) {
|
|||||||
|
|
||||||
// src/utils/inputs.ts
|
// src/utils/inputs.ts
|
||||||
var import_node_fs8 = __toESM(require("node:fs"), 1);
|
var import_node_fs8 = __toESM(require("node:fs"), 1);
|
||||||
var import_node_path = __toESM(require("node:path"), 1);
|
var import_node_path2 = __toESM(require("node:path"), 1);
|
||||||
function loadInputs() {
|
function loadInputs() {
|
||||||
const workingDirectory = getInput("working-directory");
|
const workingDirectory = getInput("working-directory");
|
||||||
const version3 = getInput("version");
|
const version3 = getInput("version");
|
||||||
@@ -102310,7 +102358,7 @@ function getToolBinDir(workingDirectory) {
|
|||||||
}
|
}
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
if (process.env.RUNNER_TEMP !== void 0) {
|
if (process.env.RUNNER_TEMP !== void 0) {
|
||||||
return `${process.env.RUNNER_TEMP}${import_node_path.default.sep}uv-tool-bin-dir`;
|
return `${process.env.RUNNER_TEMP}${import_node_path2.default.sep}uv-tool-bin-dir`;
|
||||||
}
|
}
|
||||||
throw Error(
|
throw Error(
|
||||||
"Could not determine UV_TOOL_BIN_DIR. Please make sure RUNNER_TEMP is set or provide the tool-bin-dir input"
|
"Could not determine UV_TOOL_BIN_DIR. Please make sure RUNNER_TEMP is set or provide the tool-bin-dir input"
|
||||||
@@ -102326,7 +102374,7 @@ function getToolDir(workingDirectory) {
|
|||||||
}
|
}
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
if (process.env.RUNNER_TEMP !== void 0) {
|
if (process.env.RUNNER_TEMP !== void 0) {
|
||||||
return `${process.env.RUNNER_TEMP}${import_node_path.default.sep}uv-tool-dir`;
|
return `${process.env.RUNNER_TEMP}${import_node_path2.default.sep}uv-tool-dir`;
|
||||||
}
|
}
|
||||||
throw Error(
|
throw Error(
|
||||||
"Could not determine UV_TOOL_DIR. Please make sure RUNNER_TEMP is set or provide the tool-dir input"
|
"Could not determine UV_TOOL_DIR. Please make sure RUNNER_TEMP is set or provide the tool-dir input"
|
||||||
@@ -102358,7 +102406,7 @@ function getCacheLocalPath(workingDirectory, versionFile, enableCache) {
|
|||||||
if (process.env.RUNNER_ENVIRONMENT === "github-hosted") {
|
if (process.env.RUNNER_ENVIRONMENT === "github-hosted") {
|
||||||
if (process.env.RUNNER_TEMP !== void 0) {
|
if (process.env.RUNNER_TEMP !== void 0) {
|
||||||
return {
|
return {
|
||||||
path: `${process.env.RUNNER_TEMP}${import_node_path.default.sep}setup-uv-cache`,
|
path: `${process.env.RUNNER_TEMP}${import_node_path2.default.sep}setup-uv-cache`,
|
||||||
source: 3 /* Default */
|
source: 3 /* Default */
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -102368,12 +102416,12 @@ function getCacheLocalPath(workingDirectory, versionFile, enableCache) {
|
|||||||
}
|
}
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
return {
|
return {
|
||||||
path: `${process.env.APPDATA}${import_node_path.default.sep}uv${import_node_path.default.sep}cache`,
|
path: `${process.env.APPDATA}${import_node_path2.default.sep}uv${import_node_path2.default.sep}cache`,
|
||||||
source: 3 /* Default */
|
source: 3 /* Default */
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
path: `${process.env.HOME}${import_node_path.default.sep}.cache${import_node_path.default.sep}uv`,
|
path: `${process.env.HOME}${import_node_path2.default.sep}.cache${import_node_path2.default.sep}uv`,
|
||||||
source: 3 /* Default */
|
source: 3 /* Default */
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -102404,12 +102452,12 @@ function getUvPythonDir() {
|
|||||||
}
|
}
|
||||||
if (process.env.RUNNER_ENVIRONMENT !== "github-hosted") {
|
if (process.env.RUNNER_ENVIRONMENT !== "github-hosted") {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
return `${process.env.APPDATA}${import_node_path.default.sep}uv${import_node_path.default.sep}python`;
|
return `${process.env.APPDATA}${import_node_path2.default.sep}uv${import_node_path2.default.sep}python`;
|
||||||
}
|
}
|
||||||
return `${process.env.HOME}${import_node_path.default.sep}.local${import_node_path.default.sep}share${import_node_path.default.sep}uv${import_node_path.default.sep}python`;
|
return `${process.env.HOME}${import_node_path2.default.sep}.local${import_node_path2.default.sep}share${import_node_path2.default.sep}uv${import_node_path2.default.sep}python`;
|
||||||
}
|
}
|
||||||
if (process.env.RUNNER_TEMP !== void 0) {
|
if (process.env.RUNNER_TEMP !== void 0) {
|
||||||
return `${process.env.RUNNER_TEMP}${import_node_path.default.sep}uv-python-dir`;
|
return `${process.env.RUNNER_TEMP}${import_node_path2.default.sep}uv-python-dir`;
|
||||||
}
|
}
|
||||||
throw Error(
|
throw Error(
|
||||||
"Could not determine UV_PYTHON_INSTALL_DIR. Please make sure RUNNER_TEMP is set or provide the UV_PYTHON_INSTALL_DIR environment variable"
|
"Could not determine UV_PYTHON_INSTALL_DIR. Please make sure RUNNER_TEMP is set or provide the UV_PYTHON_INSTALL_DIR environment variable"
|
||||||
@@ -102429,10 +102477,10 @@ function expandTilde(input) {
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
function normalizePath(inputPath) {
|
function normalizePath(inputPath) {
|
||||||
const normalized = import_node_path.default.normalize(inputPath);
|
const normalized = import_node_path2.default.normalize(inputPath);
|
||||||
const root = import_node_path.default.parse(normalized).root;
|
const root = import_node_path2.default.parse(normalized).root;
|
||||||
let trimmed = normalized;
|
let trimmed = normalized;
|
||||||
while (trimmed.length > root.length && trimmed.endsWith(import_node_path.default.sep)) {
|
while (trimmed.length > root.length && trimmed.endsWith(import_node_path2.default.sep)) {
|
||||||
trimmed = trimmed.slice(0, -1);
|
trimmed = trimmed.slice(0, -1);
|
||||||
}
|
}
|
||||||
return trimmed;
|
return trimmed;
|
||||||
@@ -102440,7 +102488,7 @@ function normalizePath(inputPath) {
|
|||||||
function resolveRelativePath(workingDirectory, inputPath) {
|
function resolveRelativePath(workingDirectory, inputPath) {
|
||||||
const hasNegation = inputPath.startsWith("!");
|
const hasNegation = inputPath.startsWith("!");
|
||||||
const pathWithoutNegation = hasNegation ? inputPath.substring(1) : inputPath;
|
const pathWithoutNegation = hasNegation ? inputPath.substring(1) : inputPath;
|
||||||
const resolvedPath = import_node_path.default.resolve(workingDirectory, pathWithoutNegation);
|
const resolvedPath = import_node_path2.default.resolve(workingDirectory, pathWithoutNegation);
|
||||||
debug(
|
debug(
|
||||||
`Resolving relative path ${inputPath} to ${hasNegation ? "!" : ""}${resolvedPath}`
|
`Resolving relative path ${inputPath} to ${hasNegation ? "!" : ""}${resolvedPath}`
|
||||||
);
|
);
|
||||||
|
|||||||
Vendored
+60
@@ -0,0 +1,60 @@
|
|||||||
|
import { readdir, readlink, realpath, symlink, unlink } from "node:fs/promises";
|
||||||
|
import nodePath from "node:path";
|
||||||
|
|
||||||
|
export async function repairPythonJunctions(pythonDir: string): Promise<void> {
|
||||||
|
const entries = await readdir(pythonDir, { withFileTypes: true });
|
||||||
|
const directories = new Set(
|
||||||
|
entries
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => entry.name.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const name = entry.name.toLowerCase();
|
||||||
|
if (!entry.isSymbolicLink() || !/^cpython-\d+\.\d+[+-].+$/.test(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const link = nodePath.join(pythonDir, entry.name);
|
||||||
|
let target: string;
|
||||||
|
try {
|
||||||
|
target = nodePath.resolve(pythonDir, await readlink(link));
|
||||||
|
} catch (err) {
|
||||||
|
// Fail only if the link still has the access error that requires repair.
|
||||||
|
if (await resolvesWithAccessDenied(link)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// uv omits only the patch and prerelease from its minor-version links.
|
||||||
|
const sibling = nodePath.relative(pythonDir, target).toLowerCase();
|
||||||
|
const version = /^(cpython-\d+\.\d+)\.\d+(?:(?:a|b|rc)\d+)?([+-].+)$/.exec(
|
||||||
|
sibling,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!version ||
|
||||||
|
version[1] + version[2] !== name ||
|
||||||
|
!directories.has(sibling)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Git for Windows tar can restore a junction as a file symlink when it
|
||||||
|
// extracts the link before its target. Opening it as a directory fails.
|
||||||
|
if (!(await resolvesWithAccessDenied(link))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// An inaccessible installation can also cause EPERM through a valid link.
|
||||||
|
await realpath(target);
|
||||||
|
await unlink(link);
|
||||||
|
await symlink(target, link, "junction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolvesWithAccessDenied(link: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await realpath(link);
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
return (err as NodeJS.ErrnoException).code === "EPERM";
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+21
-22
@@ -4,6 +4,7 @@ import { hashFiles } from "../hash/hash-files";
|
|||||||
import type { SetupInputs } from "../utils/inputs";
|
import type { SetupInputs } from "../utils/inputs";
|
||||||
import * as log from "../utils/logging";
|
import * as log from "../utils/logging";
|
||||||
import { getArch, getOSNameVersion, getPlatform } from "../utils/platforms";
|
import { getArch, getOSNameVersion, getPlatform } from "../utils/platforms";
|
||||||
|
import { repairPythonJunctions } from "./repair-python-junctions";
|
||||||
|
|
||||||
export const STATE_CACHE_KEY = "cache-key";
|
export const STATE_CACHE_KEY = "cache-key";
|
||||||
export const STATE_CACHE_MATCHED_KEY = "cache-matched-key";
|
export const STATE_CACHE_MATCHED_KEY = "cache-matched-key";
|
||||||
@@ -44,6 +45,9 @@ export async function restoreCache(
|
|||||||
inputs.pythonDir,
|
inputs.pythonDir,
|
||||||
STATE_PYTHON_CACHE_MATCHED_KEY,
|
STATE_PYTHON_CACHE_MATCHED_KEY,
|
||||||
"python-cache-hit",
|
"python-cache-hit",
|
||||||
|
// TODO: Remove this repair once @actions/cache preserves Windows junctions.
|
||||||
|
// https://github.com/actions/toolkit/issues/2562
|
||||||
|
process.platform === "win32" ? repairPythonJunctions : undefined,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
core.setOutput("python-cache-hit", false);
|
core.setOutput("python-cache-hit", false);
|
||||||
@@ -55,21 +59,33 @@ async function restoreCacheFromKey(
|
|||||||
cachePath: string,
|
cachePath: string,
|
||||||
stateKey: string,
|
stateKey: string,
|
||||||
outputKey: string,
|
outputKey: string,
|
||||||
|
afterRestore?: (cachePath: string) => Promise<void>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
log.info(
|
log.info(
|
||||||
`Trying to restore cache from GitHub Actions cache with key: ${cacheKey}`,
|
`Trying to restore cache from GitHub Actions cache with key: ${cacheKey}`,
|
||||||
);
|
);
|
||||||
let matchedKey: string | undefined;
|
|
||||||
try {
|
try {
|
||||||
matchedKey = await cache.restoreCache([cachePath], cacheKey);
|
const matchedKey = await cache.restoreCache([cachePath], cacheKey);
|
||||||
|
if (!matchedKey) {
|
||||||
|
log.info(`No GitHub Actions cache found for key: ${cacheKey}`);
|
||||||
|
core.setOutput(outputKey, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the match before the post-restore hook so the post step can skip
|
||||||
|
// archiving the directory again even if the hook fails. A PR that restored
|
||||||
|
// from its base branch could otherwise save the broken copy in its own cache.
|
||||||
|
core.saveState(stateKey, matchedKey);
|
||||||
|
await afterRestore?.(cachePath);
|
||||||
|
log.info(
|
||||||
|
`cache restored from GitHub Actions cache with key: ${matchedKey}`,
|
||||||
|
);
|
||||||
|
core.setOutput(outputKey, true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = (err as Error).message;
|
const message = (err as Error).message;
|
||||||
log.warning(message);
|
log.warning(message);
|
||||||
core.setOutput(outputKey, false);
|
core.setOutput(outputKey, false);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMatchResult(matchedKey, cacheKey, stateKey, outputKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function computeKeys(
|
async function computeKeys(
|
||||||
@@ -107,20 +123,3 @@ async function computeKeys(
|
|||||||
const python = inputs.cachePython ? "-py" : "";
|
const python = inputs.cachePython ? "-py" : "";
|
||||||
return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${osNameVersion}-${version}${pythonArchKey}${pruned}${python}${cacheDependencyPathHash}${suffix}`;
|
return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${osNameVersion}-${version}${pythonArchKey}${pruned}${python}${cacheDependencyPathHash}${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMatchResult(
|
|
||||||
matchedKey: string | undefined,
|
|
||||||
primaryKey: string,
|
|
||||||
stateKey: string,
|
|
||||||
outputKey: string,
|
|
||||||
): void {
|
|
||||||
if (!matchedKey) {
|
|
||||||
log.info(`No GitHub Actions cache found for key: ${primaryKey}`);
|
|
||||||
core.setOutput(outputKey, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
core.saveState(stateKey, matchedKey);
|
|
||||||
log.info(`cache restored from GitHub Actions cache with key: ${matchedKey}`);
|
|
||||||
core.setOutput(outputKey, true);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user