renovate/lib/modules/manager/pep621/processors/uv.ts

194 lines
5.7 KiB
TypeScript

import is from '@sindresorhus/is';
import { quote } from 'shlex';
import { TEMPORARY_ERROR } from '../../../../constants/error-messages';
import { logger } from '../../../../logger';
import { exec } from '../../../../util/exec';
import type { ExecOptions, ToolConstraint } from '../../../../util/exec/types';
import { getSiblingFileName, readLocalFile } from '../../../../util/fs';
import { Result } from '../../../../util/result';
import type {
PackageDependency,
UpdateArtifact,
UpdateArtifactsResult,
Upgrade,
} from '../../types';
import { type PyProject, UvLockfileSchema } from '../schema';
import { depTypes, parseDependencyList } from '../utils';
import type { PyProjectProcessor } from './types';
const uvUpdateCMD = 'uv lock';
export class UvProcessor implements PyProjectProcessor {
process(project: PyProject, deps: PackageDependency[]): PackageDependency[] {
const uv = project.tool?.uv;
if (is.nullOrUndefined(uv)) {
return deps;
}
deps.push(
...parseDependencyList(
depTypes.uvDevDependencies,
uv['dev-dependencies'],
),
);
// https://docs.astral.sh/uv/concepts/dependencies/#dependency-sources
// Skip sources that are either not yet handled by Renovate (e.g. git), or do not make sense to handle (e.g. path).
if (uv.sources) {
for (const dep of deps) {
if (!dep.depName) {
continue;
}
const depSource = uv.sources[dep.depName];
if (depSource) {
if (depSource.git) {
dep.skipReason = 'git-dependency';
} else if (depSource.url) {
dep.skipReason = 'unsupported-url';
} else if (depSource.path) {
dep.skipReason = 'path-dependency';
} else if (depSource.workspace) {
dep.skipReason = 'inherited-dependency';
} else {
dep.skipReason = 'invalid-dependency-specification';
}
}
}
}
return deps;
}
async extractLockedVersions(
project: PyProject,
deps: PackageDependency[],
packageFile: string,
): Promise<PackageDependency[]> {
const lockFileName = getSiblingFileName(packageFile, 'uv.lock');
const lockFileContent = await readLocalFile(lockFileName, 'utf8');
if (lockFileContent) {
const { val: lockFileMapping, err } = Result.parse(
lockFileContent,
UvLockfileSchema,
).unwrap();
if (err) {
logger.debug({ packageFile, err }, `Error parsing uv lock file`);
} else {
for (const dep of deps) {
const packageName = dep.packageName;
if (packageName && packageName in lockFileMapping) {
dep.lockedVersion = lockFileMapping[packageName];
}
}
}
}
return Promise.resolve(deps);
}
async updateArtifacts(
updateArtifact: UpdateArtifact,
project: PyProject,
): Promise<UpdateArtifactsResult[] | null> {
const { config, updatedDeps, packageFileName } = updateArtifact;
const isLockFileMaintenance = config.updateType === 'lockFileMaintenance';
// abort if no lockfile is defined
const lockFileName = getSiblingFileName(packageFileName, 'uv.lock');
try {
const existingLockFileContent = await readLocalFile(lockFileName, 'utf8');
if (is.nullOrUndefined(existingLockFileContent)) {
logger.debug('No uv.lock found');
return null;
}
const pythonConstraint: ToolConstraint = {
toolName: 'python',
constraint:
config.constraints?.python ?? project.project?.['requires-python'],
};
const uvConstraint: ToolConstraint = {
toolName: 'uv',
constraint: config.constraints?.uv,
};
const execOptions: ExecOptions = {
cwdFile: packageFileName,
docker: {},
userConfiguredEnv: config.env,
toolConstraints: [pythonConstraint, uvConstraint],
};
// on lockFileMaintenance do not specify any packages and update the complete lock file
// else only update specific packages
let cmd: string;
if (isLockFileMaintenance) {
cmd = `${uvUpdateCMD} --upgrade`;
} else {
cmd = generateCMD(updatedDeps);
}
await exec(cmd, execOptions);
// check for changes
const fileChanges: UpdateArtifactsResult[] = [];
const newLockContent = await readLocalFile(lockFileName, 'utf8');
const isLockFileChanged = existingLockFileContent !== newLockContent;
if (isLockFileChanged) {
fileChanges.push({
file: {
type: 'addition',
path: lockFileName,
contents: newLockContent,
},
});
} else {
logger.debug('uv.lock is unchanged');
}
return fileChanges.length ? fileChanges : null;
} catch (err) {
// istanbul ignore if
if (err.message === TEMPORARY_ERROR) {
throw err;
}
logger.debug({ err }, 'Failed to update uv lock file');
return [
{
artifactError: {
lockFile: lockFileName,
stderr: err.message,
},
},
];
}
}
}
function generateCMD(updatedDeps: Upgrade[]): string {
const deps: string[] = [];
for (const dep of updatedDeps) {
switch (dep.depType) {
case depTypes.optionalDependencies: {
deps.push(dep.depName!.split('/')[1]);
break;
}
case depTypes.uvDevDependencies: {
deps.push(dep.depName!);
break;
}
case depTypes.buildSystemRequires:
// build requirements are not locked in the lock files, no need to update.
break;
default: {
deps.push(dep.packageName!);
}
}
}
return `${uvUpdateCMD} ${deps.map((dep) => `--upgrade-package ${quote(dep)}`).join(' ')}`;
}