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


catkin_virtualenv
Author(s): Paul Bovbel
autogenerated on Thu Apr 1 2021 02:36:27