mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-04-17 02:06:29 +00:00
Refactor version resolving (#852)
This commit is contained in:
committed by
GitHub
parent
cb84d12dc6
commit
cdfb2ee6dd
@@ -95,6 +95,35 @@ describe("download-version", () => {
|
|||||||
expect(mockGetAllVersions).toHaveBeenCalledWith(undefined);
|
expect(mockGetAllVersions).toHaveBeenCalledWith(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats == exact pins as explicit versions", async () => {
|
||||||
|
const version = await resolveVersion("==0.9.26", undefined);
|
||||||
|
|
||||||
|
expect(version).toBe("0.9.26");
|
||||||
|
expect(mockGetAllVersions).not.toHaveBeenCalled();
|
||||||
|
expect(mockGetLatestVersion).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses latest for minimum-only ranges when using the highest strategy", async () => {
|
||||||
|
mockGetLatestVersion.mockResolvedValue("0.9.26");
|
||||||
|
|
||||||
|
const version = await resolveVersion(">=0.9.0", undefined, "highest");
|
||||||
|
|
||||||
|
expect(version).toBe("0.9.26");
|
||||||
|
expect(mockGetLatestVersion).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockGetLatestVersion).toHaveBeenCalledWith(undefined);
|
||||||
|
expect(mockGetAllVersions).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the lowest compatible version when requested", async () => {
|
||||||
|
mockGetAllVersions.mockResolvedValue(["0.9.26", "0.9.25"]);
|
||||||
|
|
||||||
|
const version = await resolveVersion("^0.9.0", undefined, "lowest");
|
||||||
|
|
||||||
|
expect(version).toBe("0.9.25");
|
||||||
|
expect(mockGetAllVersions).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockGetAllVersions).toHaveBeenCalledWith(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
it("uses manifest-file when provided", async () => {
|
it("uses manifest-file when provided", async () => {
|
||||||
mockGetAllVersions.mockResolvedValue(["0.9.26", "0.9.25"]);
|
mockGetAllVersions.mockResolvedValue(["0.9.26", "0.9.25"]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect, test } from "@jest/globals";
|
import { expect, test } from "@jest/globals";
|
||||||
import { getUvVersionFromFile } from "../../src/version/resolve";
|
import { getUvVersionFromFile } from "../../src/version/file-parser";
|
||||||
|
|
||||||
test("ignores dependencies starting with uv", async () => {
|
test("ignores dependencies starting with uv", async () => {
|
||||||
const parsedVersion = getUvVersionFromFile(
|
const parsedVersion = getUvVersionFromFile(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect, test } from "@jest/globals";
|
import { expect, test } from "@jest/globals";
|
||||||
import { getUvVersionFromFile } from "../../src/version/resolve";
|
import { getUvVersionFromFile } from "../../src/version/file-parser";
|
||||||
|
|
||||||
test("ignores dependencies starting with uv", async () => {
|
test("ignores dependencies starting with uv", async () => {
|
||||||
const parsedVersion = getUvVersionFromFile(
|
const parsedVersion = getUvVersionFromFile(
|
||||||
|
|||||||
125
__tests__/version/version-request-resolver.test.ts
Normal file
125
__tests__/version/version-request-resolver.test.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "@jest/globals";
|
||||||
|
import { resolveVersionRequest } from "../../src/version/version-request-resolver";
|
||||||
|
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
function createTempProject(files: Record<string, string> = {}): string {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "setup-uv-version-test-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
|
||||||
|
for (const [relativePath, content] of Object.entries(files)) {
|
||||||
|
const filePath = path.join(dir, relativePath);
|
||||||
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
fs.rmSync(dir, { force: true, recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveVersionRequest", () => {
|
||||||
|
it("prefers explicit input over version-file and workspace config", () => {
|
||||||
|
const workingDirectory = createTempProject({
|
||||||
|
".tool-versions": "uv 0.4.0\n",
|
||||||
|
"pyproject.toml": `[tool.uv]\nrequired-version = "==0.5.14"\n`,
|
||||||
|
"uv.toml": `required-version = "==0.5.15"\n`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = resolveVersionRequest({
|
||||||
|
version: "==0.6.0",
|
||||||
|
versionFile: path.join(workingDirectory, ".tool-versions"),
|
||||||
|
workingDirectory,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request).toEqual({
|
||||||
|
source: "input",
|
||||||
|
specifier: "0.6.0",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses .tool-versions when it is passed via version-file", () => {
|
||||||
|
const workingDirectory = createTempProject({
|
||||||
|
".tool-versions": "uv 0.5.15\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = resolveVersionRequest({
|
||||||
|
versionFile: path.join(workingDirectory, ".tool-versions"),
|
||||||
|
workingDirectory,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request).toEqual({
|
||||||
|
format: ".tool-versions",
|
||||||
|
source: "version-file",
|
||||||
|
sourcePath: path.join(workingDirectory, ".tool-versions"),
|
||||||
|
specifier: "0.5.15",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses requirements.txt when it is passed via version-file", () => {
|
||||||
|
const workingDirectory = createTempProject({
|
||||||
|
"requirements.txt": "uv==0.6.17\nuvicorn==0.35.0\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = resolveVersionRequest({
|
||||||
|
versionFile: path.join(workingDirectory, "requirements.txt"),
|
||||||
|
workingDirectory,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request).toEqual({
|
||||||
|
format: "requirements",
|
||||||
|
source: "version-file",
|
||||||
|
sourcePath: path.join(workingDirectory, "requirements.txt"),
|
||||||
|
specifier: "0.6.17",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers uv.toml over pyproject.toml during workspace discovery", () => {
|
||||||
|
const workingDirectory = createTempProject({
|
||||||
|
"pyproject.toml": `[tool.uv]\nrequired-version = "==0.5.14"\n`,
|
||||||
|
"uv.toml": `required-version = "==0.5.15"\n`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = resolveVersionRequest({ workingDirectory });
|
||||||
|
|
||||||
|
expect(request).toEqual({
|
||||||
|
format: "uv.toml",
|
||||||
|
source: "uv.toml",
|
||||||
|
sourcePath: path.join(workingDirectory, "uv.toml"),
|
||||||
|
specifier: "0.5.15",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to latest when no version source is found", () => {
|
||||||
|
const workingDirectory = createTempProject({});
|
||||||
|
|
||||||
|
const request = resolveVersionRequest({ workingDirectory });
|
||||||
|
|
||||||
|
expect(request).toEqual({
|
||||||
|
source: "default",
|
||||||
|
specifier: "latest",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when version-file does not resolve a version", () => {
|
||||||
|
const workingDirectory = createTempProject({
|
||||||
|
"requirements.txt": "uvicorn==0.35.0\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
resolveVersionRequest({
|
||||||
|
versionFile: path.join(workingDirectory, "requirements.txt"),
|
||||||
|
workingDirectory,
|
||||||
|
}),
|
||||||
|
).toThrow(
|
||||||
|
`Could not determine uv version from file: ${path.join(workingDirectory, "requirements.txt")}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
6
dist/save-cache/index.cjs
generated
vendored
6
dist/save-cache/index.cjs
generated
vendored
@@ -62947,6 +62947,12 @@ function getConfigValueFromTomlFile(filePath, key) {
|
|||||||
return void 0;
|
return void 0;
|
||||||
}
|
}
|
||||||
const fileContent = import_node_fs2.default.readFileSync(filePath, "utf-8");
|
const fileContent = import_node_fs2.default.readFileSync(filePath, "utf-8");
|
||||||
|
return getConfigValueFromTomlContent(filePath, fileContent, key);
|
||||||
|
}
|
||||||
|
function getConfigValueFromTomlContent(filePath, fileContent, key) {
|
||||||
|
if (!filePath.endsWith(".toml")) {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
if (filePath.endsWith("pyproject.toml")) {
|
if (filePath.endsWith("pyproject.toml")) {
|
||||||
const tomlContent2 = parse2(fileContent);
|
const tomlContent2 = parse2(fileContent);
|
||||||
return tomlContent2?.tool?.uv?.[key];
|
return tomlContent2?.tool?.uv?.[key];
|
||||||
|
|||||||
4694
dist/setup/index.cjs
generated
vendored
4694
dist/setup/index.cjs
generated
vendored
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,6 @@ import { promises as fs } from "node:fs";
|
|||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import * as tc from "@actions/tool-cache";
|
import * as tc from "@actions/tool-cache";
|
||||||
import * as pep440 from "@renovatebot/pep440";
|
|
||||||
import * as semver from "semver";
|
|
||||||
import {
|
import {
|
||||||
ASTRAL_MIRROR_PREFIX,
|
ASTRAL_MIRROR_PREFIX,
|
||||||
GITHUB_RELEASES_PREFIX,
|
GITHUB_RELEASES_PREFIX,
|
||||||
@@ -12,7 +10,9 @@ import {
|
|||||||
} from "../utils/constants";
|
} from "../utils/constants";
|
||||||
import type { Architecture, Platform } from "../utils/platforms";
|
import type { Architecture, Platform } from "../utils/platforms";
|
||||||
import { validateChecksum } from "./checksum/checksum";
|
import { validateChecksum } from "./checksum/checksum";
|
||||||
import { getAllVersions, getArtifact, getLatestVersion } from "./manifest";
|
import { getArtifact } from "./manifest";
|
||||||
|
|
||||||
|
export { resolveVersion } from "../version/resolve";
|
||||||
|
|
||||||
export function tryGetFromToolCache(
|
export function tryGetFromToolCache(
|
||||||
arch: Architecture,
|
arch: Architecture,
|
||||||
@@ -172,102 +172,3 @@ function resolveChecksum(
|
|||||||
function getExtension(platform: Platform): string {
|
function getExtension(platform: Platform): string {
|
||||||
return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz";
|
return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveVersion(
|
|
||||||
versionInput: string,
|
|
||||||
manifestUrl: string | undefined,
|
|
||||||
resolutionStrategy: "highest" | "lowest" = "highest",
|
|
||||||
): Promise<string> {
|
|
||||||
core.debug(`Resolving version: ${versionInput}`);
|
|
||||||
const isSimpleMinimumVersionSpecifier =
|
|
||||||
versionInput.includes(">") && !versionInput.includes(",");
|
|
||||||
const resolveVersionSpecifierToLatest =
|
|
||||||
isSimpleMinimumVersionSpecifier && resolutionStrategy === "highest";
|
|
||||||
|
|
||||||
if (resolveVersionSpecifierToLatest) {
|
|
||||||
core.info("Found minimum version specifier, using latest version");
|
|
||||||
}
|
|
||||||
|
|
||||||
const version =
|
|
||||||
versionInput === "latest" || resolveVersionSpecifierToLatest
|
|
||||||
? await getLatestVersion(manifestUrl)
|
|
||||||
: versionInput;
|
|
||||||
|
|
||||||
if (tc.isExplicitVersion(version)) {
|
|
||||||
core.debug(`Version ${version} is an explicit version.`);
|
|
||||||
if (
|
|
||||||
resolveVersionSpecifierToLatest &&
|
|
||||||
!pep440.satisfies(version, versionInput)
|
|
||||||
) {
|
|
||||||
throw new Error(`No version found for ${versionInput}`);
|
|
||||||
}
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
const availableVersions = await getAvailableVersions(manifestUrl);
|
|
||||||
core.debug(`Available versions: ${availableVersions}`);
|
|
||||||
const resolvedVersion =
|
|
||||||
resolutionStrategy === "lowest"
|
|
||||||
? minSatisfying(availableVersions, version)
|
|
||||||
: maxSatisfying(availableVersions, version);
|
|
||||||
|
|
||||||
if (resolvedVersion === undefined) {
|
|
||||||
throw new Error(`No version found for ${version}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAvailableVersions(
|
|
||||||
manifestUrl: string | undefined,
|
|
||||||
): Promise<string[]> {
|
|
||||||
if (manifestUrl !== undefined) {
|
|
||||||
core.info(
|
|
||||||
`Getting available versions from manifest-file ${manifestUrl} ...`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
core.info(`Getting available versions from ${VERSIONS_MANIFEST_URL} ...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await getAllVersions(manifestUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function maxSatisfying(
|
|
||||||
versions: string[],
|
|
||||||
version: string,
|
|
||||||
): string | undefined {
|
|
||||||
const maxSemver = tc.evaluateVersions(versions, version);
|
|
||||||
if (maxSemver !== "") {
|
|
||||||
core.debug(`Found a version that satisfies the semver range: ${maxSemver}`);
|
|
||||||
return maxSemver;
|
|
||||||
}
|
|
||||||
const maxPep440 = pep440.maxSatisfying(versions, version);
|
|
||||||
if (maxPep440 !== null) {
|
|
||||||
core.debug(
|
|
||||||
`Found a version that satisfies the pep440 specifier: ${maxPep440}`,
|
|
||||||
);
|
|
||||||
return maxPep440;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function minSatisfying(
|
|
||||||
versions: string[],
|
|
||||||
version: string,
|
|
||||||
): string | undefined {
|
|
||||||
// For semver, we need to use a different approach since tc.evaluateVersions only returns max
|
|
||||||
// Let's use semver directly for min satisfying
|
|
||||||
const minSemver = semver.minSatisfying(versions, version);
|
|
||||||
if (minSemver !== null) {
|
|
||||||
core.debug(`Found a version that satisfies the semver range: ${minSemver}`);
|
|
||||||
return minSemver;
|
|
||||||
}
|
|
||||||
const minPep440 = pep440.minSatisfying(versions, version);
|
|
||||||
if (minPep440 !== null) {
|
|
||||||
core.debug(
|
|
||||||
`Found a version that satisfies the pep440 specifier: ${minPep440}`,
|
|
||||||
);
|
|
||||||
return minPep440;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ export async function getLatestVersion(
|
|||||||
export async function getAllVersions(
|
export async function getAllVersions(
|
||||||
manifestUrl: string = VERSIONS_MANIFEST_URL,
|
manifestUrl: string = VERSIONS_MANIFEST_URL,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
|
core.info(
|
||||||
|
`Getting available versions from ${manifestSource(manifestUrl)} ...`,
|
||||||
|
);
|
||||||
const versions = await fetchManifest(manifestUrl);
|
const versions = await fetchManifest(manifestUrl);
|
||||||
return versions.map((versionData) => versionData.version);
|
return versions.map((versionData) => versionData.version);
|
||||||
}
|
}
|
||||||
@@ -165,6 +168,14 @@ export function clearManifestCache(manifestUrl?: string): void {
|
|||||||
cachedManifestData.delete(manifestUrl);
|
cachedManifestData.delete(manifestUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function manifestSource(manifestUrl: string): string {
|
||||||
|
if (manifestUrl === VERSIONS_MANIFEST_URL) {
|
||||||
|
return VERSIONS_MANIFEST_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `manifest-file ${manifestUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
function isManifestVersion(value: unknown): value is ManifestVersion {
|
function isManifestVersion(value: unknown): value is ManifestVersion {
|
||||||
if (!isRecord(value)) {
|
if (!isRecord(value)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import * as exec from "@actions/exec";
|
|||||||
import { restoreCache } from "./cache/restore-cache";
|
import { restoreCache } from "./cache/restore-cache";
|
||||||
import {
|
import {
|
||||||
downloadVersion,
|
downloadVersion,
|
||||||
resolveVersion,
|
|
||||||
tryGetFromToolCache,
|
tryGetFromToolCache,
|
||||||
} from "./download/download-version";
|
} from "./download/download-version";
|
||||||
import { STATE_UV_PATH, STATE_UV_VERSION } from "./utils/constants";
|
import { STATE_UV_PATH, STATE_UV_VERSION } from "./utils/constants";
|
||||||
@@ -16,7 +15,7 @@ import {
|
|||||||
getPlatform,
|
getPlatform,
|
||||||
type Platform,
|
type Platform,
|
||||||
} from "./utils/platforms";
|
} from "./utils/platforms";
|
||||||
import { getUvVersionFromFile } from "./version/resolve";
|
import { resolveUvVersion } from "./version/resolve";
|
||||||
|
|
||||||
const sourceDir = __dirname;
|
const sourceDir = __dirname;
|
||||||
|
|
||||||
@@ -112,7 +111,13 @@ async function setupUv(
|
|||||||
platform: Platform,
|
platform: Platform,
|
||||||
arch: Architecture,
|
arch: Architecture,
|
||||||
): Promise<{ uvDir: string; version: string }> {
|
): Promise<{ uvDir: string; version: string }> {
|
||||||
const resolvedVersion = await determineVersion(inputs);
|
const resolvedVersion = await resolveUvVersion({
|
||||||
|
manifestFile: inputs.manifestFile,
|
||||||
|
resolutionStrategy: inputs.resolutionStrategy,
|
||||||
|
version: inputs.version,
|
||||||
|
versionFile: inputs.versionFile,
|
||||||
|
workingDirectory: inputs.workingDirectory,
|
||||||
|
});
|
||||||
const toolCacheResult = tryGetFromToolCache(arch, resolvedVersion);
|
const toolCacheResult = tryGetFromToolCache(arch, resolvedVersion);
|
||||||
if (toolCacheResult.installedPath) {
|
if (toolCacheResult.installedPath) {
|
||||||
core.info(`Found uv in tool-cache for ${toolCacheResult.version}`);
|
core.info(`Found uv in tool-cache for ${toolCacheResult.version}`);
|
||||||
@@ -137,45 +142,6 @@ async function setupUv(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function determineVersion(inputs: SetupInputs): Promise<string> {
|
|
||||||
return await resolveVersion(
|
|
||||||
getRequestedVersion(inputs),
|
|
||||||
inputs.manifestFile,
|
|
||||||
inputs.resolutionStrategy,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRequestedVersion(inputs: SetupInputs): string {
|
|
||||||
if (inputs.version !== "") {
|
|
||||||
return inputs.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputs.versionFile !== "") {
|
|
||||||
const versionFromFile = getUvVersionFromFile(inputs.versionFile);
|
|
||||||
if (versionFromFile === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
`Could not determine uv version from file: ${inputs.versionFile}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return versionFromFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionFromUvToml = getUvVersionFromFile(
|
|
||||||
`${inputs.workingDirectory}${path.sep}uv.toml`,
|
|
||||||
);
|
|
||||||
const versionFromPyproject = getUvVersionFromFile(
|
|
||||||
`${inputs.workingDirectory}${path.sep}pyproject.toml`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (versionFromUvToml === undefined && versionFromPyproject === undefined) {
|
|
||||||
core.info(
|
|
||||||
"Could not determine uv version from uv.toml or pyproject.toml. Falling back to latest.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return versionFromUvToml || versionFromPyproject || "latest";
|
|
||||||
}
|
|
||||||
|
|
||||||
function addUvToPathAndOutput(cachedPath: string): void {
|
function addUvToPathAndOutput(cachedPath: string): void {
|
||||||
core.setOutput("uv-path", `${cachedPath}${path.sep}uv`);
|
core.setOutput("uv-path", `${cachedPath}${path.sep}uv`);
|
||||||
core.saveState(STATE_UV_PATH, `${cachedPath}${path.sep}uv`);
|
core.saveState(STATE_UV_PATH, `${cachedPath}${path.sep}uv`);
|
||||||
|
|||||||
@@ -8,7 +8,19 @@ export function getConfigValueFromTomlFile(
|
|||||||
if (!fs.existsSync(filePath) || !filePath.endsWith(".toml")) {
|
if (!fs.existsSync(filePath) || !filePath.endsWith(".toml")) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
return getConfigValueFromTomlContent(filePath, fileContent, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfigValueFromTomlContent(
|
||||||
|
filePath: string,
|
||||||
|
fileContent: string,
|
||||||
|
key: string,
|
||||||
|
): string | undefined {
|
||||||
|
if (!filePath.endsWith(".toml")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (filePath.endsWith("pyproject.toml")) {
|
if (filePath.endsWith("pyproject.toml")) {
|
||||||
const tomlContent = toml.parse(fileContent) as {
|
const tomlContent = toml.parse(fileContent) as {
|
||||||
@@ -16,6 +28,7 @@ export function getConfigValueFromTomlFile(
|
|||||||
};
|
};
|
||||||
return tomlContent?.tool?.uv?.[key];
|
return tomlContent?.tool?.uv?.[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
const tomlContent = toml.parse(fileContent) as Record<
|
const tomlContent = toml.parse(fileContent) as Record<
|
||||||
string,
|
string,
|
||||||
string | undefined
|
string | undefined
|
||||||
|
|||||||
103
src/version/file-parser.ts
Normal file
103
src/version/file-parser.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
import { getConfigValueFromTomlContent } from "../utils/config-file";
|
||||||
|
import {
|
||||||
|
getUvVersionFromParsedPyproject,
|
||||||
|
getUvVersionFromRequirementsText,
|
||||||
|
parsePyprojectContent,
|
||||||
|
} from "./requirements-file";
|
||||||
|
import { normalizeVersionSpecifier } from "./specifier";
|
||||||
|
import { getUvVersionFromToolVersions } from "./tool-versions-file";
|
||||||
|
import type { ParsedVersionFile, VersionFileFormat } from "./types";
|
||||||
|
|
||||||
|
interface VersionFileParser {
|
||||||
|
format: VersionFileFormat;
|
||||||
|
parse(filePath: string): string | undefined;
|
||||||
|
supports(filePath: string): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VERSION_FILE_PARSERS: VersionFileParser[] = [
|
||||||
|
{
|
||||||
|
format: ".tool-versions",
|
||||||
|
parse: (filePath) => getUvVersionFromToolVersions(filePath),
|
||||||
|
supports: (filePath) => filePath.endsWith(".tool-versions"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "uv.toml",
|
||||||
|
parse: (filePath) => {
|
||||||
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
return getConfigValueFromTomlContent(
|
||||||
|
filePath,
|
||||||
|
fileContent,
|
||||||
|
"required-version",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
supports: (filePath) => filePath.endsWith("uv.toml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "pyproject.toml",
|
||||||
|
parse: (filePath) => {
|
||||||
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const pyproject = parsePyprojectContent(fileContent);
|
||||||
|
const requiredVersion = pyproject.tool?.uv?.["required-version"];
|
||||||
|
|
||||||
|
if (requiredVersion !== undefined) {
|
||||||
|
return requiredVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getUvVersionFromParsedPyproject(pyproject);
|
||||||
|
},
|
||||||
|
supports: (filePath) => filePath.endsWith("pyproject.toml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "requirements",
|
||||||
|
parse: (filePath) => {
|
||||||
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
return getUvVersionFromRequirementsText(fileContent);
|
||||||
|
},
|
||||||
|
supports: (filePath) => filePath.endsWith(".txt"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getParsedVersionFile(
|
||||||
|
filePath: string,
|
||||||
|
): ParsedVersionFile | undefined {
|
||||||
|
core.info(`Trying to find version for uv in: ${filePath}`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
core.info(`Could not find file: ${filePath}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = getVersionFileParser(filePath);
|
||||||
|
if (parser === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const specifier = parser.parse(filePath);
|
||||||
|
if (specifier === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedSpecifier = normalizeVersionSpecifier(specifier);
|
||||||
|
core.info(`Found version for uv in ${filePath}: ${normalizedSpecifier}`);
|
||||||
|
return {
|
||||||
|
format: parser.format,
|
||||||
|
specifier: normalizedSpecifier,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(
|
||||||
|
`Error while parsing ${filePath}: ${(error as Error).message}`,
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUvVersionFromFile(filePath: string): string | undefined {
|
||||||
|
return getParsedVersionFile(filePath)?.specifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVersionFileParser(filePath: string): VersionFileParser | undefined {
|
||||||
|
return VERSION_FILE_PARSERS.find((parser) => parser.supports(filePath));
|
||||||
|
}
|
||||||
@@ -5,31 +5,23 @@ export function getUvVersionFromRequirementsFile(
|
|||||||
filePath: string,
|
filePath: string,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||||
|
|
||||||
if (filePath.endsWith(".txt")) {
|
if (filePath.endsWith(".txt")) {
|
||||||
|
return getUvVersionFromRequirementsText(fileContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getUvVersionFromPyprojectContent(fileContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUvVersionFromRequirementsText(
|
||||||
|
fileContent: string,
|
||||||
|
): string | undefined {
|
||||||
return getUvVersionFromAllDependencies(fileContent.split("\n"));
|
return getUvVersionFromAllDependencies(fileContent.split("\n"));
|
||||||
}
|
}
|
||||||
const dependencies = parsePyprojectDependencies(fileContent);
|
|
||||||
return getUvVersionFromAllDependencies(dependencies);
|
export function getUvVersionFromParsedPyproject(
|
||||||
}
|
pyproject: Pyproject,
|
||||||
function getUvVersionFromAllDependencies(
|
|
||||||
allDependencies: string[],
|
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
return allDependencies
|
|
||||||
.find((dep: string) => dep.match(/^uv[=<>~!]/))
|
|
||||||
?.match(/^uv([=<>~!]+\S*)/)?.[1]
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Pyproject {
|
|
||||||
project?: {
|
|
||||||
dependencies?: string[];
|
|
||||||
"optional-dependencies"?: Record<string, string[]>;
|
|
||||||
};
|
|
||||||
"dependency-groups"?: Record<string, Array<string | object>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePyprojectDependencies(pyprojectContent: string): string[] {
|
|
||||||
const pyproject: Pyproject = toml.parse(pyprojectContent);
|
|
||||||
const dependencies: string[] = pyproject?.project?.dependencies || [];
|
const dependencies: string[] = pyproject?.project?.dependencies || [];
|
||||||
const optionalDependencies: string[] = Object.values(
|
const optionalDependencies: string[] = Object.values(
|
||||||
pyproject?.project?.["optional-dependencies"] || {},
|
pyproject?.project?.["optional-dependencies"] || {},
|
||||||
@@ -39,5 +31,39 @@ function parsePyprojectDependencies(pyprojectContent: string): string[] {
|
|||||||
)
|
)
|
||||||
.flat()
|
.flat()
|
||||||
.filter((item: string | object) => typeof item === "string");
|
.filter((item: string | object) => typeof item === "string");
|
||||||
return dependencies.concat(optionalDependencies, devDependencies);
|
|
||||||
|
return getUvVersionFromAllDependencies(
|
||||||
|
dependencies.concat(optionalDependencies, devDependencies),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUvVersionFromPyprojectContent(
|
||||||
|
pyprojectContent: string,
|
||||||
|
): string | undefined {
|
||||||
|
const pyproject = parsePyprojectContent(pyprojectContent);
|
||||||
|
return getUvVersionFromParsedPyproject(pyproject);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Pyproject {
|
||||||
|
project?: {
|
||||||
|
dependencies?: string[];
|
||||||
|
"optional-dependencies"?: Record<string, string[]>;
|
||||||
|
};
|
||||||
|
"dependency-groups"?: Record<string, Array<string | object>>;
|
||||||
|
tool?: {
|
||||||
|
uv?: Record<string, string | undefined>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePyprojectContent(pyprojectContent: string): Pyproject {
|
||||||
|
return toml.parse(pyprojectContent) as Pyproject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUvVersionFromAllDependencies(
|
||||||
|
allDependencies: string[],
|
||||||
|
): string | undefined {
|
||||||
|
return allDependencies
|
||||||
|
.find((dep: string) => dep.match(/^uv[=<>~!]/))
|
||||||
|
?.match(/^uv([=<>~!]+\S*)/)?.[1]
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,183 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { getConfigValueFromTomlFile } from "../utils/config-file";
|
import * as tc from "@actions/tool-cache";
|
||||||
import { getUvVersionFromRequirementsFile } from "./requirements-file";
|
import * as pep440 from "@renovatebot/pep440";
|
||||||
import { getUvVersionFromToolVersions } from "./tool-versions-file";
|
import * as semver from "semver";
|
||||||
|
import { getAllVersions, getLatestVersion } from "../download/manifest";
|
||||||
|
import type { ResolutionStrategy } from "../utils/inputs";
|
||||||
|
import {
|
||||||
|
type ParsedVersionSpecifier,
|
||||||
|
parseVersionSpecifier,
|
||||||
|
} from "./specifier";
|
||||||
|
import type { ResolveUvVersionOptions } from "./types";
|
||||||
|
import { resolveVersionRequest } from "./version-request-resolver";
|
||||||
|
|
||||||
export function getUvVersionFromFile(filePath: string): string | undefined {
|
interface ConcreteVersionResolutionContext {
|
||||||
core.info(`Trying to find version for uv in: ${filePath}`);
|
manifestUrl?: string;
|
||||||
if (!fs.existsSync(filePath)) {
|
parsedSpecifier: ParsedVersionSpecifier;
|
||||||
core.info(`Could not find file: ${filePath}`);
|
resolutionStrategy: ResolutionStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConcreteVersionResolver {
|
||||||
|
resolve(
|
||||||
|
context: ConcreteVersionResolutionContext,
|
||||||
|
): Promise<string | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExactVersionResolver implements ConcreteVersionResolver {
|
||||||
|
async resolve(
|
||||||
|
context: ConcreteVersionResolutionContext,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (context.parsedSpecifier.kind !== "exact") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
let uvVersion: string | undefined;
|
|
||||||
try {
|
core.debug(
|
||||||
uvVersion = getUvVersionFromToolVersions(filePath);
|
`Version ${context.parsedSpecifier.normalized} is an explicit version.`,
|
||||||
if (uvVersion === undefined) {
|
);
|
||||||
uvVersion = getConfigValueFromTomlFile(filePath, "required-version");
|
return context.parsedSpecifier.normalized;
|
||||||
}
|
}
|
||||||
if (uvVersion === undefined) {
|
|
||||||
uvVersion = getUvVersionFromRequirementsFile(filePath);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
const message = (err as Error).message;
|
class LatestVersionResolver implements ConcreteVersionResolver {
|
||||||
core.warning(`Error while parsing ${filePath}: ${message}`);
|
async resolve(
|
||||||
|
context: ConcreteVersionResolutionContext,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const shouldUseLatestVersion =
|
||||||
|
context.parsedSpecifier.kind === "latest" ||
|
||||||
|
(context.parsedSpecifier.kind === "range" &&
|
||||||
|
context.parsedSpecifier.isSimpleMinimumVersionSpecifier &&
|
||||||
|
context.resolutionStrategy === "highest");
|
||||||
|
|
||||||
|
if (!shouldUseLatestVersion) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (uvVersion?.startsWith("==")) {
|
|
||||||
uvVersion = uvVersion.slice(2);
|
if (
|
||||||
|
context.parsedSpecifier.kind === "range" &&
|
||||||
|
context.parsedSpecifier.isSimpleMinimumVersionSpecifier
|
||||||
|
) {
|
||||||
|
core.info("Found minimum version specifier, using latest version");
|
||||||
}
|
}
|
||||||
if (uvVersion !== undefined) {
|
|
||||||
core.info(`Found version for uv in ${filePath}: ${uvVersion}`);
|
const latestVersion = await getLatestVersion(context.manifestUrl);
|
||||||
|
|
||||||
|
if (
|
||||||
|
context.parsedSpecifier.kind === "range" &&
|
||||||
|
context.parsedSpecifier.isSimpleMinimumVersionSpecifier &&
|
||||||
|
!pep440.satisfies(latestVersion, context.parsedSpecifier.raw)
|
||||||
|
) {
|
||||||
|
throw new Error(`No version found for ${context.parsedSpecifier.raw}`);
|
||||||
}
|
}
|
||||||
return uvVersion;
|
|
||||||
|
return latestVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RangeVersionResolver implements ConcreteVersionResolver {
|
||||||
|
async resolve(
|
||||||
|
context: ConcreteVersionResolutionContext,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (context.parsedSpecifier.kind !== "range") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableVersions = await getAllVersions(context.manifestUrl);
|
||||||
|
core.debug(`Available versions: ${availableVersions}`);
|
||||||
|
|
||||||
|
const resolvedVersion =
|
||||||
|
context.resolutionStrategy === "lowest"
|
||||||
|
? minSatisfying(availableVersions, context.parsedSpecifier.normalized)
|
||||||
|
: maxSatisfying(availableVersions, context.parsedSpecifier.normalized);
|
||||||
|
|
||||||
|
if (resolvedVersion === undefined) {
|
||||||
|
throw new Error(`No version found for ${context.parsedSpecifier.raw}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONCRETE_VERSION_RESOLVERS: ConcreteVersionResolver[] = [
|
||||||
|
new ExactVersionResolver(),
|
||||||
|
new LatestVersionResolver(),
|
||||||
|
new RangeVersionResolver(),
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function resolveUvVersion(
|
||||||
|
options: ResolveUvVersionOptions,
|
||||||
|
): Promise<string> {
|
||||||
|
const request = resolveVersionRequest(options);
|
||||||
|
const resolutionStrategy = options.resolutionStrategy ?? "highest";
|
||||||
|
const version = await resolveVersion(
|
||||||
|
request.specifier,
|
||||||
|
options.manifestFile,
|
||||||
|
resolutionStrategy,
|
||||||
|
);
|
||||||
|
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveVersion(
|
||||||
|
versionInput: string,
|
||||||
|
manifestUrl: string | undefined,
|
||||||
|
resolutionStrategy: ResolutionStrategy = "highest",
|
||||||
|
): Promise<string> {
|
||||||
|
core.debug(`Resolving version: ${versionInput}`);
|
||||||
|
|
||||||
|
const context: ConcreteVersionResolutionContext = {
|
||||||
|
manifestUrl,
|
||||||
|
parsedSpecifier: parseVersionSpecifier(versionInput),
|
||||||
|
resolutionStrategy,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const resolver of CONCRETE_VERSION_RESOLVERS) {
|
||||||
|
const version = await resolver.resolve(context);
|
||||||
|
if (version !== undefined) {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`No version found for ${versionInput}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maxSatisfying(
|
||||||
|
versions: string[],
|
||||||
|
version: string,
|
||||||
|
): string | undefined {
|
||||||
|
const maxSemver = tc.evaluateVersions(versions, version);
|
||||||
|
if (maxSemver !== "") {
|
||||||
|
core.debug(`Found a version that satisfies the semver range: ${maxSemver}`);
|
||||||
|
return maxSemver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxPep440 = pep440.maxSatisfying(versions, version);
|
||||||
|
if (maxPep440 !== null) {
|
||||||
|
core.debug(
|
||||||
|
`Found a version that satisfies the pep440 specifier: ${maxPep440}`,
|
||||||
|
);
|
||||||
|
return maxPep440;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function minSatisfying(
|
||||||
|
versions: string[],
|
||||||
|
version: string,
|
||||||
|
): string | undefined {
|
||||||
|
const minSemver = semver.minSatisfying(versions, version);
|
||||||
|
if (minSemver !== null) {
|
||||||
|
core.debug(`Found a version that satisfies the semver range: ${minSemver}`);
|
||||||
|
return minSemver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minPep440 = pep440.minSatisfying(versions, version);
|
||||||
|
if (minPep440 !== null) {
|
||||||
|
core.debug(
|
||||||
|
`Found a version that satisfies the pep440 specifier: ${minPep440}`,
|
||||||
|
);
|
||||||
|
return minPep440;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/version/specifier.ts
Normal file
59
src/version/specifier.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as tc from "@actions/tool-cache";
|
||||||
|
|
||||||
|
export type ParsedVersionSpecifier =
|
||||||
|
| {
|
||||||
|
kind: "exact";
|
||||||
|
normalized: string;
|
||||||
|
raw: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: "latest";
|
||||||
|
normalized: "latest";
|
||||||
|
raw: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
isSimpleMinimumVersionSpecifier: boolean;
|
||||||
|
kind: "range";
|
||||||
|
normalized: string;
|
||||||
|
raw: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function normalizeVersionSpecifier(specifier: string): string {
|
||||||
|
const trimmedSpecifier = specifier.trim();
|
||||||
|
|
||||||
|
if (trimmedSpecifier.startsWith("==")) {
|
||||||
|
return trimmedSpecifier.slice(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedSpecifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseVersionSpecifier(
|
||||||
|
specifier: string,
|
||||||
|
): ParsedVersionSpecifier {
|
||||||
|
const raw = specifier.trim();
|
||||||
|
const normalized = normalizeVersionSpecifier(raw);
|
||||||
|
|
||||||
|
if (normalized === "latest") {
|
||||||
|
return {
|
||||||
|
kind: "latest",
|
||||||
|
normalized: "latest",
|
||||||
|
raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tc.isExplicitVersion(normalized)) {
|
||||||
|
return {
|
||||||
|
kind: "exact",
|
||||||
|
normalized,
|
||||||
|
raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSimpleMinimumVersionSpecifier: raw.includes(">") && !raw.includes(","),
|
||||||
|
kind: "range",
|
||||||
|
normalized,
|
||||||
|
raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
34
src/version/types.ts
Normal file
34
src/version/types.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { ResolutionStrategy } from "../utils/inputs";
|
||||||
|
|
||||||
|
export type VersionSource =
|
||||||
|
| "input"
|
||||||
|
| "version-file"
|
||||||
|
| "uv.toml"
|
||||||
|
| "pyproject.toml"
|
||||||
|
| "default";
|
||||||
|
|
||||||
|
export type VersionFileFormat =
|
||||||
|
| ".tool-versions"
|
||||||
|
| "pyproject.toml"
|
||||||
|
| "requirements"
|
||||||
|
| "uv.toml";
|
||||||
|
|
||||||
|
export interface ParsedVersionFile {
|
||||||
|
format: VersionFileFormat;
|
||||||
|
specifier: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolveUvVersionOptions {
|
||||||
|
manifestFile?: string;
|
||||||
|
resolutionStrategy?: ResolutionStrategy;
|
||||||
|
version?: string;
|
||||||
|
versionFile?: string;
|
||||||
|
workingDirectory: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionRequest {
|
||||||
|
format?: VersionFileFormat;
|
||||||
|
source: VersionSource;
|
||||||
|
sourcePath?: string;
|
||||||
|
specifier: string;
|
||||||
|
}
|
||||||
158
src/version/version-request-resolver.ts
Normal file
158
src/version/version-request-resolver.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import * as path from "node:path";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
import { getParsedVersionFile } from "./file-parser";
|
||||||
|
import { normalizeVersionSpecifier } from "./specifier";
|
||||||
|
import type {
|
||||||
|
ParsedVersionFile,
|
||||||
|
ResolveUvVersionOptions,
|
||||||
|
VersionRequest,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export interface VersionRequestResolver {
|
||||||
|
resolve(context: VersionRequestContext): VersionRequest | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VersionRequestContext {
|
||||||
|
readonly version: string | undefined;
|
||||||
|
readonly versionFile: string | undefined;
|
||||||
|
readonly workingDirectory: string;
|
||||||
|
|
||||||
|
private readonly parsedFiles = new Map<
|
||||||
|
string,
|
||||||
|
ParsedVersionFile | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
version: string | undefined,
|
||||||
|
versionFile: string | undefined,
|
||||||
|
workingDirectory: string,
|
||||||
|
) {
|
||||||
|
this.version = version;
|
||||||
|
this.versionFile = versionFile;
|
||||||
|
this.workingDirectory = workingDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVersionFile(filePath: string): ParsedVersionFile | undefined {
|
||||||
|
const cachedResult = this.parsedFiles.get(filePath);
|
||||||
|
if (cachedResult !== undefined || this.parsedFiles.has(filePath)) {
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = getParsedVersionFile(filePath);
|
||||||
|
this.parsedFiles.set(filePath, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
getWorkspaceCandidates(): Array<{
|
||||||
|
source: "pyproject.toml" | "uv.toml";
|
||||||
|
sourcePath: string;
|
||||||
|
}> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "uv.toml",
|
||||||
|
sourcePath: path.join(this.workingDirectory, "uv.toml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "pyproject.toml",
|
||||||
|
sourcePath: path.join(this.workingDirectory, "pyproject.toml"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExplicitInputVersionResolver implements VersionRequestResolver {
|
||||||
|
resolve(context: VersionRequestContext): VersionRequest | undefined {
|
||||||
|
if (context.version === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
source: "input",
|
||||||
|
specifier: normalizeVersionSpecifier(context.version),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VersionFileVersionResolver implements VersionRequestResolver {
|
||||||
|
resolve(context: VersionRequestContext): VersionRequest | undefined {
|
||||||
|
if (context.versionFile === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionFile = context.getVersionFile(context.versionFile);
|
||||||
|
if (versionFile === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not determine uv version from file: ${context.versionFile}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
format: versionFile.format,
|
||||||
|
source: "version-file",
|
||||||
|
sourcePath: context.versionFile,
|
||||||
|
specifier: versionFile.specifier,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorkspaceVersionResolver implements VersionRequestResolver {
|
||||||
|
resolve(context: VersionRequestContext): VersionRequest | undefined {
|
||||||
|
for (const candidate of context.getWorkspaceCandidates()) {
|
||||||
|
const versionFile = context.getVersionFile(candidate.sourcePath);
|
||||||
|
if (versionFile === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
format: versionFile.format,
|
||||||
|
source: candidate.source,
|
||||||
|
sourcePath: candidate.sourcePath,
|
||||||
|
specifier: versionFile.specifier,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(
|
||||||
|
"Could not determine uv version from uv.toml or pyproject.toml. Falling back to latest.",
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LatestVersionResolver implements VersionRequestResolver {
|
||||||
|
resolve(): VersionRequest {
|
||||||
|
return {
|
||||||
|
source: "default",
|
||||||
|
specifier: "latest",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const VERSION_REQUEST_RESOLVERS: VersionRequestResolver[] = [
|
||||||
|
new ExplicitInputVersionResolver(),
|
||||||
|
new VersionFileVersionResolver(),
|
||||||
|
new WorkspaceVersionResolver(),
|
||||||
|
new LatestVersionResolver(),
|
||||||
|
];
|
||||||
|
|
||||||
|
export function resolveVersionRequest(
|
||||||
|
options: ResolveUvVersionOptions,
|
||||||
|
): VersionRequest {
|
||||||
|
const context = new VersionRequestContext(
|
||||||
|
emptyToUndefined(options.version),
|
||||||
|
emptyToUndefined(options.versionFile),
|
||||||
|
options.workingDirectory,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const resolver of VERSION_REQUEST_RESOLVERS) {
|
||||||
|
const request = resolver.resolve(context);
|
||||||
|
if (request !== undefined) {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Could not resolve a requested uv version.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyToUndefined(value: string | undefined): string | undefined {
|
||||||
|
return value === undefined || value === "" ? undefined : value;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user