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 try:
30  from urllib.request import urlretrieve
31 except ImportError:
32  # for python2
33  from urllib import urlretrieve
34 
35 from distutils.spawn import find_executable
36 
37 from . import run_command, relocate
38 from .collect_requirements import collect_requirements
39 
40 _BYTECODE_REGEX = re.compile(r".*\.py[co]")
41 _COMMENT_REGEX = re.compile(r"(^|\s+)#.*$", flags=re.MULTILINE)
42 
43 logger = logging.getLogger(__name__)
44 
45 
46 class Virtualenv:
47  def __init__(self, path):
48  """ Manage a virtualenv at the specified path. """
49  self.path = path
50 
51  def initialize(self, python, use_system_packages, extra_pip_args, clean=True):
52  """ Initialize a new virtualenv using the specified python version and extra arguments. """
53  if clean:
54  try:
55  shutil.rmtree(self.path)
56  except Exception:
57  pass
58 
59  system_python = find_executable(python)
60 
61  if not system_python:
62  error_msg = "Unable to find a system-installed {}.".format(python)
63  if python and python[0].isdigit():
64  error_msg += " Perhaps you meant python{}".format(python)
65  raise RuntimeError(error_msg)
66 
67  preinstall = [
68  "pip==22.0.2",
69  "pip-tools==6.10.0",
70  ]
71 
72  builtin_venv = self._check_module(system_python, "venv")
73  if builtin_venv:
74  virtualenv = [system_python, "-m", "venv"]
75  else:
76  virtualenv = ["virtualenv", "--no-setuptools", "--verbose", "--python", python]
77  # py2's virtualenv command will try install latest setuptools. setuptools>=45 not compatible with py2,
78  # but we do require a reasonably up-to-date version (because of pip==20.1), so v44 at least.
79  preinstall += ["setuptools>=44,<45"]
80 
81  if use_system_packages:
82  virtualenv.append("--system-site-packages")
83 
84  without_pip = self._check_module(system_python, "ensurepip") is False
85  if without_pip:
86  virtualenv.append('--without-pip')
87 
88  virtualenv.append(self.path)
89  run_command(virtualenv, check=True)
90 
91  if without_pip:
92  # install pip via get-pip.py
93  version_proc = run_command(
94  ['python', "-cimport sys; print('{}.{}'.format(*sys.version_info))"],
95  capture_output=True)
96  version = version_proc.stdout
97  if isinstance(version, bytes):
98  version = version.decode('utf-8')
99  version = version.strip()
100  # download pip from https://bootstrap.pypa.io/pip/
101  get_pip_path, _ = urlretrieve("https://bootstrap.pypa.io/pip/get-pip.py")
102  run_command([self._venv_bin("python"), get_pip_path], check=True)
103 
104  run_command([self._venv_bin("python"), "-m", "pip", "install"] + extra_pip_args + preinstall, check=True)
105 
106  def install(self, requirements, extra_pip_args):
107  """ Sync a virtualenv with the specified requirements. """
108  command = [self._venv_bin("python"), "-m", "pip", "install"] + extra_pip_args
109  for req in requirements:
110  run_command(command + ["-r", req], check=True)
111 
112  def check(self, requirements, extra_pip_args):
113  """ Check if a set of requirements is completely locked. """
114  with open(requirements, "r") as f:
115  existing_requirements = f.read()
116 
117  # Re-lock the requirements
118  command = [self._venv_bin("pip-compile"), "--no-header", "--annotation-style", "line", requirements, "-o", "-"]
119  if extra_pip_args:
120  command += ["--pip-args", " ".join(extra_pip_args)]
121 
122  generated_requirements = run_command(command, check=True, capture_output=True).stdout.decode()
123 
124  def _format(content):
125  # Remove comments
126  content = _COMMENT_REGEX.sub("", content)
127  # Remove case sensitivity
128  content = content.lower()
129  # Split into lines for diff
130  content = content.splitlines()
131  # ignore order
132  content.sort()
133  return content
134 
135  # Compare against existing requirements
136  diff = list(difflib.unified_diff(_format(existing_requirements), _format(generated_requirements)))
137 
138  return diff
139 
140  def lock(self, package_name, input_requirements, no_overwrite, extra_pip_args):
141  """ Create a frozen requirement set from a set of input specifications. """
142  try:
143  output_requirements = collect_requirements(package_name, no_deps=True)[0]
144  except IndexError:
145  logger.info("Package doesn't export any requirements, step can be skipped")
146  return
147 
148  if no_overwrite and os.path.exists(output_requirements):
149  logger.info("Lock file already exists, not overwriting")
150  return
151 
152  pip_compile = self._venv_bin("pip-compile")
153  command = [pip_compile, "--no-header", "--annotation-style", "line", input_requirements]
154 
155  if os.path.normpath(input_requirements) == os.path.normpath(output_requirements):
156  raise RuntimeError(
157  "Trying to write locked requirements {} into a path specified as input: {}".format(
158  output_requirements, input_requirements
159  )
160  )
161 
162  if extra_pip_args:
163  command += ["--pip-args", " ".join(extra_pip_args)]
164 
165  command += ["-o", output_requirements]
166 
167  run_command(command, check=True)
168 
169  logger.info("Wrote new lock file to {}".format(output_requirements))
170 
171  def relocate(self, target_dir):
172  """ Relocate a virtualenv to another directory. """
173  self._delete_bytecode()
174  relocate.fix_shebangs(self.path, target_dir)
175  relocate.fix_activate_path(self.path, target_dir)
176 
177  # This workaround has been flaky - let's just delete the 'local' folder entirely
178  # relocate.fix_local_symlinks(self.path)
179  local_dir = os.path.join(self.path, "local")
180  if os.path.exists(local_dir):
181  shutil.rmtree(local_dir)
182 
183  def _venv_bin(self, binary_name):
184  if os.path.exists(os.path.join(self.path, "bin", binary_name)):
185  return os.path.abspath(os.path.join(self.path, "bin", binary_name))
186  elif os.path.exists(os.path.join(self.path, "local", "bin", binary_name)):
187  return os.path.abspath(os.path.join(self.path, "local", "bin", binary_name))
188  raise RuntimeError("Binary {} not found in venv".format(binary_name))
189 
190  def _check_module(self, python_executable, module):
191  try:
192  with open(os.devnull, "w") as devnull:
193  # "-c 'import venv'" does not work with the subprocess module, but '-cimport venv' does
194  run_command([python_executable, "-cimport {}".format(module)], stderr=devnull, check=True)
195  return True
196  except subprocess.CalledProcessError:
197  return False
198 
199  def _delete_bytecode(self):
200  """ Remove all .py[co] files since they embed absolute paths. """
201  for root, _, files in os.walk(self.path):
202  for f in files:
203  if _BYTECODE_REGEX.match(f):
204  os.remove(os.path.join(root, f))
def collect_requirements(package_name, no_deps=False)
def __init__(self, path)
Definition: venv.py:47
def check(self, requirements, extra_pip_args)
Definition: venv.py:112
def initialize(self, python, use_system_packages, extra_pip_args, clean=True)
Definition: venv.py:51
def lock(self, package_name, input_requirements, no_overwrite, extra_pip_args)
Definition: venv.py:140
def install(self, requirements, extra_pip_args)
Definition: venv.py:106
def _check_module(self, python_executable, module)
Definition: venv.py:190
def relocate(self, target_dir)
Definition: venv.py:171
def _venv_bin(self, binary_name)
Definition: venv.py:183
def run_command(cmd, args, kwargs)
Definition: __init__.py:32


catkin_virtualenv
Author(s): Paul Bovbel
autogenerated on Sat Jun 10 2023 02:37:37