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