buildman: Support building within a Python venv

The Python virtualenv tool sets up a few things in the environment,
putting its path first in the PATH environment variable and setting up
a sys.prefix different from the sys.base_prefix value.

At present buildman puts the toolchain path first in PATH so that it can
be found easily during the build. For sandbox this causes problems since
/usr/bin/gcc (for example) results in '/usr/bin' being prepended to the
PATH variable. As a result, the venv is partially disabled.

The result is that sandbox builds within a venv ignore the venv, e.g.
when looking for packages.

Correct this by detecting the venv and adding the toolchain path after
the venv path.

Signed-off-by: Simon Glass <sjg@chromium.org>
This commit is contained in:
Simon Glass
2024-08-15 13:57:44 -06:00
committed by Tom Rini
parent a5d990854f
commit d0f74bd417
3 changed files with 114 additions and 4 deletions

View File

@@ -31,6 +31,9 @@ def setup(fname=''):
def add_file(data): def add_file(data):
settings.read_file(io.StringIO(data)) settings.read_file(io.StringIO(data))
def add_section(name):
settings.add_section(name)
def get_items(section): def get_items(section):
"""Get the items from a section of the config. """Get the items from a section of the config.

View File

@@ -148,6 +148,7 @@ class TestBuild(unittest.TestCase):
self.toolchains.Add('arm-linux-gcc', test=False) self.toolchains.Add('arm-linux-gcc', test=False)
self.toolchains.Add('sparc-linux-gcc', test=False) self.toolchains.Add('sparc-linux-gcc', test=False)
self.toolchains.Add('powerpc-linux-gcc', test=False) self.toolchains.Add('powerpc-linux-gcc', test=False)
self.toolchains.Add('/path/to/aarch64-linux-gcc', test=False)
self.toolchains.Add('gcc', test=False) self.toolchains.Add('gcc', test=False)
# Avoid sending any output # Avoid sending any output
@@ -869,6 +870,89 @@ class TestBuild(unittest.TestCase):
self.assertEqual([4, 5], control.read_procs(tmpdir)) self.assertEqual([4, 5], control.read_procs(tmpdir))
self.assertEqual(self.finish_time, self.cur_time) self.assertEqual(self.finish_time, self.cur_time)
def call_make_environment(self, tchn, full_path, in_env=None):
"""Call Toolchain.MakeEnvironment() and process the result
Args:
tchn (Toolchain): Toolchain to use
full_path (bool): True to return the full path in CROSS_COMPILE
rather than adding it to the PATH variable
in_env (dict): Input environment to use, None to use current env
Returns:
tuple:
dict: Changes that MakeEnvironment has made to the environment
key: Environment variable that was changed
value: New value (for PATH this only includes components
which were added)
str: Full value of the new PATH variable
"""
env = tchn.MakeEnvironment(full_path, env=in_env)
# Get the original environment
orig_env = dict(os.environb if in_env is None else in_env)
orig_path = orig_env[b'PATH'].split(b':')
# Find new variables
diff = dict((k, env[k]) for k in env if orig_env.get(k) != env[k])
# Find new / different path components
diff_path = None
new_path = None
if b'PATH' in diff:
new_path = diff[b'PATH'].split(b':')
diff_paths = [p for p in new_path if p not in orig_path]
diff_path = b':'.join(p for p in new_path if p not in orig_path)
if diff_path:
diff[b'PATH'] = diff_path
else:
del diff[b'PATH']
return diff, new_path
def test_toolchain_env(self):
"""Test PATH and other environment settings for toolchains"""
# Use a toolchain which has a path, so that full_path makes a difference
tchn = self.toolchains.Select('aarch64')
# Normal cases
diff = self.call_make_environment(tchn, full_path=False)[0]
self.assertEqual(
{b'CROSS_COMPILE': b'aarch64-linux-', b'LC_ALL': b'C',
b'PATH': b'/path/to'}, diff)
diff = self.call_make_environment(tchn, full_path=True)[0]
self.assertEqual(
{b'CROSS_COMPILE': b'/path/to/aarch64-linux-', b'LC_ALL': b'C'},
diff)
# When overriding the toolchain, only LC_ALL should be set
tchn.override_toolchain = True
diff = self.call_make_environment(tchn, full_path=True)[0]
self.assertEqual({b'LC_ALL': b'C'}, diff)
# Test that virtualenv is handled correctly
tchn.override_toolchain = False
sys.prefix = '/some/venv'
env = dict(os.environb)
env[b'PATH'] = b'/some/venv/bin:other/things'
tchn.path = '/my/path'
diff, diff_path = self.call_make_environment(tchn, False, env)
self.assertIn(b'PATH', diff)
self.assertEqual([b'/some/venv/bin', b'/my/path', b'other/things'],
diff_path)
self.assertEqual(
{b'CROSS_COMPILE': b'aarch64-linux-', b'LC_ALL': b'C',
b'PATH': b'/my/path'}, diff)
# Handle a toolchain wrapper
tchn.path = ''
bsettings.add_section('toolchain-wrapper')
bsettings.set_item('toolchain-wrapper', 'my-wrapper', 'fred')
diff = self.call_make_environment(tchn, full_path=True)[0]
self.assertEqual(
{b'CROSS_COMPILE': b'fred aarch64-linux-', b'LC_ALL': b'C'}, diff)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@@ -172,13 +172,14 @@ class Toolchain:
else: else:
raise ValueError('Unknown arg to GetEnvArgs (%d)' % which) raise ValueError('Unknown arg to GetEnvArgs (%d)' % which)
def MakeEnvironment(self, full_path): def MakeEnvironment(self, full_path, env=None):
"""Returns an environment for using the toolchain. """Returns an environment for using the toolchain.
This takes the current environment and adds CROSS_COMPILE so that This takes the current environment and adds CROSS_COMPILE so that
the tool chain will operate correctly. This also disables localized the tool chain will operate correctly. This also disables localized
output and possibly Unicode encoded output of all build tools by output and possibly Unicode encoded output of all build tools by
adding LC_ALL=C. adding LC_ALL=C. For the case where full_path is False, it prepends
the toolchain to PATH
Note that os.environb is used to obtain the environment, since in some Note that os.environb is used to obtain the environment, since in some
cases the environment many contain non-ASCII characters and we see cases the environment many contain non-ASCII characters and we see
@@ -187,15 +188,21 @@ class Toolchain:
UnicodeEncodeError: 'utf-8' codec can't encode characters in position UnicodeEncodeError: 'utf-8' codec can't encode characters in position
569-570: surrogates not allowed 569-570: surrogates not allowed
When running inside a Python venv, care is taken not to put the
toolchain path before the venv path, so that builds initiated by
buildman will still respect the venv.
Args: Args:
full_path: Return the full path in CROSS_COMPILE and don't set full_path: Return the full path in CROSS_COMPILE and don't set
PATH PATH
env (dict of bytes): Original environment, used for testing
Returns: Returns:
Dict containing the (bytes) environment to use. This is based on the Dict containing the (bytes) environment to use. This is based on the
current environment, with changes as needed to CROSS_COMPILE, PATH current environment, with changes as needed to CROSS_COMPILE, PATH
and LC_ALL. and LC_ALL.
""" """
env = dict(os.environb) env = dict(env or os.environb)
wrapper = self.GetWrapper() wrapper = self.GetWrapper()
if self.override_toolchain: if self.override_toolchain:
@@ -206,7 +213,23 @@ class Toolchain:
wrapper + os.path.join(self.path, self.cross)) wrapper + os.path.join(self.path, self.cross))
else: else:
env[b'CROSS_COMPILE'] = tools.to_bytes(wrapper + self.cross) env[b'CROSS_COMPILE'] = tools.to_bytes(wrapper + self.cross)
env[b'PATH'] = tools.to_bytes(self.path) + b':' + env[b'PATH']
# Detect a Python virtualenv and avoid defeating it
if sys.prefix != sys.base_prefix:
paths = env[b'PATH'].split(b':')
new_paths = []
to_insert = tools.to_bytes(self.path)
insert_after = tools.to_bytes(sys.prefix)
for path in paths:
new_paths.append(path)
if to_insert and path.startswith(insert_after):
new_paths.append(to_insert)
to_insert = None
if to_insert:
new_paths.append(to_insert)
env[b'PATH'] = b':'.join(new_paths)
else:
env[b'PATH'] = tools.to_bytes(self.path) + b':' + env[b'PATH']
env[b'LC_ALL'] = b'C' env[b'LC_ALL'] = b'C'