2929from .logger import logger_init
3030
3131LOG_PATH = os .path .dirname (os .path .abspath (os .path .join (__file__ , os .pardir )))
32+ DEFAULT_VENV_DIR = os .path .join (LOG_PATH , '.venv' )
3233
3334logger = logger_init (filepath = LOG_PATH ).getlogger ()
3435
3536
37+ def use_venv ():
38+ """
39+ Return True when the isolated virtual-environment path is enabled.
40+ Controlled by AVOCADO_USE_VENV=1 (or 'true'/'yes'/'on').
41+ """
42+ val = os .environ .get ("AVOCADO_USE_VENV" , "" ).lower ()
43+ return val in ("1" , "true" , "yes" , "on" )
44+
45+
46+ def get_venv_dir ():
47+ """Return the path of the optional Avocado virtual environment."""
48+ return os .environ .get ("AVOCADO_VENV" , DEFAULT_VENV_DIR )
49+
50+
51+ def prepend_venv_to_path ():
52+ """Prepend the venv bin directory to PATH when venv mode is enabled."""
53+ if use_venv ():
54+ venv_bin = os .path .join (get_venv_dir (), 'bin' )
55+ os .environ ['PATH' ] = venv_bin + os .pathsep + os .environ .get ('PATH' , '' )
56+ logger .debug ("Prepended %s to PATH (venv mode)" , venv_bin )
57+
58+
3659def runcmd (cmd , ignore_status = False , err_str = "" , info_str = "" , debug_str = "" ):
3760 """
3861 Running command and get the results
@@ -136,6 +159,15 @@ def get_avocado_bin(ignore_status=False):
136159 """
137160 Get the avocado executable path
138161 """
162+ if use_venv ():
163+ venv_avocado = os .path .join (get_venv_dir (), 'bin' , 'avocado' )
164+ if os .path .isfile (venv_avocado ) and os .access (venv_avocado , os .X_OK ):
165+ return venv_avocado
166+ if not ignore_status :
167+ logger .error ("avocado command not installed or not found in venv at %s" ,
168+ get_venv_dir ())
169+ sys .exit (1 )
170+ return ""
139171 return runcmd ('which avocado' , ignore_status = ignore_status ,
140172 err_str = "avocado command not installed or not found in path" )[1 ]
141173
@@ -218,14 +250,19 @@ def __init__(self, base_fw=[], opt_fw=[], kvm_fw=[], pip_packages=[], enable_kvm
218250 if sys .version_info [:2 ] < (3 , 6 ):
219251 logger .error ("System installed python version(%s) not supported, make sure python3.6 or above is installed to proceed" % sys .version_info [:2 ])
220252 sys .exit (1 )
221- self .pip_cmd = "pip%s" % sys .version_info [0 ]
222- # Check for pip if not attempt install and proceed
223- cmd = "%s --help >/dev/null 2>&1||(curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && python%s ./get-pip.py)" % (self .pip_cmd , sys .version_info [0 ])
224- runcmd (cmd , err_str = 'Unable to install pip3' )
253+ self .use_venv = use_venv ()
254+ self .venv_dir = get_venv_dir ()
225255
226- # Get pip version
227- pip_version_split = importlib .metadata .version (f"pip" ).split ("." )
228- self .pip_vmajor , self .pip_vminor = int (pip_version_split [0 ]), int (pip_version_split [1 ])
256+ if self .use_venv :
257+ self .python = os .path .join (self .venv_dir , 'bin' , 'python' )
258+ self .pip_cmd = os .path .join (self .venv_dir , 'bin' , 'pip' )
259+ logger .info ("Using isolated virtual environment: %s" , self .venv_dir )
260+ else :
261+ self .pip_cmd = "pip%s" % sys .version_info [0 ]
262+ cmd = "%s --help >/dev/null 2>&1||(curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && python%s ./get-pip.py)" % (self .pip_cmd , sys .version_info [0 ])
263+ runcmd (cmd , err_str = 'Unable to install pip3' )
264+ pip_version_split = importlib .metadata .version ("pip" ).split ("." )
265+ self .pip_vmajor , self .pip_vminor = int (pip_version_split [0 ]), int (pip_version_split [1 ])
229266
230267 self .uninstallitems = base_fw + opt_fw + kvm_fw + pip_packages
231268 if enable_kvm :
@@ -246,7 +283,33 @@ def __init__(self, base_fw=[], opt_fw=[], kvm_fw=[], pip_packages=[], enable_kvm
246283 else :
247284 self .install_packages .append (item [0 ])
248285
286+ def _ensure_venv (self ):
287+ ready_marker = os .path .join (self .venv_dir , '.bootstrap_ok' )
288+ if (os .path .isdir (self .venv_dir ) and os .path .isfile (self .python )
289+ and os .path .isfile (ready_marker )):
290+ logger .debug ("Reusing existing virtual environment at %s" , self .venv_dir )
291+ return
292+ if os .path .isdir (self .venv_dir ):
293+ logger .info ("Removing incomplete virtual environment at %s" , self .venv_dir )
294+ shutil .rmtree (self .venv_dir , ignore_errors = True )
295+ logger .info ("Creating isolated virtual environment at %s" , self .venv_dir )
296+ runcmd ('%s -m venv %s' % (sys .executable , self .venv_dir ),
297+ err_str = 'Failed to create virtual environment' )
298+ runcmd ('%s -m pip install --upgrade pip "setuptools<82" wheel' % self .python ,
299+ err_str = 'Failed to upgrade pip inside venv' )
300+ with open (ready_marker , 'w' ) as marker_file :
301+ marker_file .write ('ok\n ' )
302+
249303 def install (self ):
304+ if self .use_venv :
305+ self ._ensure_venv ()
306+ pip_installcmd = '%s install -U' % self .pip_cmd
307+ for package in self .install_packages :
308+ cmd = '%s %s' % (pip_installcmd , package )
309+ runcmd (cmd ,
310+ err_str = 'Package installation via pip failed: package %s' % package ,
311+ debug_str = 'Installing python package %s using pip' % package )
312+ return
250313 if os .geteuid () != 0 :
251314 pip_installcmd = '%s install --user -U' % self .pip_cmd
252315 else :
@@ -260,14 +323,42 @@ def install(self):
260323 debug_str = 'Installing python package %s using pip' % package )
261324
262325 def uninstall (self ):
326+ if self .use_venv :
327+ return
328+ self ._pip_uninstall (self .pip_cmd , self .pip_vmajor , self .pip_vminor )
329+
330+ def uninstall_system_wide (self ):
331+ venv_bin = os .path .join (self .venv_dir , 'bin' )
332+ path_entries = os .environ .get ('PATH' , '' ).split (os .pathsep )
333+ system_path = os .pathsep .join (p for p in path_entries if p and p != venv_bin )
334+ pip_cmd = shutil .which ('pip%s' % sys .version_info [0 ], path = system_path )
335+ if not pip_cmd :
336+ logger .debug ("System pip not found, skipping system-wide Avocado cleanup" )
337+ return
338+ status , output = subprocess .getstatusoutput ('%s --version' % pip_cmd )
339+ match = re .search (r'pip (\d+)\.(\d+)' , output )
340+ if status != 0 or not match :
341+ logger .debug ("Could not determine system pip version, skipping system-wide Avocado cleanup" )
342+ return
343+ pip_vmajor , pip_vminor = int (match .group (1 )), int (match .group (2 ))
344+ logger .info ("Removing system-wide Avocado packages" )
345+ self ._pip_uninstall (pip_cmd , pip_vmajor , pip_vminor )
346+
347+ def _pip_uninstall (self , pip_cmd , pip_vmajor , pip_vminor ):
263348 for package in self .uninstall_packages :
264- cmd = '%s uninstall %s -y --disable-pip-version-check' % (self . pip_cmd , package )
265- if (self . pip_vmajor > 23 ) or (self . pip_vmajor == 23 and self . pip_vminor >= 1 ):
349+ cmd = '%s uninstall %s -y --disable-pip-version-check' % (pip_cmd , package )
350+ if (pip_vmajor > 23 ) or (pip_vmajor == 23 and pip_vminor >= 1 ):
266351 cmd = cmd + ' --break-system-packages' # --break-system-packages introduced in pip 23.1
267352 runcmd (cmd , ignore_status = True ,
268353 err_str = "Error in removing package: %s" % package ,
269354 debug_str = "Uninstalling %s" % package )
270355
356+ def remove_venv (self ):
357+ if os .path .isdir (self .venv_dir ):
358+ logger .info ("Cleaning up any previously existing virtual environment at %s" ,
359+ self .venv_dir )
360+ shutil .rmtree (self .venv_dir , ignore_errors = True )
361+
271362
272363class RemoteRunner :
273364 """
@@ -427,7 +518,7 @@ def gcov_code_coverage(basedir_name, test_name, driver_name=None):
427518 runcmd ("sed -n -i '/Function/{N;p}' coverage.txt" , ignore_status = True )
428519 covrg_percentage = 0
429520 with open ('coverage.txt' , 'r+' ) as fs1 :
430- for line1 , line2 in itertools .zip_longest (* [fs1 ]* 2 ):
521+ for line1 , line2 in itertools .zip_longest (* [fs1 ] * 2 ):
431522 if not line2 .startswith ("Line" ):
432523 continue
433524 out = line2 .split (":" )[- 1 ]
0 commit comments