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 subprocess 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 out_dir = cfg['out_dir'] 157 158 bin_dir = os.path.join(options.prefix, options.bindir) 159 lib_dir = os.path.join(options.prefix, options.libdir) 160 target_include_dir = os.path.join(options.prefix, 161 options.includedir, 162 'encryptmsg', 163 'encryptmsg') 164 165 for d in [options.prefix, lib_dir, bin_dir, target_include_dir]: 166 makedirs(prepend_destdir(d)) 167 168 build_include_dir = os.path.join(options.build_dir, 'include', 'encryptmsg') 169 170 for include in sorted(os.listdir(build_include_dir)): 171 if include == 'internal': 172 continue 173 copy_file(os.path.join(build_include_dir, include), 174 prepend_destdir(os.path.join(target_include_dir, include))) 175 176 build_external_include_dir = os.path.join(options.build_dir, 'include', 'external') 177 178 for include in sorted(os.listdir(build_external_include_dir)): 179 copy_file(os.path.join(build_external_include_dir, include), 180 prepend_destdir(os.path.join(target_include_dir, include))) 181 182 if build_static_lib or target_os == 'windows': 183 static_lib = cfg['static_lib_name'] 184 copy_file(os.path.join(out_dir, static_lib), 185 prepend_destdir(os.path.join(lib_dir, os.path.basename(static_lib)))) 186 187 if build_shared_lib: 188 if target_os == "windows": 189 libname = cfg['libname'] 190 soname_base = libname + '.dll' 191 copy_executable(os.path.join(out_dir, soname_base), 192 prepend_destdir(os.path.join(lib_dir, soname_base))) 193 else: 194 soname_patch = cfg['soname_patch'] 195 soname_abi = cfg['soname_abi'] 196 soname_base = cfg['soname_base'] 197 198 copy_executable(os.path.join(out_dir, soname_patch), 199 prepend_destdir(os.path.join(lib_dir, soname_patch))) 200 201 if target_os != "openbsd": 202 prev_cwd = os.getcwd() 203 try: 204 os.chdir(prepend_destdir(lib_dir)) 205 force_symlink(soname_patch, soname_abi) 206 force_symlink(soname_patch, soname_base) 207 finally: 208 os.chdir(prev_cwd) 209 210 copy_executable(cfg['cli_exe'], prepend_destdir(os.path.join(bin_dir, cfg['cli_exe_name']))) 211 212 # On Darwin, if we are using shared libraries and we install, we should fix 213 # up the library name, otherwise the botan command won't work; ironically 214 # we only need to do this because we previously changed it from a setting 215 # that would be correct for installation to one that lets us run it from 216 # the build directory 217 if target_os == 'darwin' and build_shared_lib: 218 soname_abi = cfg['soname_abi'] 219 220 subprocess.check_call(['install_name_tool', 221 '-change', 222 os.path.join('@executable_path', soname_abi), 223 os.path.join(lib_dir, soname_abi), 224 os.path.join(bin_dir, cfg['cli_exe_name'])]) 225 226 if 'encryptmsg_pkgconfig' in cfg: 227 pkgconfig_dir = os.path.join(options.prefix, options.libdir, options.pkgconfigdir) 228 makedirs(prepend_destdir(pkgconfig_dir)) 229 copy_file(cfg['encryptmsg_pkgconfig'], 230 prepend_destdir(os.path.join(pkgconfig_dir, os.path.basename(cfg['encryptmsg_pkgconfig'])))) 231 232 if 'ffi' in cfg['mod_list']: 233 for ver in cfg['python_version'].split(','): 234 py_lib_path = os.path.join(lib_dir, 'python%s' % (ver), 'site-packages') 235 logging.debug('Installing python module to %s' % (py_lib_path)) 236 makedirs(prepend_destdir(py_lib_path)) 237 238 py_dir = cfg['python_dir'] 239 240 copy_file(os.path.join(py_dir, 'botan2.py'), 241 prepend_destdir(os.path.join(py_lib_path, 'botan2.py'))) 242 243 if cfg['with_documentation']: 244 target_doc_dir = os.path.join(options.prefix, options.docdir, 245 'botan-%d.%d.%d' % (ver_major, ver_minor, ver_patch)) 246 247 shutil.rmtree(prepend_destdir(target_doc_dir), True) 248 shutil.copytree(cfg['doc_output_dir'], prepend_destdir(target_doc_dir)) 249 250 copy_file(os.path.join(cfg['base_dir'], 'license.txt'), 251 prepend_destdir(os.path.join(target_doc_dir, 'license.txt'))) 252 copy_file(os.path.join(cfg['base_dir'], 'news.rst'), 253 prepend_destdir(os.path.join(target_doc_dir, 'news.txt'))) 254 for f in [f for f in os.listdir(cfg['doc_dir']) if f.endswith('.txt')]: 255 copy_file(os.path.join(cfg['doc_dir'], f), prepend_destdir(os.path.join(target_doc_dir, f))) 256 257 if cfg['with_rst2man']: 258 man1_dir = prepend_destdir(os.path.join(options.prefix, os.path.join(cfg['mandir'], 'man1'))) 259 makedirs(man1_dir) 260 261 copy_file(os.path.join(cfg['build_dir'], 'botan.1'), 262 os.path.join(man1_dir, 'botan.1')) 263 264 logging.info('EncryptMsg %s installation complete', cfg['version']) 265 return 0 266 267if __name__ == '__main__': 268 try: 269 sys.exit(main(sys.argv)) 270 except Exception as e: # pylint: disable=broad-except 271 logging.error('Failure: %s' % (e)) 272 import traceback 273 logging.info(traceback.format_exc()) 274 sys.exit(1) 275