blob: 0d84572876295d375cd0ba03a58a8edecafbfdd3 [file] [log] [blame]
# Copyright 2020 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Environment utility functions.
Usage:
api.environment.initialize(checkout_root=...)
with api.environment():
...
"""
import contextlib
import attr
from recipe_engine import recipe_api
@attr.s
class Package(object):
name = attr.ib(type=bytes)
version = attr.ib(type=bytes)
class EnvironmentApi(recipe_api.RecipeApi):
"""Environment utility functions."""
def __init__(self, props, *args, **kwargs):
super(EnvironmentApi, self).__init__(*args, **kwargs)
self._cargo_package_files = props.cargo_package_files
self._cipd_package_files = props.cipd_package_files
self._virtualenv_requirements = props.virtualenv_requirements
self._virtualenv_setup_py_roots = props.virtualenv_setup_py_roots
self._root_variable_name = str(props.root_variable_name)
self._relative_pigweed_root = str(props.relative_pigweed_root)
self._cipd_dir = None
self._path_prefixes = []
self._ldpath_prefixes = []
self._env = {}
self._initialized = False
self._cipd_installation_dirs = []
def _convert_paths(self, root, pathlist):
results = []
for path in pathlist:
parts = [x for x in path.split('/') if x != '.']
results.append(root.join(*parts))
return results
def _init_platform(self):
if self.m.platform.is_mac:
with self.m.step.nest('setup platform'):
with self.m.macos_sdk():
pass
def _init_cipd(self, checkout_root):
"""Install CIPD packages."""
with self.m.step.nest('setup cipd'):
self._cipd_dir = self.m.path['start_dir'].join('cipd')
self._env['PW_CIPD_INSTALL_DIR'] = self._cipd_dir
for json_path in self._convert_paths(
checkout_root, self._cipd_package_files
):
name = self.m.path.splitext(self.m.path.basename(json_path))[0]
with self.m.step.nest(name):
install_dir = self._cipd_dir.join(name)
self._env[
'PW_{}_CIPD_INSTALL_DIR'.format(name.upper())
] = install_dir
packages = self.m.file.read_json(
'read {}'.format(json_path), json_path
)
if not packages:
continue
ensure_file = self.m.cipd.EnsureFile()
for pkg in packages:
# JSON files are read as unicode, need to encode to get
# str.
ensure_file.add_package(
pkg['path'].encode(),
' '.join(x.encode() for x in pkg['tags']),
subdir=pkg.get('subdir'),
)
self.m.cipd.ensure(install_dir, ensure_file)
for path in ('', 'bin', 'mingw64/bin'):
path = install_dir.join(*path.split('/'))
if self.m.path.exists(path):
self._path_prefixes.append(path)
for path in ('', 'lib'):
path = install_dir.join(*path.split('/'))
if self.m.path.exists(path):
self._ldpath_prefixes.append(path)
self._cipd_installation_dirs.append(install_dir)
def _find_python_packages(self, roots):
"""Return all folders with setup.py entries.
Note: this function should not be called from within a virtualenv.
Virtualenvs screw with how vpython works.
Args:
roots(Collection[Path]): locations to search for setup.py files
Returns:
A list of package paths.
"""
matches = []
with self.m.step.nest('find_python_packages'):
for root in roots:
files = self.m.file.listdir(str(root), root, recursive=True)
matches.extend(
self.m.path.dirname(x)
for x in files
if self.m.path.basename(x) == 'setup.py'
)
original_matches = matches[:]
for match in original_matches:
# Remove all matches that have another match as a strict prefix.
matches = [
x for x in matches if x == match or not x.startswith(match)
]
with self.m.step.nest('packages') as step:
step.logs['raw matches'] = [str(x) for x in original_matches]
step.logs['filtered matches'] = [str(x) for x in matches]
return matches
def _init_python(self, checkout_root):
"""Initialize the Python environment. (Specifically, install 'pw'.)"""
with self.m.step.nest('setup python'):
packages = self._find_python_packages(
self._convert_paths(
checkout_root, self._virtualenv_setup_py_roots
)
)
with self.m.step.nest('setup virtualenv'), self():
venv_dir = self.m.path['start_dir'].join('venv')
cipd_python = 'python3'
# Work around weird bug where venv creation requires the name of
# the executable to be python.exe and not python3.exe on
# Windows.
if self.m.platform.is_win:
for install_dir in self._cipd_installation_dirs:
this_python = install_dir.join('bin', 'python3.exe')
if self.m.path.exists(this_python):
cipd_python = this_python
new_cipd_python = install_dir.join(
'bin', 'python.exe'
)
self.m.file.copy(
'cp python3.exe python.exe',
cipd_python,
new_cipd_python,
)
cipd_python = new_cipd_python
self.m.step(
'create venv', [cipd_python, '-m', 'venv', venv_dir]
)
venv_bin = venv_dir.join('bin')
if not self.m.path.exists(venv_bin):
alt_venv_bin = venv_dir.join('Scripts')
# TODO(mohrr) figure out how to cover this.
if self.m.path.exists(alt_venv_bin): # pragma: no cover
venv_bin = alt_venv_bin
python = venv_bin.join('python')
self.m.step(
'upgrade pip',
[python, '-m', 'pip', 'install', '--upgrade', 'pip'],
)
self._path_prefixes.append(venv_bin)
self._env['VIRTUAL_ENV'] = venv_dir
# Need to exit and reenter 'with self()' to include new context from
# creating the virtualenv.
with self.m.step.nest('install packages'), self():
pip_install_prefix = (python, '-m', 'pip', 'install')
pw_cmd = list(pip_install_prefix)
for package in packages:
pw_cmd.append('--editable={}'.format(package))
self.m.step('pigweed tools', pw_cmd)
for req in self._convert_paths(
checkout_root, self._virtualenv_requirements
):
req_cmd = list(pip_install_prefix)
req_cmd.extend(('-r', req))
self.m.step('build requirements', req_cmd)
def _init_cargo(self, checkout_root):
if not self._cargo_package_files:
return
prefix = self.m.path['start_dir'].join('cargo')
self._path_prefixes.append(prefix.join('bin'))
cache = self.m.path['cache'].join('cargo')
self._env['CARGO_TARGET_DIR'] = cache
# On desktops this is the cue to include cargo in setup, but other
# things assume this variable can tell them if we're set up with cargo,
# so set it here for those checks.
self._env['PW_CARGO_SETUP'] = '1'
with self.m.step.nest('setup cargo'):
for packages_txt in self._convert_paths(
checkout_root, self._cargo_package_files
):
raw = self.m.file.read_text(
'read {}'.format(packages_txt), packages_txt
)
for line in raw.splitlines():
line = line.strip()
if not line or line.startswith('#'):
continue
package, version = line.split()
cmd = [
'cargo',
'install',
'--force',
'--root',
prefix,
'--version',
version,
package,
]
# Deliberately narrow scope--read_text() above fails if run
# within 'with self()' because Python virtual environment
# includes Python 3 as 'python' in PATH.
with self():
self.m.step('install {}'.format(package), cmd)
def _init_misc(self):
self._env['GOCACHE'] = self.m.path['cache'].join('go')
def init(self, checkout_root):
pigweed_root = checkout_root
if self._relative_pigweed_root not in (None, '', '.'):
pigweed_root = checkout_root.join(
*self._relative_pigweed_root.split('/')
)
if self._root_variable_name:
self._env[self._root_variable_name] = checkout_root
if not self._initialized:
with self.m.step.nest('environment'):
# Setting _initialized immediately because some setup steps need
# to use the context of previous steps, and invoking self() is
# the easiest way to do so.
self._initialized = True
self._env['PW_ROOT'] = pigweed_root
self._init_platform()
self._init_cipd(checkout_root)
self._init_python(checkout_root)
self._init_cargo(checkout_root)
self._init_misc()
@contextlib.contextmanager
def __call__(self):
assert self._initialized
env_prefixes = {}
# Using reversed() because things that are added later in environment
# setup need to override things that came earlier.
if self._path_prefixes:
env_prefixes['PATH'] = reversed(self._path_prefixes)
if self._ldpath_prefixes:
env_prefixes['LD_LIBRARY_PATH'] = reversed(self._ldpath_prefixes)
with self.m.context(env_prefixes=env_prefixes, env=self._env):
with self.m.macos_sdk():
yield self
def __getattr__(self, name):
if name not in self._env:
raise AttributeError(name)
return self._env.get(name)