1#!/usr/bin/env python 2 3""" 4Botan install script 5 6(C) 2014,2015,2017 Jack Lloyd 7 8Botan is released under the Simplified BSD License (see license.txt) 9""" 10 11import errno 12import json 13import logging 14import optparse # pylint: disable=deprecated-module 15import os 16import shutil 17import sys 18import traceback 19 20def parse_command_line(args): 21 22 parser = optparse.OptionParser() 23 24 parser.add_option('--verbose', action='store_true', default=False, 25 help='Show debug messages') 26 parser.add_option('--quiet', action='store_true', default=False, 27 help='Show only warnings and errors') 28 29 build_group = optparse.OptionGroup(parser, 'Source options') 30 build_group.add_option('--build-dir', metavar='DIR', default='build', 31 help='Location of build output (default \'%default\')') 32 parser.add_option_group(build_group) 33 34 install_group = optparse.OptionGroup(parser, 'Installation options') 35 install_group.add_option('--prefix', default='/usr/local', 36 help='Set output directory (default %default)') 37 install_group.add_option('--bindir', default='bin', metavar='DIR', 38 help='Set binary subdir (default %default)') 39 install_group.add_option('--libdir', default='lib', metavar='DIR', 40 help='Set library subdir (default %default)') 41 install_group.add_option('--includedir', default='include', metavar='DIR', 42 help='Set include subdir (default %default)') 43 install_group.add_option('--docdir', default='share/doc', metavar='DIR', 44 help='Set documentation subdir (default %default)') 45 install_group.add_option('--pkgconfigdir', default='pkgconfig', metavar='DIR', 46 help='Set pkgconfig subdir (default %default)') 47 48 install_group.add_option('--umask', metavar='MASK', default='022', 49 help='Umask to set (default %default)') 50 parser.add_option_group(install_group) 51 52 (options, args) = parser.parse_args(args) 53 54 def log_level(): 55 if options.verbose: 56 return logging.DEBUG 57 if options.quiet: 58 return logging.WARNING 59 return logging.INFO 60 61 logging.getLogger().setLevel(log_level()) 62 63 return (options, args) 64 65 66class PrependDestdirError(Exception): 67 pass 68 69 70def is_subdir(path, subpath): 71 return os.path.relpath(path, start=subpath).startswith("..") 72 73 74def prepend_destdir(path): 75 """ 76 Needed because os.path.join() discards the first path if the 77 second one is absolute, which is usually the case here. Still, we 78 want relative paths to work and leverage the os awareness of 79 os.path.join(). 80 """ 81 destdir = os.environ.get('DESTDIR', "") 82 83 if destdir: 84 # DESTDIR is non-empty, but we only join absolute paths on UNIX-like file systems 85 if os.path.sep != "/": 86 raise PrependDestdirError("Only UNIX-like file systems using forward slash " \ 87 "separator supported when DESTDIR is set.") 88 if not os.path.isabs(path): 89 raise PrependDestdirError("--prefix must be an absolute path when DESTDIR is set.") 90 91 path = os.path.normpath(path) 92 # Remove / or \ prefixes if existent to accomodate for os.path.join() 93 path = path.lstrip(os.path.sep) 94 path = os.path.join(destdir, path) 95 96 if not is_subdir(destdir, path): 97 raise PrependDestdirError("path escapes DESTDIR (path='%s', destdir='%s')" % (path, destdir)) 98 99 return path 100 101 102def makedirs(dirname, exist_ok=True): 103 try: 104 logging.debug('Creating directory %s' % (dirname)) 105 os.makedirs(dirname) 106 except OSError as e: 107 if e.errno != errno.EEXIST or not exist_ok: 108 raise e 109 110# Clear link and create new one 111def force_symlink(target, linkname): 112 try: 113 os.unlink(linkname) 114 except OSError as e: 115 if e.errno != errno.ENOENT: 116 raise e 117 os.symlink(target, linkname) 118 119def calculate_exec_mode(options): 120 out = 0o777 121 if 'umask' in os.__dict__: 122 umask = int(options.umask, 8) 123 logging.debug('Setting umask to %s' % oct(umask)) 124 os.umask(int(options.umask, 8)) 125 out &= (umask ^ 0o777) 126 return out 127 128def main(args): 129 # pylint: disable=too-many-locals,too-many-branches,too-many-statements 130 131 logging.basicConfig(stream=sys.stdout, 132 format='%(levelname) 7s: %(message)s') 133 134 (options, args) = parse_command_line(args) 135 136 exe_mode = calculate_exec_mode(options) 137 138 def copy_file(src, dst): 139 logging.debug('Copying %s to %s' % (src, dst)) 140 shutil.copyfile(src, dst) 141 142 def copy_executable(src, dst): 143 copy_file(src, dst) 144 logging.debug('Make %s executable' % dst) 145 os.chmod(dst, exe_mode) 146 147 with open(os.path.join(options.build_dir, 'build_config.json')) as f: 148 cfg = json.load(f) 149 150 ver_major = int(cfg['version_major']) 151 ver_minor = int(cfg['version_minor']) 152 ver_patch = int(cfg['version_patch']) 153 target_os = cfg['os'] 154 build_shared_lib = bool(cfg['build_shared_lib']) 155 build_static_lib = bool(cfg['build_static_lib']) 156 build_cli = bool(cfg['build_cli_exe']) 157 out_dir = cfg['out_dir'] 158 159 bin_dir = options.bindir 160 lib_dir = options.libdir 161 target_include_dir = os.path.join(options.prefix, 162 options.includedir, 163 'botan-%d' % (ver_major), 164 'botan') 165 166 for d in [options.prefix, lib_dir, bin_dir, target_include_dir]: 167 makedirs(prepend_destdir(d)) 168 169 build_include_dir = os.path.join(options.build_dir, 'include', 'botan') 170 171 for include in sorted(os.listdir(build_include_dir)): 172 if include == 'internal': 173 continue 174 copy_file(os.path.join(build_include_dir, include), 175 prepend_destdir(os.path.join(target_include_dir, include))) 176 177 build_external_include_dir = os.path.join(options.build_dir, 'include', 'external') 178 179 for include in sorted(os.listdir(build_external_include_dir)): 180 copy_file(os.path.join(build_external_include_dir, include), 181 prepend_destdir(os.path.join(target_include_dir, include))) 182 183 if build_static_lib or target_os == 'windows': 184 static_lib = cfg['static_lib_name'] 185 copy_file(os.path.join(out_dir, static_lib), 186 prepend_destdir(os.path.join(lib_dir, os.path.basename(static_lib)))) 187 188 if build_shared_lib: 189 if target_os == "windows": 190 libname = cfg['libname'] 191 soname_base = libname + '.dll' 192 copy_executable(os.path.join(out_dir, soname_base), 193 prepend_destdir(os.path.join(bin_dir, soname_base))) 194 else: 195 soname_patch = cfg['soname_patch'] 196 soname_abi = cfg['soname_abi'] 197 soname_base = cfg['soname_base'] 198 199 copy_executable(os.path.join(out_dir, soname_patch), 200 prepend_destdir(os.path.join(lib_dir, soname_patch))) 201 202 if target_os != "openbsd": 203 prev_cwd = os.getcwd() 204 try: 205 os.chdir(prepend_destdir(lib_dir)) 206 force_symlink(soname_patch, soname_abi) 207 force_symlink(soname_patch, soname_base) 208 finally: 209 os.chdir(prev_cwd) 210 211 if build_cli: 212 copy_executable(cfg['cli_exe'], prepend_destdir(os.path.join(bin_dir, cfg['cli_exe_name']))) 213 214 if 'botan_pkgconfig' in cfg: 215 pkgconfig_dir = os.path.join(options.prefix, options.libdir, options.pkgconfigdir) 216 makedirs(prepend_destdir(pkgconfig_dir)) 217 copy_file(cfg['botan_pkgconfig'], 218 prepend_destdir(os.path.join(pkgconfig_dir, os.path.basename(cfg['botan_pkgconfig'])))) 219 220 if 'ffi' in cfg['mod_list'] and cfg['build_shared_lib'] is True and cfg['install_python_module'] is True: 221 for ver in cfg['python_version'].split(','): 222 py_lib_path = os.path.join(lib_dir, 'python%s' % (ver), 'site-packages') 223 logging.debug('Installing python module to %s' % (py_lib_path)) 224 makedirs(prepend_destdir(py_lib_path)) 225 226 py_dir = cfg['python_dir'] 227 228 copy_file(os.path.join(py_dir, 'botan2.py'), 229 prepend_destdir(os.path.join(py_lib_path, 'botan2.py'))) 230 231 if cfg['with_documentation']: 232 target_doc_dir = os.path.join(options.prefix, options.docdir, 233 'botan-%d.%d.%d' % (ver_major, ver_minor, ver_patch)) 234 235 shutil.rmtree(prepend_destdir(target_doc_dir), True) 236 shutil.copytree(cfg['doc_output_dir'], prepend_destdir(target_doc_dir)) 237 238 copy_file(os.path.join(cfg['base_dir'], 'license.txt'), 239 prepend_destdir(os.path.join(target_doc_dir, 'license.txt'))) 240 copy_file(os.path.join(cfg['base_dir'], 'news.rst'), 241 prepend_destdir(os.path.join(target_doc_dir, 'news.txt'))) 242 for f in [f for f in os.listdir(cfg['doc_dir']) if f.endswith('.txt')]: 243 copy_file(os.path.join(cfg['doc_dir'], f), prepend_destdir(os.path.join(target_doc_dir, f))) 244 245 if cfg['with_rst2man']: 246 man1_dir = prepend_destdir(os.path.join(options.prefix, os.path.join(cfg['mandir'], 'man1'))) 247 makedirs(man1_dir) 248 249 copy_file(os.path.join(cfg['build_dir'], 'botan.1'), 250 os.path.join(man1_dir, 'botan.1')) 251 252 logging.info('Botan %s installation complete', cfg['version']) 253 return 0 254 255if __name__ == '__main__': 256 try: 257 sys.exit(main(sys.argv)) 258 except Exception as e: # pylint: disable=broad-except 259 logging.error('Failure: %s' % (e)) 260 logging.info(traceback.format_exc()) 261 sys.exit(1) 262