UNPKG

renovate

Version:

Automated dependency updates. Flexible so you don't need to be.

317 lines • 13 kB
"use strict"; 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