Files
renovate/lib/util/exec/containerbase.ts
T
Jamie Tanna 8278ce5370 docs(constraints): document the possible constraints and installTools (#42837)
* docs(constraints): list the possible `constraints` and `installTools`

Now we have a stronger type for what a valid tool constraint is, and
what additional constraints are, we should be very explicit with what
each option is, and specify them in the `constraints` as well as our
`installTools` docs.

* test: add coverage

Co-authored-by: Claude Sonnet 4.6 <jamie.tanna+claude-code@mend.io>

---------

Co-authored-by: Claude Sonnet 4.6 <jamie.tanna+claude-code@mend.io>
2026-04-26 16:13:09 +00:00

390 lines
11 KiB
TypeScript

import { isString } from '@sindresorhus/is';
import { quote } from 'shlex';
import { GlobalConfig } from '../../config/global.ts';
import { logger } from '../../logger/index.ts';
import type { ReleaseResult } from '../../modules/datasource/index.ts';
import { id as composerVersioningId } from '../../modules/versioning/composer/index.ts';
import { id as condaVersioningId } from '../../modules/versioning/conda/index.ts';
import { id as gradleVersioningId } from '../../modules/versioning/gradle/index.ts';
import * as allVersioning from '../../modules/versioning/index.ts';
import { id as mavenVersioningId } from '../../modules/versioning/maven/index.ts';
import { id as nodeVersioningId } from '../../modules/versioning/node/index.ts';
import { id as npmVersioningId } from '../../modules/versioning/npm/index.ts';
import { id as pep440VersioningId } from '../../modules/versioning/pep440/index.ts';
import { id as pythonVersioningId } from '../../modules/versioning/python/index.ts';
import { id as rubyVersioningId } from '../../modules/versioning/ruby/index.ts';
import { id as semverVersioningId } from '../../modules/versioning/semver/index.ts';
import { id as semverCoercedVersioningId } from '../../modules/versioning/semver-coerced/index.ts';
import { getEnv } from '../env.ts';
import type { Opt, ToolConfig, ToolConstraint, ToolName } from './types.ts';
const allToolConfig: Record<ToolName, ToolConfig> = {
bazelisk: {
datasource: 'github-releases',
packageName: 'bazelbuild/bazelisk',
versioning: semverVersioningId,
},
bun: {
datasource: 'github-releases',
packageName: 'oven-sh/bun',
extractVersion: '^bun-v(?<version>.*)$',
versioning: npmVersioningId,
},
bundler: {
datasource: 'rubygems',
packageName: 'bundler',
versioning: rubyVersioningId,
},
cocoapods: {
datasource: 'rubygems',
packageName: 'cocoapods',
versioning: rubyVersioningId,
},
composer: {
datasource: 'github-releases',
packageName: 'containerbase/composer-prebuild',
versioning: composerVersioningId,
},
conan: {
datasource: 'pypi',
packageName: 'conan',
versioning: pep440VersioningId,
},
copier: {
datasource: 'pypi',
packageName: 'copier',
versioning: pep440VersioningId,
},
corepack: {
datasource: 'npm',
packageName: 'corepack',
versioning: npmVersioningId,
},
devbox: {
datasource: 'github-releases',
packageName: 'jetify-com/devbox',
versioning: semverVersioningId,
},
dotnet: {
datasource: 'dotnet-version',
packageName: 'dotnet-sdk',
versioning: semverVersioningId,
},
erlang: {
datasource: 'github-releases',
packageName: 'containerbase/erlang-prebuild',
versioning: semverCoercedVersioningId,
},
elixir: {
datasource: 'github-releases',
packageName: 'elixir-lang/elixir',
versioning: semverVersioningId,
},
flux: {
datasource: 'github-releases',
packageName: 'fluxcd/flux2',
versioning: semverVersioningId,
},
gleam: {
datasource: 'github-releases',
packageName: 'gleam-lang/gleam',
versioning: semverVersioningId,
},
golang: {
datasource: 'github-releases',
packageName: 'containerbase/golang-prebuild',
versioning: npmVersioningId,
},
gradle: {
datasource: 'gradle-version',
packageName: 'gradle',
versioning: gradleVersioningId,
},
hashin: {
datasource: 'pypi',
packageName: 'hashin',
versioning: pep440VersioningId,
},
helm: {
datasource: 'github-releases',
packageName: 'helm/helm',
versioning: semverVersioningId,
},
helmfile: {
datasource: 'github-releases',
packageName: 'helmfile/helmfile',
versioning: semverVersioningId,
},
java: {
datasource: 'java-version',
packageName: 'java?system=true',
versioning: npmVersioningId,
},
/* not used in Renovate */
'java-maven': {
datasource: 'java-version',
packageName: 'java?system=true',
versioning: mavenVersioningId,
},
jb: {
datasource: 'github-releases',
packageName: 'jsonnet-bundler/jsonnet-bundler',
versioning: semverVersioningId,
},
kustomize: {
datasource: 'github-releases',
packageName: 'kubernetes-sigs/kustomize',
extractVersion: '^kustomize/v(?<version>.*)$',
versioning: semverVersioningId,
},
maven: {
datasource: 'github-releases',
packageName: 'containerbase/maven-prebuild',
versioning: mavenVersioningId,
},
nix: {
datasource: 'github-releases',
packageName: 'containerbase/nix-prebuild',
versioning: semverVersioningId,
},
node: {
datasource: 'github-releases',
packageName: 'containerbase/node-prebuild',
versioning: nodeVersioningId,
},
npm: {
datasource: 'npm',
packageName: 'npm',
versioning: npmVersioningId,
},
pdm: {
datasource: 'github-releases',
packageName: 'pdm-project/pdm',
versioning: semverVersioningId,
},
php: {
datasource: 'github-releases',
packageName: 'containerbase/php-prebuild',
versioning: composerVersioningId,
},
'pip-tools': {
datasource: 'pypi',
packageName: 'pip-tools',
versioning: pep440VersioningId,
},
pipenv: {
datasource: 'pypi',
packageName: 'pipenv',
versioning: pep440VersioningId,
},
pnpm: {
datasource: 'npm',
packageName: 'pnpm',
versioning: npmVersioningId,
},
pixi: {
datasource: 'github-releases',
packageName: 'prefix-dev/pixi',
versioning: condaVersioningId,
extractVersion: '^v(?<version>.*)$',
},
poetry: {
datasource: 'pypi',
packageName: 'poetry',
versioning: pep440VersioningId,
},
python: {
datasource: 'github-releases',
packageName: 'containerbase/python-prebuild',
versioning: pythonVersioningId,
},
ruby: {
datasource: 'github-releases',
packageName: 'containerbase/ruby-prebuild',
versioning: rubyVersioningId,
},
rust: {
datasource: 'docker',
packageName: 'rust',
versioning: semverVersioningId,
},
uv: {
datasource: 'pypi',
packageName: 'uv',
versioning: pep440VersioningId,
},
yarn: {
datasource: 'npm',
packageName: 'yarn',
versioning: npmVersioningId,
},
'yarn-slim': {
datasource: 'npm',
packageName: 'yarn',
versioning: npmVersioningId,
},
dart: {
datasource: 'dart-version',
packageName: 'dart',
versioning: npmVersioningId,
},
flutter: {
datasource: 'github-releases',
packageName: 'containerbase/flutter-prebuild',
versioning: npmVersioningId,
},
vendir: {
datasource: 'github-releases',
packageName: 'carvel-dev/vendir',
versioning: semverVersioningId,
},
};
let _getPkgReleases: Promise<
typeof import('../../modules/datasource/index.ts')
> | null = null;
async function getPkgReleases(
toolConfig: ToolConfig,
): Promise<ReleaseResult | null> {
_getPkgReleases ??= import('../../modules/datasource/index.ts');
const { getPkgReleases } = await _getPkgReleases;
return getPkgReleases(toolConfig);
}
export function getToolConfig(toolName: ToolName): ToolConfig | undefined {
return allToolConfig[toolName];
}
export function supportsDynamicInstall(toolName: ToolName): boolean {
return !!allToolConfig[toolName];
}
export function isContainerbase(): boolean {
return !!getEnv().CONTAINERBASE;
}
export function isDynamicInstall(
toolConstraints?: Opt<ToolConstraint[]>,
): boolean {
if (GlobalConfig.get('binarySource') !== 'install') {
return false;
}
if (!isContainerbase()) {
logger.debug('Falling back to binarySource=global');
return false;
}
return (
!toolConstraints ||
toolConstraints.every((toolConstraint) =>
supportsDynamicInstall(toolConstraint.toolName),
)
);
}
function isStable(
version: string,
versioningApi: allVersioning.VersioningApi,
latest?: string,
): boolean {
if (!versioningApi.isStable(version)) {
return false;
}
if (isString(latest)) {
if (versioningApi.isGreaterThan(version, latest)) {
return false;
}
}
return true;
}
export async function resolveConstraint(
toolConstraint: ToolConstraint,
): Promise<string> {
const { toolName } = toolConstraint;
const toolConfig = allToolConfig[toolName];
if (!toolConfig) {
throw new Error(`Invalid tool to install: ${toolName}`);
}
const versioning = allVersioning.get(toolConfig.versioning);
let constraint = toolConstraint.constraint;
if (constraint) {
if (versioning.isValid(constraint)) {
if (versioning.isSingleVersion(constraint)) {
return constraint.replace(/^=+/, '').trim();
}
} else {
logger.warn(
{ toolName, constraint, versioning: toolConfig.versioning },
'Invalid tool constraint',
);
constraint = undefined;
}
}
const pkgReleases = await getPkgReleases(toolConfig);
const releases = pkgReleases?.releases ?? [];
if (!releases?.length) {
logger.warn({ toolConfig }, 'No tool releases found.');
throw new Error('No tool releases found.');
}
const matchingReleases = releases.filter(
(r) => !constraint || versioning.matches(r.version, constraint),
);
const stableMatchingVersion = matchingReleases
.filter((r) => isStable(r.version, versioning, pkgReleases?.tags?.latest))
.pop()?.version;
if (stableMatchingVersion) {
logger.debug(
{ toolName, constraint, resolvedVersion: stableMatchingVersion },
'Resolved stable matching version',
);
return stableMatchingVersion;
}
const unstableMatchingVersion = matchingReleases.pop()?.version;
if (unstableMatchingVersion) {
logger.debug(
{ toolName, constraint, resolvedVersion: unstableMatchingVersion },
'Resolved unstable matching version',
);
return unstableMatchingVersion;
}
const stableVersion = releases
.filter((r) => isStable(r.version, versioning, pkgReleases?.tags?.latest))
.pop()?.version;
if (stableVersion) {
logger.warn(
{ toolName, constraint, stableVersion },
'No matching tool versions found for constraint - using latest stable version',
);
}
const highestVersion = releases.pop()!.version;
logger.warn(
{ toolName, constraint, highestVersion },
'No matching or stable tool versions found - using an unstable version',
);
return highestVersion;
}
export async function generateInstallCommands(
toolConstraints: Opt<ToolConstraint[]>,
): Promise<string[]> {
const installCommands: string[] = [];
if (toolConstraints?.length) {
for (const toolConstraint of toolConstraints) {
const toolVersion = await resolveConstraint(toolConstraint);
const { toolName } = toolConstraint;
const installCommand = `install-tool ${toolName} ${quote(toolVersion)}`;
installCommands.push(installCommand);
}
}
return installCommands;
}