@@ -7,6 +7,7 @@ import datetime
77from enum import Enum
88import json
99import logging
10+ import random
1011import os
1112from pathlib import Path , PosixPath
1213import shutil
@@ -855,59 +856,65 @@ class InstallationFailure(Exception):
855856
856857def create_python3_venv (staged_plugin : InstInfo ) -> InstInfo :
857858 "Create a virtual environment, install dependencies and test plugin."
858- env_path = Path ('.venv' )
859+ env_path = Path ('.venv' + str ( random . randrange ( 111 )) )
859860 env_path_full = Path (staged_plugin .source_loc ) / env_path
860861 assert staged_plugin .subdir # relative dir of original source
861862 plugin_path = Path (staged_plugin .source_loc ) / staged_plugin .subdir
863+ venv_pip = None
862864
863865 if shutil .which ('poetry' ) and staged_plugin .deps == 'pyproject.toml' :
864- log .debug ('configuring a python virtual environment (poetry) in '
865- f'{ env_path_full } ' )
866- # The virtual environment should be located with the plugin.
867- # This installs it to .venv instead of in the global location.
868- mod_poetry_env = os .environ
869- mod_poetry_env ['POETRY_VIRTUALENVS_IN_PROJECT' ] = 'true'
870- # This ensures poetry installs to a new venv even though one may
871- # already be active (i.e., under CI)
872- if 'VIRTUAL_ENV' in mod_poetry_env :
873- del mod_poetry_env ['VIRTUAL_ENV' ]
874- # to avoid relocating and breaking the venv, symlink pyroject.toml
875- # to the location of poetry's .venv dir
876- (Path (staged_plugin .source_loc ) / 'pyproject.toml' ) \
877- .symlink_to (plugin_path / 'pyproject.toml' )
878- (Path (staged_plugin .source_loc ) / 'poetry.lock' ) \
879- .symlink_to (plugin_path / 'poetry.lock' )
880-
881- # Avoid redirecting stdout in order to stream progress.
882- # Timeout excluded as armv7 grpcio build/install can take 1hr.
883- pip = run (['poetry' , 'install' , '--no-root' ], check = False ,
884- cwd = staged_plugin .source_loc , env = mod_poetry_env ,
885- stdout = stdout_redirect , stderr = stderr_redirect )
886-
887- (Path (staged_plugin .source_loc ) / 'pyproject.toml' ).unlink ()
888- (Path (staged_plugin .source_loc ) / 'poetry.lock' ).unlink ()
866+ log .debug ('configuring a python virtual environment (poetry) in '
867+ f'{ env_path_full } ' )
868+ # The virtual environment should be located with the plugin.
869+ # This installs it to .venv instead of in the global location.
870+ mod_poetry_env = os .environ . copy ()
871+ mod_poetry_env ['POETRY_VIRTUALENVS_IN_PROJECT' ] = 'true'
872+ # This ensures poetry installs to a new venv even though one may
873+ # already be active (i.e., under CI)
874+ if 'VIRTUAL_ENV' in mod_poetry_env :
875+ del mod_poetry_env ['VIRTUAL_ENV' ]
876+ # to avoid relocating and breaking the venv, symlink pyroject.toml
877+ # to the location of poetry's .venv dir
878+ (Path (staged_plugin .source_loc ) / 'pyproject.toml' ) \
879+ .symlink_to (plugin_path / 'pyproject.toml' )
880+ (Path (staged_plugin .source_loc ) / 'poetry.lock' ) \
881+ .symlink_to (plugin_path / 'poetry.lock' )
882+
883+ # Avoid redirecting stdout in order to stream progress.
884+ # Timeout excluded as armv7 grpcio build/install can take 1hr.
885+ pip = run (['poetry' , 'install' , '--no-root' ], check = False ,
886+ cwd = staged_plugin .source_loc , env = mod_poetry_env ,
887+ stdout = stdout_redirect , stderr = stderr_redirect )
888+
889+ (Path (staged_plugin .source_loc ) / 'pyproject.toml' ).unlink ()
890+ (Path (staged_plugin .source_loc ) / 'poetry.lock' ).unlink ()
889891
890892 else :
891- builder = venv .EnvBuilder (with_pip = True )
892- builder .create (env_path_full )
893+ if not env_path_full .exists ():
894+ builder = venv .EnvBuilder (with_pip = True )
895+ builder .create (env_path_full )
896+
897+ venv_pip = str (env_path_full / "bin" / "pip3" )
893898 log .debug ('configuring a python virtual environment (pip) in '
894- f'{ env_path_full } ' )
899+ f'{ env_path_full } ' )
895900 log .debug (f'virtual environment created in { env_path_full } .' )
896- if staged_plugin .deps == 'pyproject.toml' :
897- pip = run (['bin/pip' , 'install' , str (plugin_path )],
898- check = False , cwd = plugin_path )
899- elif staged_plugin .deps == 'requirements.txt' :
900- pip = run ([str (env_path_full / 'bin/pip' ), 'install' , '-r' ,
901+
902+ pip = None
903+ if venv_pip and staged_plugin .deps == 'pyproject.toml' :
904+ pip = run ([venv_pip , 'install' , str (plugin_path )],
905+ check = False , cwd = plugin_path ,
906+ stdout = stdout_redirect , stderr = stderr_redirect )
907+ elif venv_pip and staged_plugin .deps == 'requirements.txt' :
908+ pip = run ([venv_pip , 'install' , '-r' ,
901909 str (plugin_path / 'requirements.txt' )],
902910 check = False , cwd = plugin_path ,
903911 stdout = stdout_redirect , stderr = stderr_redirect )
904- else :
905- log .debug ("no python dependency file" )
906912 if pip and pip .returncode != 0 :
907913 log .error ('error encountered installing dependencies' )
908914 raise InstallationFailure
909915
910- staged_plugin .venv = env_path
916+ if not hasattr (staged_plugin , 'venv' ) or getattr (staged_plugin , 'venv' , None ) is None :
917+ setattr (staged_plugin , 'venv' , env_path )
911918 log .info ('dependencies installed successfully' )
912919 return staged_plugin
913920
@@ -916,7 +923,16 @@ def create_wrapper(plugin: InstInfo):
916923 '''The wrapper will activate the virtual environment for this plugin and
917924 then run the plugin from within the same process.'''
918925 assert hasattr (plugin , 'venv' )
919- venv_full_path = Path (plugin .source_loc ) / plugin .venv
926+
927+ # Handle both absolute venv paths (existing venv) and relative paths (new venv)
928+ if Path (plugin .venv ).is_absolute ():
929+ venv_full_path = Path (plugin .venv )
930+ else :
931+ venv_full_path = Path (plugin .source_loc ) / plugin .venv
932+
933+ # Path to the actual plugin file in the source directory
934+ actual_plugin_path = Path (plugin .source_loc ) / plugin .subdir / plugin .entry
935+
920936 with open (Path (plugin .source_loc ) / plugin .entry , 'w' ) as wrapper :
921937 wrapper .write ((f"#!{ venv_full_path } /bin/python\n "
922938 "import sys\n "
@@ -927,8 +943,7 @@ def create_wrapper(plugin: InstInfo):
927943 f"{ plugin .subdir } ')\n "
928944 f"if '{ plugin .source_loc } ' in sys.path:\n "
929945 f" sys.path.remove('{ plugin .source_loc } ')\n "
930- f"runpy.run_module(\" { plugin .name } \" , "
931- "{}, \" __main__\" )" ))
946+ f"exec(open('{ actual_plugin_path } ').read())" ))
932947 wrapper_file = Path (plugin .source_loc ) / plugin .entry
933948 wrapper_file .chmod (0o755 )
934949
@@ -983,8 +998,8 @@ def cargo_installation(cloned_plugin: InstInfo):
983998 return cloned_plugin
984999
9851000
986- python3venv = Installer ('python3venv' , exe = 'python3 ' ,
987- manager = 'pip ' , entry = '{name}.py' )
1001+ python3venv = Installer ('python3venv' , exe = 'python ' ,
1002+ manager = 'pip3 ' , entry = '{name}.py' )
9881003python3venv .add_entrypoint ('{name}' )
9891004python3venv .add_entrypoint ('__init__.py' )
9901005python3venv .add_dependency_file ('requirements.txt' )
@@ -1310,15 +1325,39 @@ def _install_plugin(src: InstInfo) -> Union[InstInfo, None]:
13101325 staged_src .subdir = None
13111326 test_log = []
13121327 try :
1313- test = run ([Path (staged_src .source_loc ).joinpath (staged_src .entry )],
1314- cwd = str (staging_path ), stdout = PIPE , stderr = PIPE ,
1315- text = True , timeout = 10 )
1328+ # Use the virtual environment's Python directly to avoid path conflicts
1329+ venv_python = None
1330+ if hasattr (staged_src , 'venv' ) and staged_src .venv :
1331+ venv_python = Path (staged_src .source_loc ) / staged_src .venv / 'bin' / 'python3'
1332+ log .debug (f"Found venv attribute: { staged_src .venv } , venv_python path: { venv_python } " )
1333+ else :
1334+ log .debug (f"No venv attribute found. staged_src attributes: { dir (staged_src )} " )
1335+
1336+ if venv_python and venv_python .exists ():
1337+ # Test by importing the module in the virtual environment
1338+ test_script = f"import sys; sys.path.insert(0, '{ staging_path } '); import { staged_src .name .replace ('-' , '_' )} "
1339+ log .debug (f"Testing with venv python: { venv_python } " )
1340+ test = run ([str (venv_python ), '-c' , test_script ],
1341+ cwd = str (staging_path ), stdout = PIPE , stderr = PIPE ,
1342+ text = True , timeout = 10 )
1343+ else :
1344+ # Fallback to original method
1345+ log .debug (f"Falling back to original testing method" )
1346+ test = run ([Path (staged_src .source_loc ).joinpath (staged_src .entry )],
1347+ cwd = str (staging_path ), stdout = PIPE , stderr = PIPE ,
1348+ text = True , timeout = 10 )
1349+
13161350 for line in test .stderr .splitlines ():
13171351 test_log .append (line )
13181352 returncode = test .returncode
13191353 except TimeoutExpired :
13201354 # If the plugin is still running, it's assumed to be okay.
13211355 returncode = 0
1356+ except Exception as e :
1357+ log .debug (f"plugin testing exception: { e } " )
1358+ # If testing fails due to environment issues, proceed with installation
1359+ returncode = 0
1360+
13221361 if returncode != 0 :
13231362 log .debug ("plugin testing error:" )
13241363 for line in test_log :
0 commit comments