33
44import * as fs from 'fs-extra' ;
55import * as path from 'path' ;
6+ import { clean as cleanPep440 , satisfies as satisfiesPep440 } from '@renovatebot/pep440' ;
67import { Disposable , Event , EventEmitter , l10n , LogOutputChannel , MarkdownString , ThemeIcon , Uri } from 'vscode' ;
78import {
89 CreateEnvironmentOptions ,
@@ -31,19 +32,17 @@ import {
3132 resolveCacheEntryPath ,
3233 writeMetaJson ,
3334} from '../../../common/inlineScript/cacheLayout' ;
34- import { pickCompatibleInterpreter } from '../../../common/inlineScript/interpreter' ;
35- import {
36- InlineScriptMetadata ,
37- matchesPythonVersion ,
38- readInlineScriptMetadataFromFile ,
39- } from '../../../common/inlineScript/metadata' ;
35+ import { extractLowerBoundVersion , pickCompatibleInterpreter } from '../../../common/inlineScript/interpreter' ;
36+ import { InlineScriptMetadata , readInlineScriptMetadataFromFile } from '../../../common/inlineScript/metadata' ;
4037import { CONDA_MANAGER_ID , PYENV_MANAGER_ID , SYSTEM_MANAGER_ID } from '../../../common/constants' ;
4138import { acquireFileLock , AcquiredFileLock } from '../../../common/lockfile.apis' ;
4239import { isFileNotFoundError } from '../../../common/utils/filesystem' ;
4340import { normalizePath } from '../../../common/utils/pathUtils' ;
4441import { compareReleaseSegments , parseReleaseSegments } from '../../../common/utils/pep440Release' ;
4542import { getVenvPythonPath } from '../../../common/utils/virtualEnvironment' ;
4643import { NativePythonFinder } from '../../common/nativePythonFinder' ;
44+ import { resolveSystemPythonEnvironmentPath } from '../utils' ;
45+ import * as uvPythonInstaller from '../uvPythonInstaller' ;
4746import { createWithProgress , resolveVenvPythonEnvironmentPath } from '../venvUtils' ;
4847
4948const BASE_INTERPRETER_MANAGER_IDS = new Set ( [
@@ -79,6 +78,8 @@ type CacheEntryInspection =
7978/** Manages extension-owned PEP 723 script environments. */
8079export class InlineScriptEnvManager implements EnvironmentManager , Disposable {
8180 private readonly pendingCreations = new Map < string , Promise < PythonEnvironment | undefined > > ( ) ;
81+ private readonly directlyResolvedBaseInterpreters = new Map < string , PythonEnvironment > ( ) ;
82+ private baseInterpreterInstallationQueue : Promise < void > = Promise . resolve ( ) ;
8283
8384 private readonly _onDidChangeEnvironments = new EventEmitter < DidChangeEnvironmentsEventArgs > ( ) ;
8485 public readonly onDidChangeEnvironments : Event < DidChangeEnvironmentsEventArgs > =
@@ -131,9 +132,12 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
131132 return undefined ;
132133 }
133134
134- const selectedBase = await this . selectBaseInterpreter ( metadata ) ;
135+ let selectedBase = await this . selectBaseInterpreter ( metadata ) ;
136+ if ( ! selectedBase && options ?. quickCreate !== true ) {
137+ selectedBase = await this . installAndSelectBaseInterpreter ( metadata ) ;
138+ }
135139 if ( ! selectedBase ) {
136- this . log . warn ( `No installed Python satisfies the inline-script requirements for ${ scriptUri . fsPath } .` ) ;
140+ this . log . warn ( `No compatible Python is available for inline-script environment creation: ${ scriptUri . fsPath } .` ) ;
137141 return undefined ;
138142 }
139143
@@ -192,12 +196,24 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
192196 }
193197
194198 private async selectBaseInterpreter ( metadata : InlineScriptMetadata ) : Promise < SelectedBaseInterpreter | undefined > {
195- const globalEnvironments = await this . api . getEnvironments ( 'global' ) ;
196- const reported = globalEnvironments . filter (
197- ( environment ) =>
198- BASE_INTERPRETER_MANAGER_IDS . has ( environment . envId . managerId ) &&
199- ( environment . envId . managerId !== CONDA_MANAGER_ID || environment . name === 'base' ) ,
200- ) ;
199+ let globalEnvironments : readonly PythonEnvironment [ ] = [ ] ;
200+ try {
201+ globalEnvironments = await this . api . getEnvironments ( 'global' ) ;
202+ } catch ( error ) {
203+ this . log . warn ( `Unable to query discovered base interpreters: ${ getErrorMessage ( error ) } ` ) ;
204+ }
205+ const reported = [
206+ ...globalEnvironments . filter (
207+ ( environment ) =>
208+ BASE_INTERPRETER_MANAGER_IDS . has ( environment . envId . managerId ) &&
209+ ( environment . envId . managerId !== CONDA_MANAGER_ID || environment . name === 'base' ) ,
210+ ) ,
211+ ...[ ...this . directlyResolvedBaseInterpreters . values ( ) ] . filter (
212+ ( environment ) =>
213+ ! metadata . requiresPython ||
214+ this . matchesInstallConstraint ( metadata . requiresPython , environment . version ) ,
215+ ) ,
216+ ] ;
201217 const derivedChecks = await Promise . all (
202218 reported . map ( async ( environment ) => {
203219 if ( ! path . isAbsolute ( environment . sysPrefix ) ) {
@@ -214,10 +230,15 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
214230 ) ;
215231 let candidates = derivedChecks
216232 . filter ( ( candidate ) => ! candidate . derived )
217- . map ( ( candidate ) => candidate . environment ) ;
233+ . map ( ( candidate ) => candidate . environment )
234+ . filter (
235+ ( candidate ) =>
236+ ! metadata . requiresPython ||
237+ this . matchesInstallConstraint ( metadata . requiresPython , candidate . version ) ,
238+ ) ;
218239
219240 while ( candidates . length > 0 ) {
220- const environment = pickCompatibleInterpreter ( candidates , metadata . requiresPython ) ;
241+ const environment = pickCompatibleInterpreter ( candidates , undefined ) ;
221242 if ( ! environment ) {
222243 return undefined ;
223244 }
@@ -239,6 +260,191 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
239260 return undefined ;
240261 }
241262
263+ private async installAndSelectBaseInterpreter (
264+ metadata : InlineScriptMetadata ,
265+ ) : Promise < SelectedBaseInterpreter | undefined > {
266+ const run = this . baseInterpreterInstallationQueue . then ( ( ) =>
267+ this . installAndSelectBaseInterpreterSerially ( metadata ) ,
268+ ) ;
269+ this . baseInterpreterInstallationQueue = run . then (
270+ ( ) => undefined ,
271+ ( ) => undefined ,
272+ ) ;
273+ return run ;
274+ }
275+
276+ private async installAndSelectBaseInterpreterSerially (
277+ metadata : InlineScriptMetadata ,
278+ ) : Promise < SelectedBaseInterpreter | undefined > {
279+ const existing = await this . selectBaseInterpreter ( metadata ) ;
280+ if ( existing ) {
281+ return existing ;
282+ }
283+
284+ const requiresPython = metadata . requiresPython ?. trim ( ) || undefined ;
285+ const lowerBound = extractLowerBoundVersion ( requiresPython ) ;
286+ const version = await this . selectInstallablePythonVersion ( requiresPython , lowerBound ) ;
287+ if ( requiresPython && ! version ) {
288+ this . log . warn (
289+ 'Cannot install a Python for this inline script because no compatible install version could be selected.' ,
290+ ) ;
291+ return undefined ;
292+ }
293+
294+ const installedPath = await this . installPythonAndRefresh ( requiresPython , version ) ;
295+ if ( ! installedPath ) {
296+ return undefined ;
297+ }
298+
299+ let selected : SelectedBaseInterpreter | undefined ;
300+ try {
301+ selected = await this . selectBaseInterpreter ( metadata ) ;
302+ } catch ( error ) {
303+ this . log . warn (
304+ `Unable to refresh base-interpreter discovery after installing Python: ${ getErrorMessage ( error ) } ` ,
305+ ) ;
306+ }
307+ if ( ! selected ) {
308+ const resolved = await resolveSystemPythonEnvironmentPath (
309+ installedPath ,
310+ this . nativeFinder ,
311+ this . api ,
312+ this . baseManager ,
313+ ) ;
314+ const executable = resolved ?. execInfo ?. run . executable ;
315+ if ( resolved && executable && pickCompatibleInterpreter ( [ resolved ] , metadata . requiresPython ) ) {
316+ try {
317+ const canonicalPath = await fs . realpath ( executable ) ;
318+ if ( ! requiresPython || this . matchesInstallConstraint ( requiresPython , resolved . version ) ) {
319+ this . directlyResolvedBaseInterpreters . set ( canonicalPath , resolved ) ;
320+ selected = {
321+ environment : resolved ,
322+ canonicalPath,
323+ } ;
324+ }
325+ } catch ( error ) {
326+ this . log . warn (
327+ `Unable to resolve the Python installed for an inline script at ${ executable } : ${ getErrorMessage ( error ) } ` ,
328+ ) ;
329+ }
330+ }
331+ }
332+ if ( ! selected ) {
333+ this . log . warn (
334+ 'Python was installed for an inline script, but no compatible base interpreter was discovered after refreshing environments.' ,
335+ ) ;
336+ }
337+ return selected ;
338+ }
339+
340+ private async selectInstallablePythonVersion (
341+ requiresPython : string | undefined ,
342+ lowerBound : string | undefined ,
343+ ) : Promise < string | undefined > {
344+ if ( ! requiresPython ) {
345+ return lowerBound ;
346+ }
347+ const prereleaseLowerBound = this . extractPrereleaseLowerBound ( requiresPython ) ;
348+ if ( prereleaseLowerBound ) {
349+ return prereleaseLowerBound ;
350+ }
351+ const lowerBoundRelease = lowerBound ? parseReleaseSegments ( lowerBound ) : undefined ;
352+ if ( lowerBound && lowerBoundRelease ?. [ 0 ] === 3 ) {
353+ if ( / ^ > = \s * [ ^ , ] + $ / . test ( requiresPython ) && this . matchesInstallConstraint ( requiresPython , lowerBound ) ) {
354+ return lowerBound ;
355+ }
356+ if ( / ^ = = \s * [ ^ , * ] + $ / . test ( requiresPython ) && this . matchesInstallConstraint ( requiresPython , lowerBound ) ) {
357+ return lowerBound ;
358+ }
359+ }
360+
361+ let available : uvPythonInstaller . UvPythonVersion [ ] ;
362+ try {
363+ if ( ! ( await uvPythonInstaller . ensureUvForInlineScriptVersionLookup ( requiresPython , this . log ) ) ) {
364+ return undefined ;
365+ }
366+ available = await uvPythonInstaller . getAvailablePythonVersions ( ) ;
367+ } catch ( error ) {
368+ this . log . warn ( `Unable to query Python versions available from uv: ${ getErrorMessage ( error ) } ` ) ;
369+ return undefined ;
370+ }
371+ return available
372+ . filter (
373+ ( candidate ) =>
374+ candidate . implementation === 'cpython' &&
375+ candidate . variant === 'default' &&
376+ candidate . version_parts . major === 3 &&
377+ this . matchesInstallConstraint ( requiresPython , candidate . version ) ,
378+ )
379+ . sort ( ( left , right ) => {
380+ const leftRelease = parseReleaseSegments ( left . version ) ;
381+ const rightRelease = parseReleaseSegments ( right . version ) ;
382+ if ( ! leftRelease || ! rightRelease ) {
383+ return 0 ;
384+ }
385+ return compareReleaseSegments ( rightRelease , leftRelease ) ;
386+ } ) [ 0 ] ?. version ;
387+ }
388+
389+ private matchesInstallConstraint ( requiresPython : string , version : string ) : boolean {
390+ try {
391+ return satisfiesPep440 ( version , requiresPython , {
392+ prereleases : / (?: (?: a | a l p h a | b | b e t a | c | r c | p r e | p r e v i e w ) [ . _ - ] ? \d + | d e v [ . _ - ] ? \d + ) / i. test (
393+ requiresPython ,
394+ ) ,
395+ } ) ;
396+ } catch ( error ) {
397+ this . log . warn ( `Unable to evaluate requires-python '${ requiresPython } ': ${ getErrorMessage ( error ) } ` ) ;
398+ return false ;
399+ }
400+ }
401+
402+ private extractPrereleaseLowerBound ( requiresPython : string ) : string | undefined {
403+ return requiresPython
404+ . split ( ',' )
405+ . map ( ( clause ) =>
406+ clause
407+ . trim ( )
408+ . match (
409+ / ^ (?: > = | = = | ~ = ) \s * ( \d + (?: \. \d + ) * (?: (?: a | a l p h a | b | b e t a | c | r c | p r e | p r e v i e w ) [ . _ - ] ? \d + | [ . _ - ] ? d e v [ . _ - ] ? \d + ) ) $ / i,
410+ ) ?. [ 1 ] ,
411+ )
412+ . map ( ( version ) => ( version ? cleanPep440 ( version ) : undefined ) )
413+ . filter ( ( version ) : version is string => ! ! version )
414+ . find ( ( version ) => this . matchesInstallConstraint ( requiresPython , version ) ) ;
415+ }
416+
417+ private async installPythonAndRefresh (
418+ requiresPython : string | undefined ,
419+ version : string | undefined ,
420+ ) : Promise < string | undefined > {
421+ let installedPath : string | undefined ;
422+ try {
423+ installedPath = await uvPythonInstaller . promptInstallPythonViaUv ( 'inlineScript' , this . log , {
424+ requiresPython,
425+ version,
426+ } ) ;
427+ if ( ! installedPath ) {
428+ this . log . warn (
429+ 'Python installation for inline-script environment creation was declined or did not complete.' ,
430+ ) ;
431+ return undefined ;
432+ }
433+ } catch ( error ) {
434+ this . log . error ( `Failed to install Python for an inline script: ${ getErrorMessage ( error ) } ` ) ;
435+ return undefined ;
436+ }
437+
438+ try {
439+ await this . api . refreshEnvironments ( undefined ) ;
440+ } catch ( error ) {
441+ this . log . warn (
442+ `Python was installed for an inline script, but environment discovery could not be refreshed: ${ getErrorMessage ( error ) } ` ,
443+ ) ;
444+ }
445+ return installedPath ;
446+ }
447+
242448 private async createOrReuseEnvironment ( {
243449 cacheKey,
244450 packages,
@@ -363,7 +569,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
363569 return { kind : 'stale' } ;
364570 }
365571 const requiresPython = metadata . requiresPython ?. trim ( ) ;
366- if ( requiresPython && ! matchesPythonVersion ( requiresPython , environment . version ) ) {
572+ if ( requiresPython && ! this . matchesInstallConstraint ( requiresPython , environment . version ) ) {
367573 return { kind : 'stale' } ;
368574 }
369575
0 commit comments