mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 18:19:51 +00:00
55a7341be2
* test(manager): close azure-pipelines, bundler and cocoapods gaps Adds a pipeline whose step task is not versioned, so nothing is extracted, and a gem lock line carrying no version, which is skipped. Marks four guards that cannot be false: `extractContainer()` always returns a dep, `getLockFilePath()` always returns a path, and the cocoapods dep name, spec name and git account all derive from values their enclosing check already proved present. Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(manager): close cargo, composer and cpanfile gaps Adds a composer repository map that disables something other than packagist, which is dropped rather than recorded. Marks five guards: the cargo warning only fires for a crate dep and its retry only runs when a dep was already current, a gitlab rule without a token never passes the auth check, and both cpanfile handlers run only once their match produced a value. Two implicit elses stay counted, in the cargo version resolver and the composer repository mapper, since a coverage-ignore hint does not attach to an `else if`. Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(manager): close deps-edn, docker-compose and dockerfile gaps Adds three extractions: a deps.edn alias whose value is not a map, a compose extension carrying no image, and a dockerfile escape directive naming a character other than a backtick, which leaves the default in place. Marks the deps-edn repository name check, since object keys are always strings, and notes the repository spec chain, where a coverage-ignore hint does not attach to an `else if`. Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(manager/flux): close the flux coverage gaps Adds two GitRepository resources whose url is ssh rather than http, one pinned to a tag and one to a commit. Neither yields a source url, since only http urls can be turned into one. Marks the Kustomization image guard: the schema rejects an image without a name, so `extractImage()` cannot return null there. Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(manager): mark github-actions, gitlabci and glasskube guards None of these six needs a test. A parsed action ref is either a docker or a repository ref, `getDep()` always returns a dep for a job container and a service, and the remaining three need a listed file that cannot be read, a listed file that yields no deps, or a manifest whose repositories match no name. The glasskube resource kind chain stays counted, since a coverage-ignore hint does not attach to an `else if`. Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(manager): mark gomod and gradle-wrapper guards Four guards that need setups no test provides: a removed dep that is not added back, a go.mod the replace massaging leaves unchanged, and a daemon-jvm file carrying no toolchain version. A parsed go directive always yields a version. The additional-dependency count chain stays counted, since a coverage-ignore hint does not attach to an `else if`. Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io> --------- Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
308 lines
9.0 KiB
TypeScript
308 lines
9.0 KiB
TypeScript
import { isObject, isString } from '@sindresorhus/is';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import { coerceArray } from '../../../util/array.ts';
|
|
import { getEnv } from '../../../util/env.ts';
|
|
import {
|
|
findLocalSiblingOrParent,
|
|
readLocalFile,
|
|
} from '../../../util/fs/index.ts';
|
|
import { coerceObject } from '../../../util/object.ts';
|
|
import { api as versioning } from '../../versioning/cargo/index.ts';
|
|
import type {
|
|
ExtractConfig,
|
|
PackageDependency,
|
|
PackageFileContent,
|
|
} from '../types.ts';
|
|
import { extractLockFileVersions } from './locked-version.ts';
|
|
import { CargoConfig, CargoManifest } from './schema.ts';
|
|
import type {
|
|
CargoManagerData,
|
|
CargoRegistries,
|
|
CargoRegistryUrl,
|
|
} from './types.ts';
|
|
import { DEFAULT_REGISTRY_URL } from './utils.ts';
|
|
|
|
const DEFAULT_REGISTRY_ID = 'crates-io';
|
|
|
|
function getCargoIndexEnv(registryName: string): string | null {
|
|
const registry = registryName.toUpperCase().replaceAll('-', '_');
|
|
return getEnv()[`CARGO_REGISTRIES_${registry}_INDEX`] ?? null;
|
|
}
|
|
|
|
function extractFromSection(
|
|
dependencies: PackageDependency<CargoManagerData>[] | undefined,
|
|
cargoRegistries: CargoRegistries,
|
|
target?: string,
|
|
): PackageDependency[] {
|
|
if (!dependencies) {
|
|
return [];
|
|
}
|
|
|
|
const deps: PackageDependency<CargoManagerData>[] = [];
|
|
|
|
for (const dep of Object.values(dependencies)) {
|
|
let registryUrls: string[] | undefined;
|
|
|
|
if (dep.managerData?.registryName) {
|
|
const registryUrl =
|
|
getCargoIndexEnv(dep.managerData.registryName) ??
|
|
cargoRegistries[dep.managerData?.registryName];
|
|
if (registryUrl) {
|
|
if (registryUrl !== DEFAULT_REGISTRY_URL) {
|
|
registryUrls = [registryUrl];
|
|
}
|
|
} else {
|
|
dep.skipReason = 'unknown-registry';
|
|
}
|
|
}
|
|
|
|
if (registryUrls) {
|
|
dep.registryUrls = registryUrls;
|
|
} else {
|
|
// if we don't have an explicit registry URL check if the default registry has a non-standard url
|
|
if (cargoRegistries[DEFAULT_REGISTRY_ID]) {
|
|
if (cargoRegistries[DEFAULT_REGISTRY_ID] !== DEFAULT_REGISTRY_URL) {
|
|
dep.registryUrls = [cargoRegistries[DEFAULT_REGISTRY_ID]];
|
|
}
|
|
} else {
|
|
// we always expect to have DEFAULT_REGISTRY_ID set, if it's not it means the config defines an alternative
|
|
// registry that we couldn't resolve.
|
|
dep.skipReason = 'unknown-registry';
|
|
}
|
|
}
|
|
|
|
if (target) {
|
|
dep.target = target;
|
|
}
|
|
deps.push(dep);
|
|
}
|
|
|
|
return deps;
|
|
}
|
|
|
|
/** Reads `.cargo/config.toml`, or, if not found, `.cargo/config` */
|
|
async function readCargoConfig(): Promise<CargoConfig | null> {
|
|
for (const configName of ['config.toml', 'config']) {
|
|
const path = `.cargo/${configName}`;
|
|
const payload = await readLocalFile(path, 'utf8');
|
|
if (payload) {
|
|
const parsedCargoConfig = CargoConfig.safeParse(payload);
|
|
if (parsedCargoConfig.success) {
|
|
return parsedCargoConfig.data;
|
|
}
|
|
logger.debug(
|
|
{ err: parsedCargoConfig.error, path },
|
|
`Error parsing cargo config`,
|
|
);
|
|
}
|
|
}
|
|
|
|
logger.debug('Neither .cargo/config nor .cargo/config.toml found');
|
|
return null;
|
|
}
|
|
|
|
/** Extracts a map of cargo registries from a CargoConfig */
|
|
function extractCargoRegistries(config: CargoConfig): CargoRegistries {
|
|
const result: CargoRegistries = {};
|
|
// check if we're overriding our default registry index
|
|
result[DEFAULT_REGISTRY_ID] = resolveRegistryIndex(
|
|
DEFAULT_REGISTRY_ID,
|
|
config,
|
|
);
|
|
|
|
const registryNames = new Set([
|
|
...Object.keys(coerceObject(config.registries)),
|
|
...Object.keys(coerceObject(config.source)),
|
|
]);
|
|
for (const registryName of registryNames) {
|
|
result[registryName] = resolveRegistryIndex(registryName, config);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function resolveRegistryIndex(
|
|
registryName: string,
|
|
config: CargoConfig,
|
|
originalNames = new Set<string>(),
|
|
): CargoRegistryUrl {
|
|
// if we have a source replacement, follow that.
|
|
// https://doc.rust-lang.org/cargo/reference/source-replacement.html
|
|
const replacementName = config.source?.[registryName]?.['replace-with'];
|
|
if (replacementName) {
|
|
logger.debug(
|
|
`Replacing index of cargo registry ${registryName} with ${replacementName}`,
|
|
);
|
|
if (originalNames.has(replacementName)) {
|
|
logger.warn({ registryName }, 'cargo registry resolves to itself');
|
|
return null;
|
|
}
|
|
return resolveRegistryIndex(
|
|
replacementName,
|
|
config,
|
|
originalNames.add(replacementName),
|
|
);
|
|
}
|
|
|
|
const sourceRegistry = config.source?.[registryName]?.registry;
|
|
if (sourceRegistry) {
|
|
logger.debug(
|
|
`Replacing cargo source registry with ${sourceRegistry} for ${registryName}`,
|
|
);
|
|
return sourceRegistry;
|
|
}
|
|
|
|
const registryIndex = config.registries?.[registryName]?.index;
|
|
if (registryIndex) {
|
|
return registryIndex;
|
|
}
|
|
// we don't need an explicit index if we're using the default registry
|
|
if (registryName === DEFAULT_REGISTRY_ID) {
|
|
return DEFAULT_REGISTRY_URL;
|
|
}
|
|
logger.debug(`${registryName} cargo registry is missing index`);
|
|
return null;
|
|
}
|
|
|
|
export async function extractPackageFile(
|
|
content: string,
|
|
packageFile: string,
|
|
_config?: ExtractConfig,
|
|
): Promise<PackageFileContent<CargoManagerData> | null> {
|
|
logger.trace(`cargo.extractPackageFile(${packageFile})`);
|
|
|
|
const cargoConfig = coerceObject(await readCargoConfig());
|
|
const cargoRegistries = extractCargoRegistries(cargoConfig);
|
|
|
|
const parsedCargoManifest = CargoManifest.safeParse(content);
|
|
if (!parsedCargoManifest.success) {
|
|
logger.debug(
|
|
{ err: parsedCargoManifest.error, packageFile },
|
|
'Error parsing Cargo.toml file',
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const cargoManifest = parsedCargoManifest.data;
|
|
|
|
/*
|
|
There are the following sections in Cargo.toml:
|
|
[package]
|
|
[dependencies]
|
|
[dev-dependencies]
|
|
[build-dependencies]
|
|
[target.*.dependencies]
|
|
[workspace.dependencies]
|
|
*/
|
|
const targetSection = cargoManifest.target;
|
|
// An array of all dependencies in the target section
|
|
let targetDeps: PackageDependency[] = [];
|
|
if (targetSection) {
|
|
const targets = Object.keys(targetSection);
|
|
targets.forEach((target) => {
|
|
const targetContent = targetSection[target];
|
|
// Dependencies for `${target}`
|
|
const deps = [
|
|
...extractFromSection(
|
|
targetContent.dependencies,
|
|
cargoRegistries,
|
|
target,
|
|
),
|
|
...extractFromSection(
|
|
targetContent['dev-dependencies'],
|
|
cargoRegistries,
|
|
target,
|
|
),
|
|
...extractFromSection(
|
|
targetContent['build-dependencies'],
|
|
cargoRegistries,
|
|
target,
|
|
),
|
|
];
|
|
targetDeps = targetDeps.concat(deps);
|
|
});
|
|
}
|
|
|
|
const workspaceSection = cargoManifest.workspace;
|
|
let workspaceDeps: PackageDependency[] = [];
|
|
if (workspaceSection) {
|
|
workspaceDeps = extractFromSection(
|
|
workspaceSection.dependencies,
|
|
cargoRegistries,
|
|
undefined,
|
|
);
|
|
}
|
|
|
|
const deps = [
|
|
...extractFromSection(cargoManifest.dependencies, cargoRegistries),
|
|
...extractFromSection(cargoManifest['dev-dependencies'], cargoRegistries),
|
|
...extractFromSection(cargoManifest['build-dependencies'], cargoRegistries),
|
|
...targetDeps,
|
|
...workspaceDeps,
|
|
];
|
|
if (!deps.length) {
|
|
return null;
|
|
}
|
|
|
|
const packageSection = cargoManifest.package;
|
|
let version: string | undefined = undefined;
|
|
if (packageSection) {
|
|
if (isString(packageSection.version)) {
|
|
version = packageSection.version;
|
|
// NOTE: a version that is neither a string nor a workspace reference
|
|
// leaves it unset. A coverage-ignore hint cannot suppress the implicit
|
|
// else on an `else if`.
|
|
} else if (
|
|
isObject(packageSection.version) &&
|
|
cargoManifest.workspace?.package?.version
|
|
) {
|
|
// TODO: Support reading from parent workspace manifest?
|
|
version = cargoManifest.workspace.package.version;
|
|
}
|
|
}
|
|
|
|
const lockFileName = await findLocalSiblingOrParent(
|
|
packageFile,
|
|
'Cargo.lock',
|
|
);
|
|
const res: PackageFileContent = { deps, packageFileVersion: version };
|
|
if (lockFileName) {
|
|
logger.debug(
|
|
`Found lock file ${lockFileName} for packageFile: ${packageFile}`,
|
|
);
|
|
|
|
const versionsByPackage = await extractLockFileVersions(lockFileName);
|
|
if (!versionsByPackage) {
|
|
logger.debug(
|
|
`Could not extract lock file versions from ${lockFileName}.`,
|
|
);
|
|
return res;
|
|
}
|
|
|
|
res.lockFiles = [lockFileName];
|
|
|
|
for (const dep of deps) {
|
|
const packageName = dep.packageName ?? dep.depName!;
|
|
const versions = coerceArray(versionsByPackage.get(packageName));
|
|
|
|
let lockedVersion: string | null = null;
|
|
if (dep.currentValue) {
|
|
lockedVersion = versioning.getSatisfyingVersion(
|
|
versions,
|
|
dep.currentValue,
|
|
);
|
|
}
|
|
|
|
if (lockedVersion) {
|
|
dep.lockedVersion = lockedVersion;
|
|
} else {
|
|
logger.debug(
|
|
`No locked version found for package ${dep.depName} in the range of ${dep.currentValue}.`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|