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