aboutsummaryrefslogtreecommitdiff
path: root/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py
diff options
context:
space:
mode:
Diffstat (limited to 'pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py')
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py115
1 files changed, 94 insertions, 21 deletions
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py b/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py
index f346d0557..7f4886cb9 100644
--- a/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py
@@ -32,6 +32,10 @@ import tempfile
_DATETIME_STRING = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
+def _is_windows() -> bool:
+ return platform.system().lower() == 'windows'
+
+
class GnTarget(object): # pylint: disable=useless-object-inheritance
def __init__(self, val):
self.directory, self.target = val.split('#', 1)
@@ -58,7 +62,7 @@ class GitRepoNotFound(Exception):
def _installed_packages(venv_python):
- cmd = (venv_python, '-m', 'pip', 'list', '--disable-pip-version-check')
+ cmd = (venv_python, '-m', 'pip', '--disable-pip-version-check', 'list')
output = subprocess.check_output(cmd).splitlines()
return set(x.split()[0].lower() for x in output[2:])
@@ -110,7 +114,7 @@ def _find_files_by_name(roots, name, allow_nesting=False):
def _check_venv(python, version, venv_path, pyvenv_cfg):
- if platform.system().lower() == 'windows':
+ if _is_windows():
return
# Check if the python location and version used for the existing virtualenv
@@ -126,14 +130,14 @@ def _check_venv(python, version, venv_path, pyvenv_cfg):
home = pyvenv_values.get('home')
if pydir != home and not pydir.startswith(venv_path):
shutil.rmtree(venv_path)
- elif pyvenv_values.get('version') not in version:
+ elif pyvenv_values.get('version') not in '.'.join(map(str, version)):
shutil.rmtree(venv_path)
def _check_python_install_permissions(python):
# These pickle files are not included on windows.
# The path on windows is environment/cipd/packages/python/bin/Lib/lib2to3/
- if platform.system().lower() == 'windows':
+ if _is_windows():
return
# Make any existing lib2to3 pickle files read+write. This is needed for
@@ -169,12 +173,32 @@ def _flatten(*items):
yield item
-def install( # pylint: disable=too-many-arguments,too-many-locals
+def _python_version(python_path: str):
+ """Returns the version (major, minor, rev) of the `python_path` binary."""
+ # Prints values like "3.10.0"
+ command = (
+ python_path,
+ '-c',
+ 'import sys; print(".".join(map(str, sys.version_info[:3])))',
+ )
+ version_str = (
+ subprocess.check_output(command, stderr=subprocess.STDOUT)
+ .strip()
+ .decode()
+ )
+ return tuple(map(int, version_str.split('.')))
+
+
+def install( # pylint: disable=too-many-arguments,too-many-locals,too-many-statements
project_root,
venv_path,
full_envsetup=True,
requirements=None,
constraints=None,
+ pip_install_disable_cache=None,
+ pip_install_find_links=None,
+ pip_install_offline=None,
+ pip_install_require_hashes=None,
gn_args=(),
gn_targets=(),
gn_out_dir=None,
@@ -185,12 +209,8 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
):
"""Creates a venv and installs all packages in this Git repo."""
- version = (
- subprocess.check_output((python, '--version'), stderr=subprocess.STDOUT)
- .strip()
- .decode()
- )
- if ' 3.' not in version:
+ version = _python_version(python)
+ if version[0] != 3:
print('=' * 60, file=sys.stderr)
print('Unexpected Python version:', version, file=sys.stderr)
print('=' * 60, file=sys.stderr)
@@ -207,12 +227,15 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
else:
env = contextlib.nullcontext()
- # Delete activation scripts. Typically they're created read-only and venv
- # will complain when trying to write over them fails.
- if os.path.isdir(venv_bin):
- for entry in os.listdir(venv_bin):
- if entry.lower().startswith('activate'):
- os.unlink(os.path.join(venv_bin, entry))
+ # Virtual environments may contain read-only files (notably activate
+ # scripts). `venv` calls below will fail if they are not writeable.
+ if os.path.isdir(venv_path):
+ for root, _dirs, files in os.walk(venv_path):
+ for file in files:
+ path = os.path.join(root, file)
+ mode = os.lstat(path).st_mode
+ if not (stat.S_ISLNK(mode) or (mode & stat.S_IWRITE)):
+ os.chmod(path, mode | stat.S_IWRITE)
pyvenv_cfg = os.path.join(venv_path, 'pyvenv.cfg')
@@ -231,7 +254,27 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
# TODO(spang): Pass --upgrade-deps and remove pip & setuptools
# upgrade below. This can only be done once the minimum python
# version is at least 3.9.
- cmd = [python, '-m', 'venv', '--upgrade']
+ cmd = [python, '-m', 'venv']
+
+ # Windows requires strange wizardry, and must follow symlinks
+ # starting with 3.11.
+ #
+ # Without this, windows fails bootstrap trying to copy
+ # "environment\cipd\packages\python\bin\venvlauncher.exe"
+ #
+ # This file doesn't exist in Python 3.11 on Windows and may be a bug
+ # in venv. Pigweed already uses symlinks on Windows for the GN build,
+ # so adding this option is not an issue.
+ #
+ # Further excitement is had when trying to update a virtual environment
+ # that is created using symlinks under Windows. `venv` will fail with
+ # and error that the source and destination are the same file. To work
+ # around this, we run `venv` in `--clear` mode under Windows.
+ if _is_windows() and version >= (3, 11):
+ cmd += ['--clear', '--symlinks']
+ else:
+ cmd += ['--upgrade']
+
cmd += ['--system-site-packages'] if system_packages else []
cmd += [venv_path]
_check_call(cmd, env=envcopy)
@@ -255,10 +298,33 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
):
os.unlink(egg_link)
+ pip_install_args = []
+ if pip_install_find_links:
+ for package_dir in pip_install_find_links:
+ pip_install_args.append('--find-links')
+ with env():
+ pip_install_args.append(os.path.expandvars(package_dir))
+ if pip_install_require_hashes:
+ pip_install_args.append('--require-hashes')
+ if pip_install_offline:
+ pip_install_args.append('--no-index')
+ if pip_install_disable_cache:
+ pip_install_args.append('--no-cache-dir')
+
def pip_install(*args):
args = list(_flatten(args))
with env():
- cmd = [venv_python, '-m', 'pip', 'install'] + args
+ cmd = (
+ [
+ venv_python,
+ '-m',
+ 'pip',
+ '--disable-pip-version-check',
+ 'install',
+ ]
+ + pip_install_args
+ + args
+ )
return _check_call(cmd)
constraint_args = []
@@ -277,6 +343,7 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
# Include wheel so pip installs can be done without build
# isolation.
'wheel',
+ 'pip-tools',
constraint_args,
)
@@ -301,7 +368,7 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
def install_packages(gn_target):
if gn_out_dir is None:
- build_dir = os.path.join(venv_path, 'gn-install-dir')
+ build_dir = os.path.join(venv_path, 'gn')
else:
build_dir = gn_out_dir
@@ -360,7 +427,13 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
with open(os.path.join(venv_path, 'pip-list.log'), 'w') as outs:
subprocess.check_call(
- [venv_python, '-m', 'pip', 'list'],
+ [
+ venv_python,
+ '-m',
+ 'pip',
+ '--disable-pip-version-check',
+ 'list',
+ ],
stdout=outs,
)