mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-09-04 14:39:28 +00:00
Expose Python runtime identity for downstream caching
This commit is contained in:
+31
-11
@@ -347,14 +347,14 @@ jobs:
|
|||||||
if [ "$PYTHON_VERSION" != "3.13.1t" ]; then
|
if [ "$PYTHON_VERSION" != "3.13.1t" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ -n "$PYTHON_VERSION_RESOLVED" ]; then
|
if [ -n "$PYTHON_RUNTIME_ID" ]; then
|
||||||
echo "python-version-resolved should be empty without environment activation"
|
echo "python-runtime-id should be empty without environment activation"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: ${{ steps.setup-uv.outputs.python-version }}
|
PYTHON_VERSION: ${{ steps.setup-uv.outputs.python-version }}
|
||||||
PYTHON_VERSION_RESOLVED: ${{ steps.setup-uv.outputs.python-version-resolved }}
|
PYTHON_RUNTIME_ID: ${{ steps.setup-uv.outputs.python-runtime-id }}
|
||||||
- run: uv sync
|
- run: uv sync
|
||||||
working-directory: __tests__/fixtures/uv-project
|
working-directory: __tests__/fixtures/uv-project
|
||||||
|
|
||||||
@@ -369,6 +369,8 @@ jobs:
|
|||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python-version: "3.14.0rc2"
|
python-version: "3.14.0rc2"
|
||||||
|
- os: ubuntu-latest
|
||||||
|
python-version: "3.14.0rc2t"
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python-version: "pypy3.11"
|
python-version: "pypy3.11"
|
||||||
steps:
|
steps:
|
||||||
@@ -384,9 +386,27 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
- name: Verify Python version outputs and cache key
|
- name: Verify Python version outputs and cache key
|
||||||
run: |
|
run: |
|
||||||
expected=$(python -I -c 'import platform; print(platform.python_version())')
|
expected=$(python -I - <<'PY'
|
||||||
if [ "$PYTHON_VERSION_RESOLVED" != "$expected" ]; then
|
import platform
|
||||||
echo "Wrong resolved Python version: $PYTHON_VERSION_RESOLVED (expected $expected)"
|
import sys
|
||||||
|
import sysconfig
|
||||||
|
|
||||||
|
if sys.implementation.name == "cpython":
|
||||||
|
runtime = f"cpython-{platform.python_version()}"
|
||||||
|
else:
|
||||||
|
version = sys.implementation.version
|
||||||
|
implementation_version = f"{version.major}.{version.minor}.{version.micro}"
|
||||||
|
suffix = {"alpha": "a", "beta": "b", "candidate": "rc", "final": ""}[version.releaselevel]
|
||||||
|
if suffix:
|
||||||
|
implementation_version += f"{suffix}{version.serial}"
|
||||||
|
runtime = f"{sys.implementation.name}-{implementation_version}-python-{platform.python_version()}"
|
||||||
|
if sysconfig.get_config_var("Py_GIL_DISABLED") == 1:
|
||||||
|
runtime += "-freethreaded"
|
||||||
|
print(runtime)
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
if [ "$PYTHON_RUNTIME_ID" != "$expected" ]; then
|
||||||
|
echo "Wrong Python runtime ID: $PYTHON_RUNTIME_ID (expected $expected)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ "$PYTHON_VERSION" != "$PYTHON_REQUEST" ]; then
|
if [ "$PYTHON_VERSION" != "$PYTHON_REQUEST" ]; then
|
||||||
@@ -402,7 +422,7 @@ jobs:
|
|||||||
CACHE_KEY: ${{ steps.setup-uv.outputs.cache-key }}
|
CACHE_KEY: ${{ steps.setup-uv.outputs.cache-key }}
|
||||||
PYTHON_REQUEST: ${{ matrix.python-version }}
|
PYTHON_REQUEST: ${{ matrix.python-version }}
|
||||||
PYTHON_VERSION: ${{ steps.setup-uv.outputs.python-version }}
|
PYTHON_VERSION: ${{ steps.setup-uv.outputs.python-version }}
|
||||||
PYTHON_VERSION_RESOLVED: ${{ steps.setup-uv.outputs.python-version-resolved }}
|
PYTHON_RUNTIME_ID: ${{ steps.setup-uv.outputs.python-runtime-id }}
|
||||||
- name: Verify output venv is set
|
- name: Verify output venv is set
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$UV_VENV" ]; then
|
if [ -z "$UV_VENV" ]; then
|
||||||
@@ -467,15 +487,15 @@ jobs:
|
|||||||
raise SystemExit(f"Python is not running from custom venv: {sys.executable}")
|
raise SystemExit(f"Python is not running from custom venv: {sys.executable}")
|
||||||
PY
|
PY
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Verify resolved Python version from custom venv
|
- name: Verify Python runtime ID from custom venv
|
||||||
run: |
|
run: |
|
||||||
if [ "$PYTHON_VERSION_RESOLVED" != "3.13.1" ]; then
|
if [ "$PYTHON_RUNTIME_ID" != "cpython-3.13.1-freethreaded" ]; then
|
||||||
echo "Wrong resolved Python version: $PYTHON_VERSION_RESOLVED"
|
echo "Wrong Python runtime ID: $PYTHON_RUNTIME_ID"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION_RESOLVED: ${{ steps.setup-uv.outputs.python-version-resolved }}
|
PYTHON_RUNTIME_ID: ${{ steps.setup-uv.outputs.python-runtime-id }}
|
||||||
|
|
||||||
test-activate-environment-no-project:
|
test-activate-environment-no-project:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -138,8 +138,9 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
|
|||||||
- `cache-hit`: A boolean value to indicate a cache entry was found.
|
- `cache-hit`: A boolean value to indicate a cache entry was found.
|
||||||
- `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-version-resolved`: The full Python version of the activated venv, including prerelease suffixes.
|
- `python-runtime-id`: An opaque identifier for the activated venv's Python runtime, including
|
||||||
Empty when `activate-environment` is false or the version cannot be determined.
|
implementation, full version, and free-threaded build information. Useful as a cache-key component.
|
||||||
|
Empty when `activate-environment` is false or the runtime cannot be determined.
|
||||||
- `python-cache-hit`: A boolean value to indicate the Python cache entry was found.
|
- `python-cache-hit`: A boolean value to indicate the Python cache entry was found.
|
||||||
|
|
||||||
### Python version
|
### Python version
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import * as path from "node:path";
|
||||||
|
import type * as exec from "@actions/exec";
|
||||||
|
import {
|
||||||
|
afterEach,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
jest,
|
||||||
|
} from "@jest/globals";
|
||||||
|
import { createSetupInputs } from "../helpers/setup-inputs";
|
||||||
|
|
||||||
|
const mockGetExecOutput = jest.fn<typeof exec.getExecOutput>();
|
||||||
|
const mockDebug = jest.fn();
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
|
||||||
|
jest.unstable_mockModule("@actions/core", () => ({
|
||||||
|
debug: mockDebug,
|
||||||
|
isDebug: jest.fn(() => false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule("@actions/exec", () => ({
|
||||||
|
getExecOutput: mockGetExecOutput,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { getPythonRuntimeId } = await import("../../src/utils/python-runtime");
|
||||||
|
|
||||||
|
function mockRuntime(overrides: Record<string, unknown> = {}) {
|
||||||
|
mockGetExecOutput.mockResolvedValue({
|
||||||
|
exitCode: 0,
|
||||||
|
stderr: "",
|
||||||
|
stdout: `${JSON.stringify({
|
||||||
|
freethreaded: false,
|
||||||
|
implementation: "cpython",
|
||||||
|
implementationVersion: [3, 13, 1, "final", 0],
|
||||||
|
pythonVersion: "3.13.1",
|
||||||
|
...overrides,
|
||||||
|
})}\r\n`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGetExecOutput.mockReset();
|
||||||
|
mockRuntime();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPythonRuntimeId", () => {
|
||||||
|
it("does not query Python when environment activation is disabled", async () => {
|
||||||
|
const inputs = createSetupInputs({ pythonVersion: "3.15t" });
|
||||||
|
|
||||||
|
expect(await getPythonRuntimeId(inputs)).toBe("");
|
||||||
|
expect(mockGetExecOutput).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("identifies the resolved CPython version rather than the request", async () => {
|
||||||
|
const inputs = createSetupInputs({
|
||||||
|
activateEnvironment: true,
|
||||||
|
pythonVersion: "3.13",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await getPythonRuntimeId(inputs)).toBe("cpython-3.13.1");
|
||||||
|
expect(inputs.pythonVersion).toBe("3.13");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["3.15.0a1", false, "cpython-3.15.0a1"],
|
||||||
|
["3.15.0b2", false, "cpython-3.15.0b2"],
|
||||||
|
["3.15.0rc1", false, "cpython-3.15.0rc1"],
|
||||||
|
["3.15.0rc1", true, "cpython-3.15.0rc1-freethreaded"],
|
||||||
|
["3.15.0rc2", true, "cpython-3.15.0rc2-freethreaded"],
|
||||||
|
["3.15.0", true, "cpython-3.15.0-freethreaded"],
|
||||||
|
])(
|
||||||
|
"preserves version %s and free-threading=%s as %s",
|
||||||
|
async (pythonVersion, freethreaded, expected) => {
|
||||||
|
mockRuntime({ freethreaded, pythonVersion });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await getPythonRuntimeId(
|
||||||
|
createSetupInputs({ activateEnvironment: true }),
|
||||||
|
),
|
||||||
|
).toBe(expected);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("derives free-threading from the interpreter rather than the input", async () => {
|
||||||
|
const inputs = createSetupInputs({
|
||||||
|
activateEnvironment: true,
|
||||||
|
pythonVersion: "3.13.1t",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await getPythonRuntimeId(inputs)).toBe("cpython-3.13.1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[[7, 3, 23, "final", 0], "pypy-7.3.23-python-3.11.15"],
|
||||||
|
[[7, 3, 24, "final", 0], "pypy-7.3.24-python-3.11.15"],
|
||||||
|
[[7, 3, 24, "alpha", 1], "pypy-7.3.24a1-python-3.11.15"],
|
||||||
|
[[7, 3, 24, "beta", 2], "pypy-7.3.24b2-python-3.11.15"],
|
||||||
|
[[7, 3, 24, "candidate", 3], "pypy-7.3.24rc3-python-3.11.15"],
|
||||||
|
])("includes PyPy implementation version %j", async (version, expected) => {
|
||||||
|
mockRuntime({
|
||||||
|
implementation: "pypy",
|
||||||
|
implementationVersion: version,
|
||||||
|
pythonVersion: "3.11.15",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await getPythonRuntimeId(
|
||||||
|
createSetupInputs({ activateEnvironment: true }),
|
||||||
|
),
|
||||||
|
).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes the name and version of other Python implementations", async () => {
|
||||||
|
mockRuntime({
|
||||||
|
implementation: "graalpy",
|
||||||
|
implementationVersion: [25, 0, 0, "final", 0],
|
||||||
|
pythonVersion: "3.12.8",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await getPythonRuntimeId(
|
||||||
|
createSetupInputs({ activateEnvironment: true }),
|
||||||
|
),
|
||||||
|
).toBe("graalpy-25.0.0-python-3.12.8");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["linux", "darwin", "win32"])(
|
||||||
|
"queries the custom venv directly on %s, including paths with spaces",
|
||||||
|
async (platform) => {
|
||||||
|
Object.defineProperty(process, "platform", { value: platform });
|
||||||
|
const inputs = createSetupInputs({
|
||||||
|
activateEnvironment: true,
|
||||||
|
venvPath: "/runner temp/custom venv",
|
||||||
|
workingDirectory: "/different/project",
|
||||||
|
});
|
||||||
|
|
||||||
|
await getPythonRuntimeId(inputs);
|
||||||
|
|
||||||
|
const pythonPath =
|
||||||
|
platform === "win32"
|
||||||
|
? path.join(inputs.venvPath, "Scripts", "python.exe")
|
||||||
|
: path.join(inputs.venvPath, "bin", "python");
|
||||||
|
expect(mockGetExecOutput).toHaveBeenCalledWith(
|
||||||
|
`"${pythonPath}"`,
|
||||||
|
["-I", "-c", expect.any(String)],
|
||||||
|
{ silent: true },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("escapes quotes in the venv path for @actions/exec", async () => {
|
||||||
|
Object.defineProperty(process, "platform", { value: "linux" });
|
||||||
|
const inputs = createSetupInputs({
|
||||||
|
activateEnvironment: true,
|
||||||
|
venvPath: '/workspace/a "quoted" venv',
|
||||||
|
});
|
||||||
|
|
||||||
|
await getPythonRuntimeId(inputs);
|
||||||
|
|
||||||
|
expect(mockGetExecOutput.mock.calls[0][0]).toContain('a \\"quoted\\" venv');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["", "not JSON", "null", "{}"])(
|
||||||
|
"returns an empty output for invalid interpreter output: %j",
|
||||||
|
async (stdout) => {
|
||||||
|
mockGetExecOutput.mockResolvedValue({ exitCode: 0, stderr: "", stdout });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await getPythonRuntimeId(
|
||||||
|
createSetupInputs({ activateEnvironment: true }),
|
||||||
|
),
|
||||||
|
).toBe("");
|
||||||
|
expect(mockDebug).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("returns an empty output for an invalid implementation version", async () => {
|
||||||
|
mockRuntime({
|
||||||
|
implementation: "pypy",
|
||||||
|
implementationVersion: [7, 3, 24, "unknown", 0],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await getPythonRuntimeId(
|
||||||
|
createSetupInputs({ activateEnvironment: true }),
|
||||||
|
),
|
||||||
|
).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([new Error("interpreter missing"), "interpreter failed"])(
|
||||||
|
"returns an empty output if the interpreter cannot be queried: %s",
|
||||||
|
async (error) => {
|
||||||
|
mockGetExecOutput.mockRejectedValue(error);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await getPythonRuntimeId(
|
||||||
|
createSetupInputs({ activateEnvironment: true }),
|
||||||
|
),
|
||||||
|
).toBe("");
|
||||||
|
expect(mockDebug).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(error instanceof Error ? error.message : error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import * as path from "node:path";
|
|
||||||
import type * as exec from "@actions/exec";
|
|
||||||
import {
|
|
||||||
afterEach,
|
|
||||||
beforeEach,
|
|
||||||
describe,
|
|
||||||
expect,
|
|
||||||
it,
|
|
||||||
jest,
|
|
||||||
} from "@jest/globals";
|
|
||||||
import { createSetupInputs } from "../helpers/setup-inputs";
|
|
||||||
|
|
||||||
const mockGetExecOutput = jest.fn<typeof exec.getExecOutput>();
|
|
||||||
const mockDebug = jest.fn();
|
|
||||||
const originalPlatform = process.platform;
|
|
||||||
|
|
||||||
jest.unstable_mockModule("@actions/core", () => ({
|
|
||||||
debug: mockDebug,
|
|
||||||
isDebug: jest.fn(() => false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule("@actions/exec", () => ({
|
|
||||||
getExecOutput: mockGetExecOutput,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { getResolvedPythonVersion } = await import(
|
|
||||||
"../../src/utils/python-version"
|
|
||||||
);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockGetExecOutput.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getResolvedPythonVersion", () => {
|
|
||||||
it("does not query Python when environment activation is disabled", async () => {
|
|
||||||
const inputs = createSetupInputs({ pythonVersion: "3.15t" });
|
|
||||||
|
|
||||||
expect(await getResolvedPythonVersion(inputs)).toBe("");
|
|
||||||
expect(mockGetExecOutput).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
["3.13", "3.13.1"],
|
|
||||||
["3.13.1t", "3.13.1"],
|
|
||||||
["3.15t", "3.15.0rc1"],
|
|
||||||
["pypy3.11", "3.11.11"],
|
|
||||||
["", "3.12.9"],
|
|
||||||
])(
|
|
||||||
"resolves %j to the interpreter's version %s",
|
|
||||||
async (request, version) => {
|
|
||||||
mockGetExecOutput.mockResolvedValue({
|
|
||||||
exitCode: 0,
|
|
||||||
stderr: "",
|
|
||||||
stdout: `${version}\r\n`,
|
|
||||||
});
|
|
||||||
const inputs = createSetupInputs({
|
|
||||||
activateEnvironment: true,
|
|
||||||
pythonVersion: request,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await getResolvedPythonVersion(inputs)).toBe(version);
|
|
||||||
expect(inputs.pythonVersion).toBe(request);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it.each(["linux", "darwin", "win32"])(
|
|
||||||
"queries the custom venv directly on %s, including paths with spaces",
|
|
||||||
async (platform) => {
|
|
||||||
Object.defineProperty(process, "platform", { value: platform });
|
|
||||||
mockGetExecOutput.mockResolvedValue({
|
|
||||||
exitCode: 0,
|
|
||||||
stderr: "",
|
|
||||||
stdout: "3.13.1\n",
|
|
||||||
});
|
|
||||||
const inputs = createSetupInputs({
|
|
||||||
activateEnvironment: true,
|
|
||||||
venvPath: "/runner temp/custom venv",
|
|
||||||
workingDirectory: "/different/project",
|
|
||||||
});
|
|
||||||
|
|
||||||
await getResolvedPythonVersion(inputs);
|
|
||||||
|
|
||||||
const pythonPath =
|
|
||||||
platform === "win32"
|
|
||||||
? path.join(inputs.venvPath, "Scripts", "python.exe")
|
|
||||||
: path.join(inputs.venvPath, "bin", "python");
|
|
||||||
expect(mockGetExecOutput).toHaveBeenCalledWith(
|
|
||||||
`"${pythonPath}"`,
|
|
||||||
["-I", "-c", "import platform; print(platform.python_version())"],
|
|
||||||
{ silent: true },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it("escapes quotes in the venv path for @actions/exec", async () => {
|
|
||||||
Object.defineProperty(process, "platform", { value: "linux" });
|
|
||||||
mockGetExecOutput.mockResolvedValue({
|
|
||||||
exitCode: 0,
|
|
||||||
stderr: "",
|
|
||||||
stdout: "3.13.1\n",
|
|
||||||
});
|
|
||||||
const inputs = createSetupInputs({
|
|
||||||
activateEnvironment: true,
|
|
||||||
venvPath: '/workspace/a "quoted" venv',
|
|
||||||
});
|
|
||||||
|
|
||||||
await getResolvedPythonVersion(inputs);
|
|
||||||
|
|
||||||
expect(mockGetExecOutput.mock.calls[0][0]).toContain('a \\"quoted\\" venv');
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([new Error("interpreter missing"), "interpreter failed"])(
|
|
||||||
"returns an empty output if the interpreter cannot be queried: %s",
|
|
||||||
async (error) => {
|
|
||||||
mockGetExecOutput.mockRejectedValue(error);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await getResolvedPythonVersion(
|
|
||||||
createSetupInputs({ activateEnvironment: true }),
|
|
||||||
),
|
|
||||||
).toBe("");
|
|
||||||
expect(mockDebug).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining(error instanceof Error ? error.message : error),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
+1
-1
@@ -79,7 +79,7 @@ outputs:
|
|||||||
type: string
|
type: string
|
||||||
python-version:
|
python-version:
|
||||||
type: string
|
type: string
|
||||||
python-version-resolved:
|
python-runtime-id:
|
||||||
type: string
|
type: string
|
||||||
python-cache-hit:
|
python-cache-hit:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|||||||
+2
-2
@@ -107,8 +107,8 @@ outputs:
|
|||||||
description: "Path to the activated venv if activate-environment is true"
|
description: "Path to the activated venv if activate-environment is true"
|
||||||
python-version:
|
python-version:
|
||||||
description: "The Python version that was set."
|
description: "The Python version that was set."
|
||||||
python-version-resolved:
|
python-runtime-id:
|
||||||
description: "The full Python version of the activated venv, including prerelease suffixes. Empty when activate-environment is false or the version cannot be determined."
|
description: "An opaque identifier for the activated venv's Python runtime, including implementation, full version, and free-threaded build information. Empty when activate-environment is false or the runtime cannot be determined."
|
||||||
python-cache-hit:
|
python-cache-hit:
|
||||||
description: "A boolean value to indicate the Python cache entry was found"
|
description: "A boolean value to indicate the Python cache entry was found"
|
||||||
runs:
|
runs:
|
||||||
|
|||||||
+38
-9
@@ -102101,9 +102101,41 @@ function getResolutionStrategy() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/utils/python-version.ts
|
// src/utils/python-runtime.ts
|
||||||
var import_node_path2 = require("node:path");
|
var import_node_path2 = require("node:path");
|
||||||
async function getResolvedPythonVersion(inputs) {
|
var PYTHON_RUNTIME_QUERY = `
|
||||||
|
import json
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
import sysconfig
|
||||||
|
|
||||||
|
print(json.dumps({
|
||||||
|
"implementation": sys.implementation.name,
|
||||||
|
"implementationVersion": list(sys.implementation.version),
|
||||||
|
"pythonVersion": platform.python_version(),
|
||||||
|
"freethreaded": sysconfig.get_config_var("Py_GIL_DISABLED") == 1,
|
||||||
|
}))
|
||||||
|
`;
|
||||||
|
function formatRuntimeId(runtime) {
|
||||||
|
if (typeof runtime.implementation !== "string" || runtime.implementation === "" || typeof runtime.pythonVersion !== "string" || runtime.pythonVersion === "" || typeof runtime.freethreaded !== "boolean") {
|
||||||
|
throw new Error("Invalid Python runtime metadata");
|
||||||
|
}
|
||||||
|
let id = `cpython-${runtime.pythonVersion}`;
|
||||||
|
if (runtime.implementation !== "cpython") {
|
||||||
|
const [major2, minor2, micro, releaseLevel, serial] = runtime.implementationVersion;
|
||||||
|
const suffixes = { alpha: "a", beta: "b", candidate: "rc", final: "" };
|
||||||
|
const suffix = suffixes[releaseLevel];
|
||||||
|
if (suffix === void 0 || ![major2, minor2, micro, serial].every(
|
||||||
|
(part) => Number.isInteger(part) && part >= 0
|
||||||
|
)) {
|
||||||
|
throw new Error("Invalid Python implementation version");
|
||||||
|
}
|
||||||
|
const implementationVersion = `${major2}.${minor2}.${micro}${suffix}${suffix ? serial : ""}`;
|
||||||
|
id = `${runtime.implementation}-${implementationVersion}-python-${runtime.pythonVersion}`;
|
||||||
|
}
|
||||||
|
return runtime.freethreaded ? `${id}-freethreaded` : id;
|
||||||
|
}
|
||||||
|
async function getPythonRuntimeId(inputs) {
|
||||||
if (!inputs.activateEnvironment) {
|
if (!inputs.activateEnvironment) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -102111,13 +102143,13 @@ async function getResolvedPythonVersion(inputs) {
|
|||||||
try {
|
try {
|
||||||
const { stdout } = await getExecOutput(
|
const { stdout } = await getExecOutput(
|
||||||
`"${pythonPath.replace(/"/g, '\\"')}"`,
|
`"${pythonPath.replace(/"/g, '\\"')}"`,
|
||||||
["-I", "-c", "import platform; print(platform.python_version())"],
|
["-I", "-c", PYTHON_RUNTIME_QUERY],
|
||||||
{ silent: !isDebug() }
|
{ silent: !isDebug() }
|
||||||
);
|
);
|
||||||
return stdout.trim();
|
return formatRuntimeId(JSON.parse(stdout));
|
||||||
} catch (error2) {
|
} catch (error2) {
|
||||||
debug(
|
debug(
|
||||||
`Failed to get the activated environment's Python version. Error: ${error2 instanceof Error ? error2.message : String(error2)}`
|
`Failed to identify the activated environment's Python runtime. Error: ${error2 instanceof Error ? error2.message : String(error2)}`
|
||||||
);
|
);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -102193,10 +102225,7 @@ async function run() {
|
|||||||
info2(`Successfully installed uv version ${setupResult.version}`);
|
info2(`Successfully installed uv version ${setupResult.version}`);
|
||||||
const detectedPythonVersion = await getPythonVersion2(inputs);
|
const detectedPythonVersion = await getPythonVersion2(inputs);
|
||||||
setOutput("python-version", detectedPythonVersion);
|
setOutput("python-version", detectedPythonVersion);
|
||||||
setOutput(
|
setOutput("python-runtime-id", await getPythonRuntimeId(inputs));
|
||||||
"python-version-resolved",
|
|
||||||
await getResolvedPythonVersion(inputs)
|
|
||||||
);
|
|
||||||
if (inputs.enableCache) {
|
if (inputs.enableCache) {
|
||||||
await restoreCache2(inputs, detectedPythonVersion);
|
await restoreCache2(inputs, detectedPythonVersion);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,18 +17,35 @@ This allows directly using it in later steps:
|
|||||||
|
|
||||||
By default, the venv is created at `.venv` inside the `working-directory`.
|
By default, the venv is created at `.venv` inside the `working-directory`.
|
||||||
|
|
||||||
With `activate-environment: true`, the `python-version-resolved` output contains the
|
With `activate-environment: true`, the `python-runtime-id` output identifies the
|
||||||
venv's full Python version, as reported by `platform.python_version()`. For example,
|
venv's Python runtime using its implementation, full Python version (including
|
||||||
a `python-version` input of `3.15t` can resolve to `3.15.0rc1`. Use
|
prerelease numbers), and whether the interpreter was built for free threading.
|
||||||
`${{ steps.setup-uv.outputs.python-version-resolved }}` in later steps to access
|
For implementations other than CPython, it also includes the implementation's own
|
||||||
the resolved version (with `id: setup-uv` on the setup step).
|
version, so upgrading PyPy changes the identifier even if its Python version stays
|
||||||
|
the same.
|
||||||
|
|
||||||
This is the Python language version: it includes prerelease suffixes, but no
|
Example identifiers:
|
||||||
implementation name or free-threaded marker. For cache keys that distinguish PyPy
|
|
||||||
from CPython or free-threaded builds, include the requested version as well.
|
| Runtime | `python-runtime-id` |
|
||||||
The output is empty when `activate-environment` is false or the version cannot be
|
| --- | --- |
|
||||||
determined. The existing `python-version` output and setup-uv's cache keys are
|
| CPython RC1 | `cpython-3.15.0rc1` |
|
||||||
unaffected.
|
| Free-threaded CPython RC1 | `cpython-3.15.0rc1-freethreaded` |
|
||||||
|
| Free-threaded CPython RC2 | `cpython-3.15.0rc2-freethreaded` |
|
||||||
|
| PyPy | `pypy-7.3.23-python-3.11.15` |
|
||||||
|
|
||||||
|
This is an opaque identifier in a setup-uv-defined format, not a Python version
|
||||||
|
specifier. Use the whole value as a cache-key component rather than parsing it.
|
||||||
|
Combine it with the platform and dependency information relevant to your cache,
|
||||||
|
for example (with `id: setup-uv` on the setup step):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
key: build-${{ runner.os }}-${{ runner.arch }}-${{ steps.setup-uv.outputs.python-runtime-id }}-${{ hashFiles('uv.lock') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
The free-threaded marker describes the interpreter's build even when the GIL is
|
||||||
|
enabled at runtime. The output is empty when `activate-environment` is false or the
|
||||||
|
runtime cannot be determined. The existing `python-version` output and setup-uv's
|
||||||
|
cache keys are unaffected.
|
||||||
|
|
||||||
You can customize the venv location with `venv-path`, for example to place it in the runner temp directory:
|
You can customize the venv location with `venv-path`, for example to place it in the runner temp directory:
|
||||||
|
|
||||||
|
|||||||
+2
-5
@@ -16,7 +16,7 @@ import {
|
|||||||
getPlatform,
|
getPlatform,
|
||||||
type Platform,
|
type Platform,
|
||||||
} from "./utils/platforms";
|
} from "./utils/platforms";
|
||||||
import { getResolvedPythonVersion } from "./utils/python-version";
|
import { getPythonRuntimeId } from "./utils/python-runtime";
|
||||||
import { resolveUvVersion } from "./version/resolve";
|
import { resolveUvVersion } from "./version/resolve";
|
||||||
|
|
||||||
const sourceDir = __dirname;
|
const sourceDir = __dirname;
|
||||||
@@ -102,10 +102,7 @@ async function run(): Promise<void> {
|
|||||||
|
|
||||||
const detectedPythonVersion = await getPythonVersion(inputs);
|
const detectedPythonVersion = await getPythonVersion(inputs);
|
||||||
core.setOutput("python-version", detectedPythonVersion);
|
core.setOutput("python-version", detectedPythonVersion);
|
||||||
core.setOutput(
|
core.setOutput("python-runtime-id", await getPythonRuntimeId(inputs));
|
||||||
"python-version-resolved",
|
|
||||||
await getResolvedPythonVersion(inputs),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (inputs.enableCache) {
|
if (inputs.enableCache) {
|
||||||
await restoreCache(inputs, detectedPythonVersion);
|
await restoreCache(inputs, detectedPythonVersion);
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
import * as exec from "@actions/exec";
|
||||||
|
import type { SetupInputs } from "./inputs";
|
||||||
|
|
||||||
|
const PYTHON_RUNTIME_QUERY = `
|
||||||
|
import json
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
import sysconfig
|
||||||
|
|
||||||
|
print(json.dumps({
|
||||||
|
"implementation": sys.implementation.name,
|
||||||
|
"implementationVersion": list(sys.implementation.version),
|
||||||
|
"pythonVersion": platform.python_version(),
|
||||||
|
"freethreaded": sysconfig.get_config_var("Py_GIL_DISABLED") == 1,
|
||||||
|
}))
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ReleaseLevel = "alpha" | "beta" | "candidate" | "final";
|
||||||
|
|
||||||
|
interface PythonRuntime {
|
||||||
|
implementation: string;
|
||||||
|
implementationVersion: [number, number, number, ReleaseLevel, number];
|
||||||
|
pythonVersion: string;
|
||||||
|
freethreaded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRuntimeId(runtime: PythonRuntime): string {
|
||||||
|
if (
|
||||||
|
typeof runtime.implementation !== "string" ||
|
||||||
|
runtime.implementation === "" ||
|
||||||
|
typeof runtime.pythonVersion !== "string" ||
|
||||||
|
runtime.pythonVersion === "" ||
|
||||||
|
typeof runtime.freethreaded !== "boolean"
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid Python runtime metadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = `cpython-${runtime.pythonVersion}`;
|
||||||
|
if (runtime.implementation !== "cpython") {
|
||||||
|
const [major, minor, micro, releaseLevel, serial] =
|
||||||
|
runtime.implementationVersion;
|
||||||
|
const suffixes = { alpha: "a", beta: "b", candidate: "rc", final: "" };
|
||||||
|
const suffix = suffixes[releaseLevel];
|
||||||
|
if (
|
||||||
|
suffix === undefined ||
|
||||||
|
![major, minor, micro, serial].every(
|
||||||
|
(part) => Number.isInteger(part) && part >= 0,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid Python implementation version");
|
||||||
|
}
|
||||||
|
const implementationVersion = `${major}.${minor}.${micro}${suffix}${suffix ? serial : ""}`;
|
||||||
|
id = `${runtime.implementation}-${implementationVersion}-python-${runtime.pythonVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return runtime.freethreaded ? `${id}-freethreaded` : id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPythonRuntimeId(inputs: SetupInputs): Promise<string> {
|
||||||
|
if (!inputs.activateEnvironment) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const pythonPath =
|
||||||
|
process.platform === "win32"
|
||||||
|
? join(inputs.venvPath, "Scripts", "python.exe")
|
||||||
|
: join(inputs.venvPath, "bin", "python");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// @actions/exec parses the executable as a command line, so quote the path.
|
||||||
|
const { stdout } = await exec.getExecOutput(
|
||||||
|
`"${pythonPath.replace(/"/g, '\\"')}"`,
|
||||||
|
["-I", "-c", PYTHON_RUNTIME_QUERY],
|
||||||
|
{ silent: !core.isDebug() },
|
||||||
|
);
|
||||||
|
return formatRuntimeId(JSON.parse(stdout));
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(
|
||||||
|
`Failed to identify the activated environment's Python runtime. Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { join } from "node:path";
|
|
||||||
import * as core from "@actions/core";
|
|
||||||
import * as exec from "@actions/exec";
|
|
||||||
import type { SetupInputs } from "./inputs";
|
|
||||||
|
|
||||||
export async function getResolvedPythonVersion(
|
|
||||||
inputs: SetupInputs,
|
|
||||||
): Promise<string> {
|
|
||||||
if (!inputs.activateEnvironment) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const pythonPath =
|
|
||||||
process.platform === "win32"
|
|
||||||
? join(inputs.venvPath, "Scripts", "python.exe")
|
|
||||||
: join(inputs.venvPath, "bin", "python");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// @actions/exec parses the executable as a command line, so quote the path.
|
|
||||||
const { stdout } = await exec.getExecOutput(
|
|
||||||
`"${pythonPath.replace(/"/g, '\\"')}"`,
|
|
||||||
["-I", "-c", "import platform; print(platform.python_version())"],
|
|
||||||
{ silent: !core.isDebug() },
|
|
||||||
);
|
|
||||||
return stdout.trim();
|
|
||||||
} catch (error) {
|
|
||||||
core.debug(
|
|
||||||
`Failed to get the activated environment's Python version. Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user