import { isString } from '@sindresorhus/is'; import { z } from 'zod/v4'; import { logger } from '../../../logger/index.ts'; import { coerceArray } from '../../../util/array.ts'; import { getSiblingFileName, localPathExists } from '../../../util/fs/index.ts'; import { coerceNumber } from '../../../util/number.ts'; import { Result } from '../../../util/result.ts'; import { ensureTrailingSlash, isHttpUrl, joinUrlParts, } from '../../../util/url.ts'; import { defaultRegistryUrl as defaultCondaRegistryApi } from '../../datasource/conda/common.ts'; import type { RegistryStrategy } from '../../datasource/index.ts'; import type { PackageFileContent } from '../types.ts'; import { type Channels, type PixiConfig, PixiFile, type PixiPackageDependency, PixiPyProject, } from './schema.ts'; export function getUserPixiConfig( content: string, packageFile: string, ): null | PixiConfig { if ( packageFile === 'pyproject.toml' || packageFile.endsWith('/pyproject.toml') ) { const { val, err } = Result.parse(content, PixiPyProject).unwrap(); if (err) { logger.debug({ packageFile, err }, `error parsing ${packageFile}`); return null; } return val.tool?.pixi ?? null; } if (packageFile === 'pixi.toml' || packageFile.endsWith('/pixi.toml')) { const { val, err } = Result.parse(content, PixiFile).unwrap(); if (err) { logger.debug({ packageFile, err }, `error parsing ${packageFile}`); return null; } return val; } const { val, err } = Result.parse( content, z.union([PixiFile, PixiPyProject.transform((p) => p.tool?.pixi)]), ).unwrap(); if (err) { logger.debug({ packageFile, err }, `error parsing ${packageFile}`); return null; } return val ?? null; } export async function extractPackageFile( content: string, packageFile: string, ): Promise { logger.trace(`pixi.extractPackageFile(${packageFile})`); const val = getUserPixiConfig(content, packageFile); if (!val) { return null; } const lockfileName = getSiblingFileName(packageFile, 'pixi.lock'); const lockFiles: string[] = []; if (await localPathExists(lockfileName)) { lockFiles.push(lockfileName); } const project = val.project; const channelPriority = project['channel-priority']; let registryStrategy: RegistryStrategy | undefined; if (channelPriority === 'disabled') { registryStrategy = 'merge'; } // resolve channels and build registry urls for each channel with order const conda: PixiPackageDependency[] = []; for (const item of val.conda) { conda.push( addRegistryUrls({ ...item, channels: project.channels, registryStrategy, }), ); } for (const item of val.feature.conda) { conda.push( addRegistryUrls({ ...item, registryStrategy, channels: [...coerceArray(item.channels), ...project.channels], }), ); } return { lockFiles, deps: [conda, val.pypi, val.feature.pypi].flat(), }; } function addRegistryUrls(item: PixiPackageDependency): PixiPackageDependency { const channels = orderChannels(item.channels); if (item.channel) { return { ...item, channels, registryUrls: [channelToRegistryUrl(item.channel)], }; } if (channels.length === 0) { return { ...item, channels, skipStage: 'extract', skipReason: 'unknown-registry', }; } const registryUrls: string[] = []; for (const channel of channels) { registryUrls.push(channelToRegistryUrl(channel)); } return { ...item, channels, registryUrls, }; } function channelToRegistryUrl(channel: string): string { if (isHttpUrl(channel)) { return ensureTrailingSlash(channel); } return ensureTrailingSlash(joinUrlParts(defaultCondaRegistryApi, channel)); } function orderChannels(channels: Channels = []): string[] { return channels .map((channel, index) => { if (isString(channel)) { return { channel, priority: 0, index }; } return { ...channel, priority: coerceNumber(channel.priority), index }; }) .toSorted((a, b) => { // first based on priority then based on index if (a.priority !== b.priority) { return b.priority - a.priority; } return a.index - b.index; }) .map((c) => c.channel); }