1#!/usr/bin/python2 2# 3# Please keep this code python 2.4 compatible and standalone. 4 5""" 6Fetch, build and install external Python library dependancies. 7 8This fetches external python libraries, builds them using your host's 9python and installs them under our own autotest/site-packages/ directory. 10 11Usage? Just run it. 12 utils/build_externals.py 13""" 14 15import argparse 16import compileall 17import logging 18import os 19import sys 20 21import common 22from autotest_lib.client.common_lib import logging_config, logging_manager 23from autotest_lib.client.common_lib import utils 24from autotest_lib.utils import external_packages 25 26# bring in site packages as well 27utils.import_site_module(__file__, 'autotest_lib.utils.site_external_packages') 28 29# Where package source be fetched to relative to the top of the autotest tree. 30PACKAGE_DIR = 'ExternalSource' 31 32# Where packages will be installed to relative to the top of the autotest tree. 33INSTALL_DIR = 'site-packages' 34 35# Installs all packages, even if the system already has the version required 36INSTALL_ALL = False 37 38 39# Want to add more packages to fetch, build and install? See the class 40# definitions at the end of external_packages.py for examples of how to do it. 41 42 43class BuildExternalsLoggingConfig(logging_config.LoggingConfig): 44 """Logging manager config.""" 45 46 def configure_logging(self, results_dir=None, verbose=False): 47 """Configure logging.""" 48 super(BuildExternalsLoggingConfig, self).configure_logging( 49 use_console=True, 50 verbose=verbose) 51 52 53def main(): 54 """ 55 Find all ExternalPackage classes defined in this file and ask them to 56 fetch, build and install themselves. 57 """ 58 options = parse_arguments(sys.argv[1:]) 59 logging_manager.configure_logging(BuildExternalsLoggingConfig(), 60 verbose=True) 61 os.umask(022) 62 63 top_of_tree = external_packages.find_top_of_autotest_tree() 64 package_dir = os.path.join(top_of_tree, PACKAGE_DIR) 65 install_dir = os.path.join(top_of_tree, INSTALL_DIR) 66 67 # Make sure the install_dir is in our python module search path 68 # as well as the PYTHONPATH being used by all our setup.py 69 # install subprocesses. 70 if install_dir not in sys.path: 71 sys.path.insert(0, install_dir) 72 env_python_path_varname = 'PYTHONPATH' 73 env_python_path = os.environ.get(env_python_path_varname, '') 74 if install_dir+':' not in env_python_path: 75 os.environ[env_python_path_varname] = ':'.join([ 76 install_dir, env_python_path]) 77 78 fetched_packages, fetch_errors = fetch_necessary_packages( 79 package_dir, install_dir, set(options.names_to_check)) 80 install_errors = build_and_install_packages(fetched_packages, install_dir, 81 options.use_chromite_master) 82 83 # Byte compile the code after it has been installed in its final 84 # location as .pyc files contain the path passed to compile_dir(). 85 # When printing exception tracebacks, python uses that path first to look 86 # for the source code before checking the directory of the .pyc file. 87 # Don't leave references to our temporary build dir in the files. 88 logging.info('compiling .py files in %s to .pyc', install_dir) 89 compileall.compile_dir(install_dir, quiet=True) 90 91 # Some things install with whacky permissions, fix that. 92 external_packages.system("chmod -R a+rX '%s'" % install_dir) 93 94 errors = fetch_errors + install_errors 95 for error_msg in errors: 96 logging.error(error_msg) 97 98 if not errors: 99 logging.info("Syntax errors from pylint above are expected, not " 100 "problematic. SUCCESS.") 101 else: 102 logging.info("Problematic errors encountered. FAILURE.") 103 return len(errors) 104 105 106def parse_arguments(args): 107 """Parse command line arguments. 108 109 @param args: The command line arguments to parse. (ususally sys.argsv[1:]) 110 111 @returns An argparse.Namespace populated with argument values. 112 """ 113 parser = argparse.ArgumentParser( 114 description='Command to build third party dependencies required ' 115 'for autotest.') 116 parser.add_argument('--use_chromite_master', action='store_true', 117 help='Update chromite to master branch, rather than ' 118 'prod.') 119 parser.add_argument('--names_to_check', nargs='*', type=str, default=set(), 120 help='Package names to check whether they are needed ' 121 'in current system.') 122 return parser.parse_args(args) 123 124 125def fetch_necessary_packages(dest_dir, install_dir, names_to_check=set()): 126 """ 127 Fetches all ExternalPackages into dest_dir. 128 129 @param dest_dir: Directory the packages should be fetched into. 130 @param install_dir: Directory where packages will later installed. 131 @param names_to_check: A set of package names to check whether they are 132 needed on current system. Default is empty. 133 134 @returns A tuple containing two lists: 135 * A list of ExternalPackage instances that were fetched and 136 need to be installed. 137 * A list of error messages for any failed fetches. 138 """ 139 errors = [] 140 fetched_packages = [] 141 for package_class in external_packages.ExternalPackage.subclasses: 142 package = package_class() 143 if names_to_check and package.name.lower() not in names_to_check: 144 continue 145 if not package.is_needed(install_dir): 146 logging.info('A new %s is not needed on this system.', 147 package.name) 148 if INSTALL_ALL: 149 logging.info('Installing anyways...') 150 else: 151 continue 152 if not package.fetch(dest_dir): 153 msg = 'Unable to download %s' % package.name 154 logging.error(msg) 155 errors.append(msg) 156 else: 157 fetched_packages.append(package) 158 159 return fetched_packages, errors 160 161 162def build_and_install_packages(packages, install_dir, 163 use_chromite_master=True): 164 """ 165 Builds and installs all packages into install_dir. 166 167 @param packages - A list of already fetched ExternalPackage instances. 168 @param install_dir - Directory the packages should be installed into. 169 @param use_chromite_master: True if updating chromite to master branch. Due 170 to the non-usage of origin/prod tag, the default 171 value for this argument has been set to True. 172 This argument has not been removed for backward 173 compatibility. 174 175 @returns A list of error messages for any installs that failed. 176 """ 177 errors = [] 178 for package in packages: 179 if package.name.lower() == 'chromiterepo': 180 if not use_chromite_master: 181 logging.warning( 182 'Even though use_chromite_master has been set to %s, it ' 183 'will be checked out to master as the origin/prod tag ' 184 'carries little value now.', use_chromite_master) 185 logging.info('Checking out autotest-chromite to master branch.') 186 result = package.build_and_install( 187 install_dir, master_branch=True) 188 else: 189 result = package.build_and_install(install_dir) 190 if isinstance(result, bool): 191 success = result 192 message = None 193 else: 194 success = result[0] 195 message = result[1] 196 if not success: 197 msg = ('Unable to build and install %s.\nError: %s' % 198 (package.name, message)) 199 logging.error(msg) 200 errors.append(msg) 201 return errors 202 203 204if __name__ == '__main__': 205 sys.exit(main()) 206