Skip to content

Commit 8083777

Browse files
Merge pull request #403 from bssrikanth/venv
Support optional isolated venv for Avocado bootstrap
2 parents cfe447a + 3f11bdb commit 8083777

3 files changed

Lines changed: 125 additions & 11 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
*.log
22
__pycache__
33
*.pyc
4+
.venv

avocado-setup.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,12 @@ def need_bootstrap(enable_kvm=False):
206206
logger.debug("Check if bootstrap required")
207207
needs_bootstrap = False
208208
# Check for avocado
209-
if 'no avocado ' in helper.get_avocado_bin(ignore_status=True):
209+
if helper.use_venv():
210+
venv_avocado = os.path.join(helper.get_venv_dir(), 'bin', 'avocado')
211+
if not (os.path.isfile(venv_avocado) and os.access(venv_avocado, os.X_OK)):
212+
logger.debug("Avocado needs to be installed")
213+
needs_bootstrap = True
214+
elif 'no avocado ' in helper.get_avocado_bin(ignore_status=True):
210215
logger.debug("Avocado needs to be installed")
211216
needs_bootstrap = True
212217
if enable_kvm:
@@ -305,6 +310,10 @@ def create_config(logdir):
305310

306311
with open(avocado_conf, 'w+') as conf:
307312
config.write(conf)
313+
if helper.use_venv():
314+
venv_conf_dir = os.path.join(helper.get_venv_dir(), '.config', 'avocado')
315+
os.makedirs(venv_conf_dir, exist_ok=True)
316+
shutil.copy2(avocado_conf, os.path.join(venv_conf_dir, 'avocado.conf'))
308317

309318

310319
def guest_download(guestos):
@@ -504,6 +513,9 @@ def env_clean(deep=False):
504513
"""
505514
logger.info("Cleaning the Environment")
506515
pipManager.uninstall()
516+
if helper.use_venv():
517+
pipManager.uninstall_system_wide()
518+
pipManager.remove_venv()
507519
if os.path.isdir(prescript):
508520
helper.remove_file(prescript, prescript_dir)
509521

@@ -721,6 +733,10 @@ def parse_test_config(test_config_file, avocado_bin, enable_kvm, runner):
721733
help='To remove/uninstall autotest, avocado from system')
722734
parser.add_argument('--enable-kvm', dest="enable_kvm", action='store_true',
723735
default=False, help='enable bootstrap kvm tests')
736+
parser.add_argument('--use-venv', '--venv', dest='use_venv', action='store_true',
737+
default=False,
738+
help='Install Avocado into an isolated virtual environment (.venv). '
739+
'Can also be enabled with AVOCADO_USE_VENV=1')
724740
parser.add_argument('--runner', dest="runner", action='store_true',
725741
default=False, help='To use legacy runner with --test-runner runner flag')
726742
parser.add_argument('--code-cov', dest='linux_src_path', action='store',
@@ -746,6 +762,12 @@ def parse_test_config(test_config_file, avocado_bin, enable_kvm, runner):
746762

747763
args = parser.parse_args()
748764

765+
if args.use_venv:
766+
os.environ['AVOCADO_USE_VENV'] = '1'
767+
if helper.use_venv() and not os.environ.get('AVOCADO_VENV'):
768+
os.environ['AVOCADO_VENV'] = os.path.join(BASE_PATH, '.venv')
769+
helper.prepend_venv_to_path()
770+
749771
if args.CONFIG_PATH:
750772
if os.path.exists(args.CONFIG_PATH):
751773
CONFIGFILE.read(args.CONFIG_PATH)

lib/helper.py

Lines changed: 101 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,33 @@
2929
from .logger import logger_init
3030

3131
LOG_PATH = os.path.dirname(os.path.abspath(os.path.join(__file__, os.pardir)))
32+
DEFAULT_VENV_DIR = os.path.join(LOG_PATH, '.venv')
3233

3334
logger = 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+
3659
def 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

272363
class 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

Comments
 (0)