Files
Michael Hofmann 934c01dd62 feat(pip-compile): support --constraint alias and --overrides for uv (#43485)
* feat(pip-compile): support --constraint alias and --overrides for uv

uv accepts both --constraint and --constraints (clap aliases) and
records the exact form used into lockfile headers. Only --constraints
was in the uv allowlist, so --constraint headers were rejected.

Similarly, uv supports --override/--overrides for version overrides,
but neither form was recognized by Renovate at all.

This commit:
- adds --constraint to uvOptionsWithArguments (value handler already
  handles both forms)
- adds --override and --overrides to uvOptionsWithArguments with a
  value handler that parses overridesFiles
- tracks override files as dependencies in the file graph (like
  constraints) so lockfiles are recompiled when overrides change
- uses dotted mermaid edges for overrides (same as constraints)

Ref: https://github.com/renovatebot/renovate/discussions/39488
Co-authored-by: Cursor <cursoragent@cursor.com>

* test(pip-compile): add coverage for overrides dependency tracking

Add a test case in extract.spec.ts that exercises the overridesFiles
loop in extractAllPackageFiles, fixing the codecov/patch failure.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 12:45:23 +00:00

98 lines
3.3 KiB
TypeScript

import { Graph, topologicalSort } from 'graph-data-structure';
import upath from 'upath';
import { logger } from '../../../logger/index.ts';
import type { PackageFile } from '../types.ts';
import type { DependencyBetweenFiles, PipCompileArgs } from './types.ts';
export function sortPackageFiles(
depsBetweenFiles: DependencyBetweenFiles[],
packageFiles: Map<string, PackageFile>,
): PackageFile[] {
const result: PackageFile[] = [];
const graph = new Graph();
depsBetweenFiles.forEach(({ sourceFile, outputFile }) => {
graph.addEdge(sourceFile, outputFile);
});
const sorted = topologicalSort(graph);
for (const file of sorted) {
if (packageFiles.has(file)) {
const packageFile = packageFiles.get(file)!;
const sortedLockFiles = [];
// TODO(not7cd): this needs better test case
for (const lockFile of packageFile.lockFiles!) {
if (sorted.includes(lockFile)) {
sortedLockFiles.push(lockFile);
}
}
packageFile.lockFiles = sortedLockFiles;
result.push(packageFile);
}
}
// istanbul ignore if: should never happen
if (result.length !== packageFiles.size) {
throw new Error('Topological sort failed to include all package files');
}
return result;
}
export function generateMermaidGraph(
depsBetweenFiles: DependencyBetweenFiles[],
lockFileArgs: Map<string, PipCompileArgs>,
): string {
const lockFiles = [];
for (const lockFile of lockFileArgs.keys()) {
// TODO: add extra args to the lock file ${extraArgs ? '\n' + extraArgs : ''}
// const extraArgs = pipCompileArgs.extra
// ?.map((v) => '--extra=' + v)
// .join('\n');
lockFiles.push(` ${lockFile}[[${lockFile}]]`);
}
const edges = depsBetweenFiles.map(({ sourceFile, outputFile, type }) => {
return ` ${sourceFile} -${type === 'requirement' ? '' : '.'}-> ${outputFile}`;
});
return `graph TD\n${lockFiles.join('\n')}\n${edges.join('\n')}`;
}
export function inferCommandExecDir(
outputFilePath: string,
outputFileArg: string | undefined,
): string {
if (!outputFileArg) {
// implicit output file is in the same directory where command was executed
return upath.normalize(upath.dirname(outputFilePath));
}
if (upath.normalize(outputFileArg).startsWith('..')) {
throw new Error(
`Cannot infer command execution directory from path ${outputFileArg}`,
);
}
if (upath.basename(outputFileArg) !== upath.basename(outputFilePath)) {
throw new Error(
`Output file name mismatch: ${upath.basename(outputFileArg)} vs ${upath.basename(outputFilePath)}`,
);
}
const outputFileDir = upath.normalize(upath.dirname(outputFileArg));
let commandExecDir = upath.normalize(upath.dirname(outputFilePath));
for (const dir of outputFileDir.split('/').reverse()) {
if (commandExecDir.endsWith(dir)) {
commandExecDir = upath.join(commandExecDir.slice(0, -dir.length), '.');
// outputFileDir = upath.join(outputFileDir.slice(0, -dir.length), '.');
} else {
break;
}
}
commandExecDir = upath.normalizeTrim(commandExecDir);
if (commandExecDir !== '.') {
logger.debug(
{
commandExecDir,
outputFileArg,
outputFilePath,
},
`pip-compile: command was not executed in repository root`,
);
}
return commandExecDir;
}