1+ #!/usr/bin/env python
2+
3+ # -------------------------------------------------------------------------
4+ # Copyright (c) Microsoft Corporation. All rights reserved.
5+ # Licensed under the MIT License. See License.txt in the project root for
6+ # license information.
7+ # --------------------------------------------------------------------------
8+ """Package manager utilities for detecting and using pip or uv."""
9+
10+ import subprocess
11+ import sys
12+ import venv
13+ from pathlib import Path
14+ from venvtools import ExtendedEnvBuilder
15+
16+
17+ class PackageManagerNotFoundError (Exception ):
18+ """Raised when no suitable package manager is found."""
19+ pass
20+
21+
22+ def _check_command_available (command : str ) -> bool :
23+ """Check if a command is available in the environment."""
24+ try :
25+ subprocess .run ([command , "--version" ], capture_output = True , check = True )
26+ return True
27+ except (subprocess .CalledProcessError , FileNotFoundError ):
28+ return False
29+
30+
31+ def detect_package_manager () -> str :
32+ """Detect the best available package manager.
33+
34+ Returns:
35+ str: The package manager command ('uv' or 'pip')
36+
37+ Raises:
38+ PackageManagerNotFoundError: If no suitable package manager is found
39+ """
40+ # Check for uv first since it's more modern and faster
41+ if _check_command_available ("uv" ):
42+ return "uv"
43+
44+ # Fall back to pip
45+ if _check_command_available ("pip" ):
46+ return "pip"
47+
48+ # As a last resort, try using python -m pip
49+ try :
50+ subprocess .run ([sys .executable , "-m" , "pip" , "--version" ],
51+ capture_output = True , check = True )
52+ return "python -m pip"
53+ except (subprocess .CalledProcessError , FileNotFoundError ):
54+ pass
55+
56+ raise PackageManagerNotFoundError (
57+ "No suitable package manager found. Please install either uv or pip."
58+ )
59+
60+
61+ def get_install_command (package_manager : str , venv_context = None ) -> list :
62+ """Get the install command for the given package manager.
63+
64+ Args:
65+ package_manager: The package manager command ('uv', 'pip', or 'python -m pip')
66+ venv_context: The virtual environment context (optional, used for pip)
67+
68+ Returns:
69+ list: The base install command as a list
70+ """
71+ if package_manager == "uv" :
72+ cmd = ["uv" , "pip" , "install" ]
73+ if venv_context :
74+ cmd .extend (["--python" , venv_context .env_exe ])
75+ return cmd
76+ elif package_manager == "pip" :
77+ if venv_context :
78+ return [venv_context .env_exe , "-m" , "pip" , "install" ]
79+ else :
80+ return ["pip" , "install" ]
81+ elif package_manager == "python -m pip" :
82+ if venv_context :
83+ return [venv_context .env_exe , "-m" , "pip" , "install" ]
84+ else :
85+ return [sys .executable , "-m" , "pip" , "install" ]
86+ else :
87+ raise ValueError (f"Unknown package manager: { package_manager } " )
88+
89+
90+ def install_packages (packages : list , venv_context = None , package_manager : str = None ) -> None :
91+ """Install packages using the available package manager.
92+
93+ Args:
94+ packages: List of packages to install
95+ venv_context: Virtual environment context (optional)
96+ package_manager: Package manager to use (auto-detected if None)
97+ """
98+ if package_manager is None :
99+ package_manager = detect_package_manager ()
100+
101+ install_cmd = get_install_command (package_manager , venv_context )
102+
103+ try :
104+ subprocess .check_call (install_cmd + packages )
105+ except subprocess .CalledProcessError as e :
106+ raise RuntimeError (f"Failed to install packages with { package_manager } : { e } " )
107+
108+
109+ def create_venv_with_package_manager (venv_path ):
110+ """Create virtual environment using the best available package manager.
111+
112+ Args:
113+ venv_path: Path where to create the virtual environment
114+
115+ Returns:
116+ venv_context: Virtual environment context object
117+ """
118+ package_manager = detect_package_manager ()
119+
120+ if package_manager == "uv" :
121+ # Use uv to create and manage the virtual environment
122+ if not venv_path .exists ():
123+ subprocess .check_call (["uv" , "venv" , str (venv_path )])
124+
125+ # Create a mock venv_context for compatibility
126+ class MockVenvContext :
127+ def __init__ (self , venv_path ):
128+ self .env_exe = str (venv_path / "bin" / "python" ) if sys .platform != "win32" else str (venv_path / "Scripts" / "python.exe" )
129+
130+ return MockVenvContext (venv_path )
131+ else :
132+ # Use standard venv for pip
133+ if venv_path .exists ():
134+ env_builder = venv .EnvBuilder (with_pip = True )
135+ return env_builder .ensure_directories (venv_path )
136+ else :
137+ env_builder = ExtendedEnvBuilder (with_pip = True , upgrade_deps = True )
138+ env_builder .create (venv_path )
139+ return env_builder .context
0 commit comments