renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
317 lines • 13 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.UvProcessor = void 0;
const tslib_1 = require("tslib");
const is_1 = tslib_1.__importDefault(require("@sindresorhus/is"));
const shlex_1 = require("shlex");
const error_messages_1 = require("../../../../constants/error-messages");
const logger_1 = require("../../../../logger");
const exec_1 = require("../../../../util/exec");
const fs_1 = require("../../../../util/fs");
const auth_1 = require("../../../../util/git/auth");
const host_rules_1 = require("../../../../util/host-rules");
const result_1 = require("../../../../util/result");
const url_1 = require("../../../../util/url");
const pypi_1 = require("../../../datasource/pypi");
const util_1 = require("../../../datasource/util");
const util_2 = require("../../util");
const schema_1 = require("../schema");
const utils_1 = require("../utils");
const uvUpdateCMD = 'uv lock';
class UvProcessor {
process(project, deps) {
const uv = project.tool?.uv;
if (!uv) {
return deps;
}
const hasExplicitDefault = uv.index?.some((index) => index.default && index.explicit);
const defaultIndex = uv.index?.find((index) => index.default && !index.explicit);
const implicitIndexUrls = uv.index
?.filter((index) => !index.explicit && index.name !== defaultIndex?.name)
?.map(({ url }) => url);
const devDependencies = uv['dev-dependencies'];
if (devDependencies) {
deps.push(...devDependencies);
}
// https://docs.astral.sh/uv/concepts/dependencies/#dependency-sources
// Skip sources that do not make sense to handle (e.g. path).
if (uv.sources || defaultIndex || implicitIndexUrls) {
for (const dep of deps) {
/* v8 ignore next 3 -- needs test */
if (!dep.packageName) {
continue;
}
if (dep.depType === 'requires-python') {
continue;
}
// Using `packageName` as it applies PEP 508 normalization, which is
// also applied by uv when matching a source to a dependency.
const depSource = uv.sources?.[dep.packageName];
if (depSource) {
// Dependency is pinned to a specific source.
dep.depType = utils_1.depTypes.uvSources;
if ('index' in depSource) {
const index = uv.index?.find(({ name }) => name === depSource.index);
if (index) {
dep.registryUrls = [index.url];
}
}
else if ('git' in depSource) {
(0, util_2.applyGitSource)(dep, depSource.git, depSource.rev, depSource.tag, depSource.branch);
}
else if ('url' in depSource) {
dep.skipReason = 'unsupported-url';
}
else if ('path' in depSource) {
dep.skipReason = 'path-dependency';
}
else if ('workspace' in depSource) {
dep.skipReason = 'inherited-dependency';
/* v8 ignore next 3 -- needs test */
}
else {
dep.skipReason = 'unknown-registry';
}
}
else {
// Dependency is not pinned to a specific source, so we need to
// determine the source based on the index configuration.
if (hasExplicitDefault) {
// don't fall back to pypi if there is an explicit default index
dep.registryUrls = [];
}
else if (defaultIndex) {
// There is a default index configured, so use it.
dep.registryUrls = [defaultIndex.url];
}
if (implicitIndexUrls?.length) {
// If there are implicit indexes, check them first and fall back
// to the default.
dep.registryUrls = implicitIndexUrls.concat(dep.registryUrls ?? pypi_1.PypiDatasource.defaultURL);
}
}
}
}
return deps;
}
async extractLockedVersions(project, deps, packageFile) {
const lockFileName = await (0, fs_1.findLocalSiblingOrParent)(packageFile, 'uv.lock');
if (lockFileName === null) {
logger_1.logger.debug({ packageFile }, `No uv lock file found`);
}
else {
const lockFileContent = await (0, fs_1.readLocalFile)(lockFileName, 'utf8');
if (lockFileContent) {
const { val: lockFileMapping, err } = result_1.Result.parse(lockFileContent, schema_1.UvLockfile).unwrap();
if (err) {
logger_1.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 getLockfiles(_project, lockfiles, packageFile) {
const lockFileName = await (0, fs_1.findLocalSiblingOrParent)(packageFile, 'uv.lock');
if (!lockFileName) {
logger_1.logger.debug({ packageFile }, `No uv lock file found`);
return lockfiles;
}
lockfiles.push(lockFileName);
return lockfiles;
}
async updateArtifacts(updateArtifact, project) {
const { config, updatedDeps, packageFileName } = updateArtifact;
const { isLockFileMaintenance } = config;
// abort if no lockfile is defined
const lockFileName = await (0, fs_1.findLocalSiblingOrParent)(packageFileName, 'uv.lock');
if (lockFileName === null) {
logger_1.logger.debug({ packageFileName }, `No uv lock file found`);
return null;
}
try {
const existingLockFileContent = await (0, fs_1.readLocalFile)(lockFileName, 'utf8');
if (!existingLockFileContent) {
logger_1.logger.debug('No uv.lock found');
return null;
}
const pythonConstraint = {
toolName: 'python',
constraint: config.constraints?.python ?? project.project?.['requires-python'],
};
const uvConstraint = {
toolName: 'uv',
constraint: config.constraints?.uv ?? project.tool?.uv?.['required-version'],
};
const extraEnv = {
...(0, auth_1.getGitEnvironmentVariables)(['pep621']),
...(await getUvExtraIndexUrl(project, updateArtifact.updatedDeps)),
...(await getUvIndexCredentials(project)),
};
const execOptions = {
cwdFile: packageFileName,
extraEnv,
docker: {},
toolConstraints: [pythonConstraint, uvConstraint],
};
// on lockFileMaintenance do not specify any packages and update the complete lock file
// else only update specific packages
let cmd;
if (isLockFileMaintenance) {
cmd = `${uvUpdateCMD} --upgrade`;
}
else {
cmd = generateCMD(updatedDeps);
}
await (0, exec_1.exec)(cmd, execOptions);
// check for changes
const fileChanges = [];
const newLockContent = await (0, fs_1.readLocalFile)(lockFileName, 'utf8');
const isLockFileChanged = existingLockFileContent !== newLockContent;
if (isLockFileChanged) {
fileChanges.push({
file: {
type: 'addition',
path: lockFileName,
contents: newLockContent,
},
});
}
else {
logger_1.logger.debug('uv.lock is unchanged');
}
return fileChanges.length ? fileChanges : null;
}
catch (err) {
if (err.message === error_messages_1.TEMPORARY_ERROR) {
throw err;
}
logger_1.logger.debug({ err }, 'Failed to update uv lock file');
return [
{
artifactError: {
lockFile: lockFileName,
stderr: err.message,
},
},
];
}
}
}
exports.UvProcessor = UvProcessor;
function generateCMD(updatedDeps) {
const deps = [];
for (const dep of updatedDeps) {
switch (dep.depType) {
case utils_1.depTypes.optionalDependencies: {
deps.push(dep.depName);
break;
}
case utils_1.depTypes.uvDevDependencies:
case utils_1.depTypes.uvSources: {
deps.push(dep.depName);
break;
}
case utils_1.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 ${(0, shlex_1.quote)(dep)}`).join(' ')}`;
}
function getMatchingHostRule(url) {
return (0, host_rules_1.find)({ hostType: pypi_1.PypiDatasource.id, url });
}
async function getUsernamePassword(url) {
const rule = getMatchingHostRule(url.toString());
if (rule.username || rule.password) {
return rule;
}
if (url.hostname.endsWith('.pkg.dev')) {
const hostRule = await (0, util_1.getGoogleAuthHostRule)();
if (hostRule) {
return hostRule;
}
else {
logger_1.logger.once.debug({ url }, 'Could not get Google access token');
}
}
return {};
}
async function getUvExtraIndexUrl(project, deps) {
const pyPiRegistryUrls = deps
.filter((dep) => dep.datasource === pypi_1.PypiDatasource.id)
.filter((dep) => {
// Remove dependencies that are pinned to a specific index
const sources = project.tool?.uv?.sources;
const packageName = dep.packageName;
return !sources || !(packageName in sources);
})
.flatMap((dep) => dep.registryUrls)
.filter(is_1.default.string)
.filter((registryUrl) => {
// Check if the registry URL is not the default one and not already configured
const configuredIndexUrls = project.tool?.uv?.index?.map(({ url }) => url) ?? [];
return (registryUrl !== pypi_1.PypiDatasource.defaultURL &&
!configuredIndexUrls.includes(registryUrl));
});
const registryUrls = new Set(pyPiRegistryUrls);
const extraIndexUrls = [];
for (const registryUrl of registryUrls) {
const parsedUrl = (0, url_1.parseUrl)(registryUrl);
if (!parsedUrl) {
continue;
}
const { username, password } = await getUsernamePassword(parsedUrl);
if (username || password) {
if (username) {
parsedUrl.username = username;
}
if (password) {
parsedUrl.password = password;
}
}
extraIndexUrls.push(parsedUrl.toString());
}
return {
UV_EXTRA_INDEX_URL: extraIndexUrls.join(' '),
};
}
async function getUvIndexCredentials(project) {
const uv_indexes = project.tool?.uv?.index;
if (!uv_indexes) {
return {};
}
const entries = [];
for (const { name, url } of uv_indexes) {
const parsedUrl = (0, url_1.parseUrl)(url);
/* v8 ignore next 3 -- needs test */
if (!parsedUrl) {
continue;
}
// If no name is provided for the index, authentication information must be passed through alternative methods
if (!name) {
continue;
}
const { username, password } = await getUsernamePassword(parsedUrl);
const NAME = name.toUpperCase().replace(/[^A-Z0-9]/g, '_');
if (username) {
entries.push([`UV_INDEX_${NAME}_USERNAME`, username]);
}
if (password) {
entries.push([`UV_INDEX_${NAME}_PASSWORD`, password]);
}
}
return Object.fromEntries(entries);
}
//# sourceMappingURL=uv.js.map