venv.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # Software License Agreement (GPL)
3 #
4 # \file venv.py
5 # \authors Paul Bovbel <pbovbel@locusrobotics.com>
6 # \copyright Copyright (c) (2017,), Locus Robotics, All rights reserved.
7 #
8 # This program is free software: you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License as
10 # published by the Free Software Foundation, either version 2 of the
11 # License, or (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 # General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 from __future__ import print_function
21 
22 import difflib
23 import logging
24 import os
25 import re
26 import shutil
27 import subprocess
28 import tempfile
29 
30 try:
31  from urllib.request import urlretrieve
32 except ImportError:
33  # for python2
34  from urllib import urlretrieve
35 
36 from distutils.spawn import find_executable
37 
38 from . import run_command, relocate
39 from .collect_requirements import collect_requirements
40 
41 _BYTECODE_REGEX = re.compile(r".*\.py[co]")
42 _COMMENT_REGEX = re.compile(r"(^|\s+)#.*$", flags=re.MULTILINE)
43 
44 logger = logging.getLogger(__name__)
45 
46 
47 class Virtualenv:
48  def __init__(self, path):
49  """Manage a virtualenv at the specified path."""
50  self.path = path
51 
52  def initialize(self, python, use_system_packages, extra_pip_args, clean=True):
53  """Initialize a new virtualenv using the specified python version and extra arguments."""
54  if clean:
55  try:
56  shutil.rmtree(self.path)
57  except Exception:
58  pass
59 
60  system_python = find_executable(python)
61 
62  if not system_python:
63  error_msg = "Unable to find a system-installed {}.".format(python)
64  if python and python[0].isdigit():
65  error_msg += " Perhaps you meant python{}".format(python)
66  raise RuntimeError(error_msg)
67 
68  preinstall = [
69  "pip==24.0",
70  "pip-tools==7.4.1",
71  ]
72 
73  builtin_venv = self._check_module(system_python, "venv")
74  if builtin_venv:
75  virtualenv = [system_python, "-m", "venv"]
76  else:
77  virtualenv = ["virtualenv", "--no-setuptools", "--verbose", "--python", python]
78  # py2's virtualenv command will try install latest setuptools. setuptools>=45 not compatible with py2,
79  # but we do require a reasonably up-to-date version (because of pip==20.1), so v44 at least.
80  preinstall += ["setuptools>=44,<45"]
81 
82  if use_system_packages:
83  virtualenv.append("--system-site-packages")
84 
85  without_pip = self._check_module(system_python, "ensurepip") is False
86  if without_pip:
87  virtualenv.append("--without-pip")
88 
89  virtualenv.append(self.path)
90  run_command(virtualenv, check=True)
91 
92  if without_pip:
93  # install pip via get-pip.py
94  version_proc = run_command(
95  ["python", "-cimport sys; print('{}.{}'.format(*sys.version_info))"], capture_output=True
96  )
97  version = version_proc.stdout
98  if isinstance(version, bytes):
99  version = version.decode("utf-8")
100  version = version.strip()
101  # download pip from https://bootstrap.pypa.io/pip/
102  get_pip_path, _ = urlretrieve("https://bootstrap.pypa.io/pip/get-pip.py")
103  run_command([self._venv_bin("python"), get_pip_path], check=True)
104 
105  # (gservin): test --no-cache-dir
106  run_command(
107  [self._venv_bin("python"), "-m", "pip", "install", "--no-cache-dir", "-vvv"] + extra_pip_args + preinstall,
108  check=True,
109  )
110 
111  def install(self, requirements, extra_pip_args):
112  """Purge the cache first before installing.""" # (KLAD) testing to debug an issue on build farm
113  command = [self._venv_bin("python"), "-m", "pip", "cache", "purge"]
114  """ Sync a virtualenv with the specified requirements.""" # (KLAD) testing no-cache-dir
115  command = [self._venv_bin("python"), "-m", "pip", "install", "-vvv", "--no-cache-dir"] + extra_pip_args
116  for req in requirements:
117  run_command(command + ["-r", req], check=True)
118 
119  def check(self, requirements, extra_pip_args):
120  """Check if a set of requirements is completely locked."""
121  with open(requirements, "r") as f:
122  existing_requirements = f.read()
123 
124  # Re-lock the requirements
125  command = [self._venv_bin("pip-compile"), "--no-header", "--annotation-style", "line", requirements, "-o", "-"]
126  if extra_pip_args:
127  command += ["--pip-args", " ".join(extra_pip_args)]
128 
129  generated_requirements = run_command(command, check=True, capture_output=True).stdout.decode()
130 
131  def _format(content):
132  # Remove comments
133  content = _COMMENT_REGEX.sub("", content)
134  # Remove case sensitivity
135  content = content.lower()
136  # Split into lines for diff
137  content = content.splitlines()
138  # ignore order
139  content.sort()
140  return content
141 
142  # Compare against existing requirements
143  diff = list(difflib.unified_diff(_format(existing_requirements), _format(generated_requirements)))
144 
145  return diff
146 
147  def lock(self, package_name, input_requirements, no_overwrite, extra_pip_args):
148  """Create a frozen requirement set from a set of input specifications."""
149  try:
150  output_requirements = collect_requirements(package_name, no_deps=True)[0]
151  except IndexError:
152  logger.info("Package doesn't export any requirements, step can be skipped")
153  return
154 
155  if no_overwrite and os.path.exists(output_requirements):
156  logger.info("Lock file already exists, not overwriting")
157  return
158 
159  pip_compile = self._venv_bin("pip-compile")
160  command = [pip_compile, "--no-header", "--annotation-style", "line", input_requirements]
161 
162  if os.path.normpath(input_requirements) == os.path.normpath(output_requirements):
163  raise RuntimeError(
164  "Trying to write locked requirements {} into a path specified as input: {}".format(
165  output_requirements, input_requirements
166  )
167  )
168 
169  if extra_pip_args:
170  command += ["--pip-args", " ".join(extra_pip_args)]
171 
172  command += ["-o", output_requirements]
173 
174  run_command(command, check=True)
175 
176  logger.info("Wrote new lock file to {}".format(output_requirements))
177 
178  def relocate(self, target_dir):
179  """Relocate a virtualenv to another directory."""
180  self._delete_bytecode()
181  relocate.fix_shebangs(self.path, target_dir)
182  relocate.fix_activate_path(self.path, target_dir)
183 
184  # This workaround has been flaky - let's just delete the 'local' folder entirely
185  # relocate.fix_local_symlinks(self.path)
186  local_dir = os.path.join(self.path, "local")
187  if os.path.exists(local_dir):
188  shutil.rmtree(local_dir)
189 
190  def _venv_bin(self, binary_name):
191  if os.path.exists(os.path.join(self.path, "bin", binary_name)):
192  return os.path.abspath(os.path.join(self.path, "bin", binary_name))
193  elif os.path.exists(os.path.join(self.path, "local", "bin", binary_name)):
194  return os.path.abspath(os.path.join(self.path, "local", "bin", binary_name))
195  raise RuntimeError("Binary {} not found in venv".format(binary_name))
196 
197  def _check_module(self, python_executable, module):
198  try:
199  with open(os.devnull, "w") as devnull:
200  # "-c 'import venv'" does not work with the subprocess module, but '-cimport venv' does
201  run_command([python_executable, "-cimport {}".format(module)], stderr=devnull, check=True)
202  return True
203  except subprocess.CalledProcessError:
204  return False
205 
206  def _delete_bytecode(self):
207  """Remove all .py[co] files since they embed absolute paths."""
208  for root, _, files in os.walk(self.path):
209  for f in files:
210  if _BYTECODE_REGEX.match(f):
211  os.remove(os.path.join(root, f))
catkin_virtualenv.collect_requirements.collect_requirements
def collect_requirements(package_name, no_deps=False)
Definition: collect_requirements.py:57
catkin_virtualenv.run_command
def run_command(cmd, *args, **kwargs)
Definition: __init__.py:32
catkin_virtualenv.venv.Virtualenv._venv_bin
def _venv_bin(self, binary_name)
Definition: venv.py:190
catkin_virtualenv.venv.Virtualenv.relocate
def relocate(self, target_dir)
Definition: venv.py:178
catkin_virtualenv.venv.Virtualenv._delete_bytecode
def _delete_bytecode(self)
Definition: venv.py:206
catkin_virtualenv.venv.Virtualenv.path
path
Definition: venv.py:50
catkin_virtualenv.venv.Virtualenv
Definition: venv.py:47
catkin_virtualenv.venv.Virtualenv.check
def check(self, requirements, extra_pip_args)
Definition: venv.py:119
catkin_virtualenv.venv.Virtualenv.lock
def lock(self, package_name, input_requirements, no_overwrite, extra_pip_args)
Definition: venv.py:147
catkin_virtualenv.venv.Virtualenv.__init__
def __init__(self, path)
Definition: venv.py:48
catkin_virtualenv.venv.Virtualenv._check_module
def _check_module(self, python_executable, module)
Definition: venv.py:197
catkin_virtualenv.venv.Virtualenv.install
def install(self, requirements, extra_pip_args)
Definition: venv.py:111
catkin_virtualenv.venv.Virtualenv.initialize
def initialize(self, python, use_system_packages, extra_pip_args, clean=True)
Definition: venv.py:52


catkin_virtualenv
Author(s): Paul Bovbel
autogenerated on Mon Mar 17 2025 02:35:33