blob: fc87c56a9d17a83844967409dab211dc9a704585 [file] [log] [blame]
# Copyright 2018 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Sysroot service."""
import logging
import glob
import multiprocessing
import os
import shutil
import tempfile
from typing import List, NamedTuple
import urllib
from chromite.lib import build_target_lib
from chromite.lib import cache
from chromite.lib import chroot_lib
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import osutils
from chromite.lib import portage_util
from chromite.lib import sysroot_lib
from chromite.lib import workon_helper
from chromite.utils import metrics
class Error(Exception):
"""Base error class for the module."""
class NoFilesError(Error):
"""When there are no files to archive."""
class InvalidArgumentsError(Error):
"""Invalid arguments."""
class NotInChrootError(Error):
"""When SetupBoard is run outside of the chroot."""
class UpdateChrootError(Error):
"""Error occurred when running update chroot."""
class SetupBoardRunConfig(object):
"""Value object for full setup board run configurations."""
def __init__(self, set_default=False, force=False, usepkg=True, jobs=None,
regen_configs=False, quiet=False, update_toolchain=True,
upgrade_chroot=True, init_board_pkgs=True, local_build=False,
toolchain_changed=False, package_indexes=None,
expanded_binhost_inheritance: bool = False):
"""Initialize method.
set_default (bool): Whether to set the passed board as the default.
force (bool): Force a new sysroot creation when it already exists.
usepkg (bool): Whether to use binary packages to bootstrap.
jobs (int): Max number of simultaneous packages to build.
regen_configs (bool): Whether to only regen the configs.
quiet (bool): Whether to print notification when sysroot exists.
update_toolchain (bool): Update the toolchain?
upgrade_chroot (bool): Upgrade the chroot before building?
init_board_pkgs (bool): Emerging packages to sysroot?
local_build (bool): Bootstrap only from local packages?
toolchain_changed (bool): Has a toolchain change occurred? Implies
package_indexes (list[PackageIndexInfo]): List of information about
available prebuilts, youngest first, or None.
expanded_binhost_inheritance: Allow expanded binhost inheritance.
self.set_default = set_default
self.force = force or toolchain_changed
self.usepkg = usepkg = jobs
self.regen_configs = regen_configs
self.quiet = quiet
self.update_toolchain = update_toolchain
self.update_chroot = upgrade_chroot
self.init_board_pkgs = init_board_pkgs
self.local_build = local_build
self.package_indexes = package_indexes or []
self.expanded_binhost_inheritance = expanded_binhost_inheritance
def GetUpdateChrootArgs(self):
"""Create a list containing the relevant update_chroot arguments.
args = []
if self.usepkg:
args += ['--usepkg']
args += ['--nousepkg']
args += ['--jobs', str(]
if not self.update_toolchain:
args += ['--skip_toolchain_update']
return args
class BuildPackagesRunConfig(object):
"""Value object to hold build packages run configs."""
def __init__(self,
expanded_binhosts: bool = False,
setup_board: bool = True,
dryrun: bool = False):
"""Init method.
usepkg (bool): Whether to use binpkgs or build from source. False
currently triggers a local build, which will enable local reuse.
install_debug_symbols (bool): Whether to include the debug symbols for all
packages (list[str]|None): The list of packages to install, by default
install all packages for the target.
use_flags (list[str]|None): A list of use flags to set.
use_goma (bool): Whether to enable goma.
incremental_build (bool): Whether to treat the build as an incremental
build or a fresh build. Always treating it as an incremental build is
safe, but certain operations can be faster when we know we are doing
a fresh build.
package_indexes (list[PackageIndexInfo]): List of information about
available prebuilts, youngest first, or None.
expanded_binhosts: Whether to enable/disable the expanded binhost
inheritance feature for the sysroot.
setup_board: Whether to run setup_board in build_packages.
dryrun: Whether to do a dryrun and not actually build any packages.
self.usepkg = usepkg
self.install_debug_symbols = install_debug_symbols
self.packages = packages
self.use_flags = use_flags
self.use_goma = use_goma
self.is_incremental = incremental_build
self.package_indexes = package_indexes or []
self.expanded_binhosts = expanded_binhosts
self.setup_board = setup_board
self.dryrun = dryrun
def GetBuildPackagesArgs(self):
"""Get the build_packages script arguments."""
# Defaults for the builder.
# TODO(saklein): Parametrize/rework the defaults when build_packages is
# ported to chromite.
args = [
if not self.usepkg:
if self.install_debug_symbols:
if self.use_goma:
if not self.is_incremental:
if self.expanded_binhosts:
if not self.setup_board:
if self.packages:
if self.dryrun:
return args
def HasUseFlags(self):
"""Check if we have use flags."""
return bool(self.use_flags)
def GetUseFlags(self):
"""Get the use flags as a single string."""
use_flags = self.use_flags
if use_flags:
# We have use flags to set, but we need to append them to any existing
# use flags rather than overwrite them completely.
# TODO(saklein) Add config for whether to extend or overwrite?
existing_flags = os.environ.get('USE', '').split()
use_flags = existing_flags
return ' '.join(use_flags)
return None
def GetEnv(self):
"""Get the env from this config."""
env = {}
if self.HasUseFlags():
env['USE'] = self.GetUseFlags()
if self.use_goma:
env['USE_GOMA'] = 'true'
if self.package_indexes:
env['PORTAGE_BINHOST'] = ' '.join(
x.location for x in reversed(self.package_indexes))
return env
def SetupBoard(target, accept_licenses=None, run_configs=None):
"""Run the full process to setup a board's sysroot.
This is the entry point to run the setup_board script.
target (build_target_lib.BuildTarget): The build target configuration.
accept_licenses (str|None): The additional licenses to accept.
run_configs (SetupBoardRunConfig): The run configs.
sysroot_lib.ToolchainInstallError when the toolchain fails to install.
if not cros_build_lib.IsInsideChroot():
# TODO(saklein) switch to build out command and run inside chroot.
raise NotInChrootError('SetupBoard must be run from inside the chroot')
# Make sure we have valid run configs setup.
run_configs = run_configs or SetupBoardRunConfig()
sysroot = Create(target, run_configs, accept_licenses)
if run_configs.regen_configs:
# We're now done if we're only regenerating the configs.
InstallToolchain(target, sysroot, run_configs)
def Create(target, run_configs, accept_licenses):
"""Create a sysroot.
This entry point is the subset of the full setup process that does the
creation and configuration of a sysroot, including installing portage.
target (build_target.BuildTarget): The build target being installed in the
sysroot being created.
run_configs (SetupBoardRunConfig): The run configs.
accept_licenses (str|None): The additional licenses to accept.
sysroot = sysroot_lib.Sysroot(target.root)
if sysroot.Exists() and not run_configs.force and not run_configs.quiet:
logging.warning('Board output directory already exists: %s\n'
'Use --force to clobber the board root and start again.',
# Override regen_configs setting to force full setup run if the sysroot does
# not exist.
run_configs.regen_configs = run_configs.regen_configs and sysroot.Exists()
# Make sure the chroot is fully up to date before we start unless the
# chroot update is explicitly disabled.
if run_configs.update_chroot:'Updating chroot.')
update_chroot = [os.path.join(constants.CROSUTILS_DIR, 'update_chroot'),
update_chroot += run_configs.GetUpdateChrootArgs()
except cros_build_lib.RunCommandError:
raise UpdateChrootError('Error occurred while updating the chroot. '
'See the logs for more information.')
# Delete old sysroot to force a fresh start if requested.
if sysroot.Exists() and run_configs.force:
# Step 1: Create folders.
# Dependencies: None.
# Create the skeleton.'Creating sysroot directories.')
# Step 2: Standalone configurations.
# Dependencies: Folders exist.
# Install main, board setup, and user make.conf files.'Installing configurations into sysroot.')
_InstallConfigs(sysroot, target)
# Step 3: Portage configurations.
# Dependencies: make.conf.board_setup.
# Create the command wrappers, choose profile, and make.conf.board.
# Refresh the workon symlinks to compensate for'Setting up portage in the sysroot.')
# Developer Experience Step: Set default board (if requested) to allow
# running later commands without needing to pass the --board argument.
if run_configs.set_default:
return sysroot
def GenerateArchive(output_dir, build_target_name, pkg_list):
"""Generate a sysroot tarball for informational builders.
output_dir (string): Directory to contain the created the sysroot.
build_target_name (string): The build target for the sysroot being created.
pkg_list (list[string]|None): List of 'category/package' package strings.
Path to the sysroot tar file.
cmd = ['cros_generate_sysroot',
'--out-file', constants.TARGET_SYSROOT_TAR,
'--out-dir', output_dir,
'--board', build_target_name,
'--package', ' '.join(pkg_list)], cwd=constants.SOURCE_ROOT)
return os.path.join(output_dir, constants.TARGET_SYSROOT_TAR)
def CreateSimpleChromeSysroot(chroot, _sysroot_class, build_target, output_dir):
"""Create a sysroot for SimpleChrome to use.
chroot: The chroot class used for these artifacts.
sysroot_class: The sysroot class used for these artifacts.
build_target: The build target used for these artifacts.
output_dir: The path to write artifacts to.
Path to the sysroot tar file.
cmd = ['cros_generate_sysroot', '--out-dir', '/tmp', '--board',, '--deps-only', '--package', constants.CHROME_CP], cwd=constants.SOURCE_ROOT, enter_chroot=True,
chroot_args=chroot.get_enter_args(), extra_env=chroot.env)
# Move the artifact out of the chroot.
sysroot_tar_path = os.path.join(
chroot.path, os.path.join('tmp', constants.CHROME_SYSROOT_TAR))
shutil.copy(sysroot_tar_path, output_dir)
return os.path.join(output_dir, constants.CHROME_SYSROOT_TAR)
def CreateChromeEbuildEnv(chroot, sysroot_class, _build_target, output_dir):
"""Generate Chrome ebuild environment.
chroot: The chroot class used for these artifacts.
sysroot_class (sysroot_lib.Sysroot): The sysroot where the original
environment archive can be found.
output_dir (str): Where the result should be stored.
str: The path to the archive, or None.
pkg_dir = chroot.full_path(sysroot_class.path, portage_util.VDB_PATH)
files = glob.glob(os.path.join(pkg_dir, constants.CHROME_CP) + '-*')
if not files:
logging.warning('No package found for %s', constants.CHROME_CP)
return None
if len(files) > 1:
logging.warning('Expected one package for %s, found %d',
constants.CHROME_CP, len(files))
chrome_dir = sorted(files)[-1]
env_bzip = os.path.join(chrome_dir, 'environment.bz2')
result_path = os.path.join(output_dir, constants.CHROME_ENV_TAR)
with osutils.TempDir() as tempdir:
# Convert from bzip2 to tar format.
bzip2 = cros_build_lib.FindCompressor(cros_build_lib.COMP_BZIP2)
tempdir_tar_path = os.path.join(tempdir, constants.CHROME_ENV_FILE)[bzip2, '-d', env_bzip, '-c'],
cros_build_lib.CreateTarball(result_path, tempdir)
return result_path
def InstallToolchain(target, sysroot, run_configs):
"""Update the toolchain to a sysroot.
This entry point just installs the target's toolchain into the sysroot.
Everything else must have been done already for this to be successful.
target (build_target_lib.BuildTarget): The target whose toolchain is being
sysroot (sysroot_lib.Sysroot): The sysroot where the toolchain is being
run_configs (SetupBoardRunConfig): The run configs.
if not sysroot.Exists():
# Sanity check before we try installing anything.
raise ValueError('The sysroot must exist, run Create first.')
# Step 4: Install toolchain and packages.
# Dependencies: Portage configs and wrappers have been installed.
if run_configs.init_board_pkgs:'Updating toolchain.')
# Use the local packages if we're doing a local only build or usepkg is set.
local_init = run_configs.usepkg or run_configs.local_build
_InstallToolchain(sysroot, target, local_init=local_init)
def BuildPackages(target, sysroot, run_configs):
"""Build and install packages into a sysroot.
target (build_target_lib.BuildTarget): The target whose packages are being
sysroot (sysroot_lib.Sysroot): The sysroot where the packages are being
run_configs (BuildPackagesRunConfig): The run configs.
cmd = [os.path.join(constants.CROSUTILS_DIR, 'build_packages'),
'--board',, '--board_root', sysroot.path]
cmd += run_configs.GetBuildPackagesArgs()
extra_env = run_configs.GetEnv()
extra_env['USE_NEW_PARALLEL_EMERGE'] = '1'
with osutils.TempDir() as tempdir:
extra_env[constants.CROS_METRICS_DIR_ENVVAR] = tempdir
# REVIEW: discuss which dimensions to flatten into the metric
# name other than
with metrics.timer('service.sysroot.BuildPackages.RunCommand'):, extra_env=extra_env)
except cros_build_lib.RunCommandError as e:
failed_pkgs = portage_util.ParseDieHookStatusFile(tempdir)
raise sysroot_lib.PackageInstallError(
str(e), e.result, exception=e, packages=failed_pkgs)
def _CreateSysrootSkeleton(sysroot):
"""Create the sysroot skeleton.
Dependencies: None.
Creates the sysroot directory structure and installs the portage hooks.
sysroot (sysroot_lib.Sysroot): The sysroot.
def _InstallConfigs(sysroot, target):
"""Install standalone configuration files into the sysroot.
Dependencies: The sysroot exists (i.e. CreateSysrootSkeleton).
Installs the main, board setup, and user make.conf files.
sysroot (sysroot_lib.Sysroot): The sysroot.
target (build_target.BuildTarget): The build target being setup in
the sysroot.
def _InstallPortageConfigs(sysroot,
expanded_binhost_inheritance: bool = False):
"""Install portage wrappers and configurations.
Dependencies: make.conf.board_setup (InstallConfigs).
Create the command wrappers, choose profile, and generate make.conf.board.
Refresh the workon symlinks to compensate for
sysroot (sysroot_lib.Sysroot): The sysroot.
target (build_target.BuildTarget): The build target being installed
in the sysroot.
accept_licenses (str): Additional accepted licenses as a string.
local_build (bool): If the build is a local only build.
package_indexes (list[PackageIndexInfo]): List of information about
available prebuilts, youngest first, or None.
expanded_binhost_inheritance: Whether to allow expanded binhost inheritance.
_ChooseProfile(target, sysroot)
_RefreshWorkonSymlinks(, sysroot)
# Must be done after the profile is chosen or binhosts may be incomplete.
def _InstallToolchain(sysroot, target, local_init=True):
"""Install toolchain and packages.
Dependencies: Portage configs and wrappers have been installed
Install the toolchain and the implicit dependencies.
sysroot (sysroot_lib.Sysroot): The sysroot to install to.
target (build_target.BuildTarget): The build target whose toolchain is
being installed.
local_init (bool): Whether to use local packages to bootstrap implicit
sysroot.UpdateToolchain(, local_init=local_init)
def _RefreshWorkonSymlinks(target, sysroot):
"""Force refresh the workon symlinks.
Create an instance of the WorkonHelper, which will recreate all symlinks
to masked/unmasked packages currently worked on in case the sysroot was
recreated (
This was done with a call to `cros_workon list` in the bash version of
the script, but all we actually need is for the WorkonHelper to be
instantiated since it refreshes the symlinks in its __init__.
target (str): The build target name.
sysroot (sysroot_lib.Sysroot): The board's sysroot.
workon_helper.WorkonHelper(sysroot.path, friendly_name=target)
def _ChooseProfile(target, sysroot):
"""Helper function to execute cros_choose_profile.
TODO(saklein) Refactor cros_choose_profile to avoid needing the run
call here, and by extension this method all together.
target (build_target_lib.BuildTarget): The build target whose profile is
being chosen.
sysroot (sysroot_lib.Sysroot): The sysroot for which the profile is
being chosen.
choose_profile = ['cros_choose_profile', '--board',,
'--board-root', sysroot.path]
if target.profile:
# Chooses base by default, only override when we have a passed param.
choose_profile += ['--profile', target.profile]
try:, print_cmd=False)
except cros_build_lib.RunCommandError as e:
logging.error('Selecting profile failed, removing incomplete board '
raise e
def BundleDebugSymbols(chroot: chroot_lib.Chroot,
sysroot_class: sysroot_lib.Sysroot,
_build_target: build_target_lib.BuildTarget,
output_dir: str) -> str:
"""Bundle debug symbols into a tarball for importing into GCE.
Bundle the debug symbols found in the sysroot into a .tgz. This assumes
these files are present.
chroot: The chroot class used for these artifacts.
sysroot_class: The sysroot class used for these artifacts.
build_target: The build target used for these artifacts.
output_dir: The path to write artifacts to.
A string path to the output debug.tgz artifact, or None.
base_path = chroot.full_path(sysroot_class.path)
debug_dir = os.path.join(base_path, 'usr/lib/debug')
if not os.path.isdir(debug_dir):
logging.error('No debug directory found at %s.', debug_dir)
return None
# Create tarball from destination_tmp, then copy it...
tarball_path = os.path.join(output_dir, constants.DEBUG_SYMBOLS_TAR)
exclude_breakpad_tar_arg = ('--exclude=%s' %
os.path.join(debug_dir, 'breakpad'))
result = None
result = cros_build_lib.CreateTarball(
except cros_build_lib.TarballError:
if not result or result.returncode:
# We don't abort here, because the tar may still be somewhat intact.
err = result.return_code if result else 'TarballError'
logging.error('Error (%s) when creating tarball %s from %s', err,
tarball_path, debug_dir)
if os.path.exists(tarball_path):
return tarball_path
return None
def BundleBreakpadSymbols(chroot: chroot_lib.Chroot,
sysroot_class: sysroot_lib.Sysroot,
build_target: build_target_lib.BuildTarget,
output_dir: str) -> str:
"""Bundle breakpad debug symbols into a tarball for importing into GCE.
Call the GenerateBreakpadSymbols function and archive this into a tar.gz.
chroot: The chroot class used for these artifacts.
sysroot_class: The sysroot class used for these artifacts.
build_target: The build target used for these artifacts.
output_dir: The path to write artifacts to.
A string path to the output debug_breakpad.tar.gz artifact, or None.
base_path = chroot.full_path(sysroot_class.path)
result = GenerateBreakpadSymbols(chroot, build_target, debug=True)
# Verify breakpad symbol generation before gathering the sym files.
if result.returncode:
logging.error('Error (%d) when generating breakpad symbols',
return None
with chroot.tempdir() as symbol_tmpdir, chroot.tempdir() as dest_tmpdir:
breakpad_dir = os.path.join(base_path, 'usr/lib/debug/breakpad')
# Call list on the atifacts.GatherSymbolFiles generator function to
# materialize and consume all entries so that all are copied to
# dest dir and complete list of all symbol files is returned.
sym_file_list = list(
GatherSymbolFiles(tempdir=symbol_tmpdir, destdir=dest_tmpdir,
if not sym_file_list:
logging.warning('No sym files found in %s.', breakpad_dir)
# Create tarball from destination_tmp, then copy it...
tarball_path = os.path.join(output_dir,
result = cros_build_lib.CreateTarball(tarball_path, dest_tmpdir)
if result.returncode != 0:
logging.error('Error (%d) when creating tarball %s from %s',
result.returncode, tarball_path, dest_tmpdir)
return None
return tarball_path
# A SymbolFileTuple is a data object that contains:
# relative_path (str): Relative path to the file based on initial search path.
# source_file_name (str): Full path to where the SymbolFile was found.
# For example, if the search path for symbol files is '/some/bot/path/'
# and a symbol file is found at '/some/bot/path/a/b/c/file1.sym',
# then the relative_path would be 'a/b/c/file1.sym' and the source_file_name
# would be '/some/bot/path/a/b/c/file1.sym'.
# The source_file_name is informational for two reasons:
# 1) They are typically copied off a machine (such as a build bot) where
# that path will disappear, which is why when we find them they get
# copied to a destination directory.
# 2) For tar files, the source_file_name is not a full path that can be
# opened, since it is the path the tar file plus the relative path of
# the file when we untar it.
class SymbolFileTuple(NamedTuple):
"""Contain a relative and full path to a SymbolFile."""
relative_path: str
source_file_name: str
def GenerateBreakpadSymbols(chroot: chroot_lib.Chroot,
build_target: build_target_lib.BuildTarget,
debug: bool) -> cros_build_lib.CommandResult:
"""Generate breakpad (go/breakpad) symbols for debugging.
This function generates .sym files to /build/<board>/usr/lib/debug/breakpad
from .debug files found in /build/<board>/usr/lib/debug by calling
chroot: The chroot in which the sysroot should be built.
build_target: The sysroot's build target.
debug: Include extra debugging output.
# The firmware directory contains elf symbols that we have trouble parsing
# and that don't help with breakpad debugging (see
exclude_dirs = ['firmware']
cmd = [
if debug:
cmd += ['--debug']
# Execute for board in parallel with half # of cpus available to avoid
# starving other parallel processes on the same machine.
cmd += [
'--board=%s' %,
'--jobs', str(max(1, multiprocessing.cpu_count() // 2))
cmd += ['--exclude-dir=%s' % x for x in exclude_dirs]'Generating breakpad symbols: %s.', cmd)
result =
return result
def GatherSymbolFiles(tempdir:str, destdir:str,
paths: List[str]) -> List[SymbolFileTuple]:
"""Locate symbol files in |paths|
This generator function searches all paths for .sym files and copies them to
destdir. A path to a tarball will result in the tarball being unpacked and
examined. A path to a directory will result in the directory being searched
for .sym files. The generator yields SymbolFileTuple objects that contain
symbol file references which are valid after this exits. Those files may exist
externally, or be created in the tempdir (when expanding tarballs). Typical
usage in the BuildAPI will be for the .sym files to exist under a directory
such as /build/<board>/usr/lib/debug/breakpad so that the path to a sym file
will always be unique.
Note: the caller must clean up the tempdir.
Note: this function is recursive for tar files.
tempdir: Path to use for temporary files.
destdir: All .sym files are copied to this path. Tarfiles are opened inside
a tempdir and any .sym files within them are copied to destdir from within
that temp path.
paths: A list of input paths to walk. Files are returned based on .sym name
w/out any checking internal symbol file format.
Dirs are searched for files that end in ".sym". Urls are not handled.
Tarballs are unpacked and walked.
A SymbolFileTuple for every symbol file found in paths.
"""'GatherSymbolFiles tempdir %s destdir %s paths %s',
tempdir, destdir, paths)
for p in paths:
o = urllib.parse.urlparse(p)
if o.scheme:
raise NotImplementedError('URL paths are not expected/handled: ', p)
elif not os.path.exists(p):
raise NoFilesError('The path did not exist: ', p)
elif os.path.isdir(p):
for root, _, files in os.walk(p):
for f in files:
if f.endswith('.sym'):
# If p is '/tmp/foo' and filename is '/tmp/foo/bar/bar.sym',
# relative_path = 'bar/bar.sym'
filename = os.path.join(root, f)
relative_path = filename[len(p):].lstrip('/')
shutil.copy(filename, os.path.join(destdir, relative_path))
except IOError:
# Handles pre-3.3 Python where we may need to make the target
# path's dirname before copying.
os.makedirs(os.path.join(destdir, os.path.dirname(relative_path)))
shutil.copy(filename, os.path.join(destdir, relative_path))
yield SymbolFileTuple(relative_path=relative_path,
elif cros_build_lib.IsTarball(p):
tardir = tempfile.mkdtemp(dir=tempdir)
cache.Untar(os.path.realpath(p), tardir)
for sym in GatherSymbolFiles(tardir, destdir, [tardir]):
# The SymbolFileTuple is generated from [tardir], but we want the
# source_file_name (which informational) to reflect the tar path
# plus the relative path after the file is untarred.
# Thus, something like /botpath/some/path/tmp22dl33sa/dir1/fileB.sym
# (where the tardir is /botpath/some/path/tmp22dl33sa)
# has a resulting path /botpath/some/path/symfiles.tar/dir1/fileB.sym
# When we call GatherSymbolFiles with [tardir] as the argument,
# the os.path.isdir case above will walk the tar contents,
# processing only .sym. Non-sym files within the tar file will be
# ignored (even tar files within tar files, which we don't expect).
new_source_file_name = sym.source_file_name.replace(tardir, p)
yield SymbolFileTuple(
elif os.path.isfile(p):
# Path p is a file. This code path is only executed when a full file path
# is one of the elements in the 'paths' argument. When a directory is an
# element of the 'paths' argument, we walk the tree (above) and process
# each file. When a tarball is an element of the 'paths' argument, we
# untar it into a directory and recurse with the temp tardir as the
# directory, so that tarfile contents are processed (above) in the os.walk
# of the directory.
if p.endswith('.sym'):
shutil.copy(p, destdir)
yield SymbolFileTuple(relative_path=os.path.basename(p),
raise ValueError('Unexpected input to GatherSymbolFiles: ', p)