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 } from './types.ts'; const allToolConfig: Record = { bazelisk: { datasource: 'github-releases', packageName: 'bazelbuild/bazelisk', versioning: semverVersioningId, }, bun: { datasource: 'github-releases', packageName: 'oven-sh/bun', extractVersion: '^bun-v(?.*)$', 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(?.*)$', 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(?.*)$', }, 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.js') > | null = null; async function getPkgReleases( toolConfig: ToolConfig, ): Promise { _getPkgReleases ??= import('../../modules/datasource/index.js'); const { getPkgReleases } = await _getPkgReleases; return getPkgReleases(toolConfig); } export function supportsDynamicInstall(toolName: string): boolean { return !!allToolConfig[toolName]; } export function isContainerbase(): boolean { return !!getEnv().CONTAINERBASE; } export function isDynamicInstall( toolConstraints?: Opt, ): 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 { 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, ): Promise { 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; }