<reponame>castarco/unit-e<gh_stars>10-100
#!/usr/bin/env python3
# Copyright (c) 2018-2019 The Bitcoin Core developers
# Copyright (c) 2019 The Unit-e developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://opensource.org/licenses/MIT.

import argparse
import os
import subprocess
import sys

import platform

import tempfile

from pathlib import Path
from contextlib import contextmanager

OSSLSIGNCODE_VER = '1.7.1'
OSSLSIGNCODE_DIR = 'osslsigncode-'+OSSLSIGNCODE_VER

@contextmanager
def cd(destination_dir):
    original_dir = os.getcwd()
    os.chdir(destination_dir)
    try:
        yield
    finally:
        os.chdir(original_dir)

class Installer:
    """ Wrapper around native package installer, supports apt-get and brew, only
        installs packages which aren't installed yet."""
    def __init__(self, backend=None, quiet=False):
        if backend != "apt" and backend != "brew":
            raise ValueError("Invalid value for backend argument: '%'. Valid values are `apt` and `brew`.")
        self.backend = backend
        self.updated = False
        self.to_install = []
        if quiet and self.backend == "apt":
            self.flags = ['-qq']
        else:
            self.flags = []

    def backend_command(self, subcommand):
        if self.backend == 'apt':
            if subcommand == 'ls':
                command = ['dpkg', '-s']
            else:
                command = ['sudo', 'apt-get', subcommand] + self.flags
        elif self.backend == 'brew':
            command = ['brew', subcommand]
        return command

    def update(self):
        if not self.updated:
            self.updated = True
            subprocess.check_call(self.backend_command('update'))

    def try_to_install(self, *programs):
        self.update()
        print(self.backend + ': installing', ", ".join(programs))
        return subprocess.call(self.backend_command('install') + list(programs)) == 0

    def batch_install(self):
        if not self.to_install:
            print(self.backend + ': nothing to install')
            return

        if not self.try_to_install(*self.to_install):
            print('Could not install packages.', file=sys.stderr)
            exit(1)
        self.to_install = []

    def is_installed(self, program):
        return subprocess.call(self.backend_command('ls') + [program], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0

    def add_requirements(self, *programs):
        for program in programs:
            if not self.is_installed(program):
                self.to_install.append(program)


def verify_user_specified_osslsigncode(user_spec_path):
    if not Path(user_spec_path).is_file():
        raise Exception('provided osslsign does not exists: {}'.format(user_spec_path))

    try:
        if subprocess.call([user_spec_path, '--version'], stderr=subprocess.DEVNULL) != 255:
            raise Exception('cannot execute provided osslsigncode: {}'.format(user_spec_path))
    except subprocess.CalledProcessError as e:
        raise Exception('unexpected exception raised while executing provided osslsigncode: {}'.format(str(e)))

    return Path(user_spec_path).resolve()

def find_osslsigncode(user_spec_path):
    if user_spec_path:
        return verify_user_specified_osslsigncode(user_spec_path)

    which_ossl_proc = subprocess.Popen(['which', 'osslsigncode'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
    if which_ossl_proc.wait() == 0:
        ossl_path = which_ossl_proc.communicate()[0].split()[0].decode()
        if subprocess.call([ossl_path, '--version'], stderr=subprocess.DEVNULL) == 255:
            return ossl_path

    expected_path = Path(OSSLSIGNCODE_DIR, 'osslsigncode')
    if expected_path.is_file() and subprocess.call([expected_path, '--version'], stderr=subprocess.DEVNULL) == 255:
        return expected_path.resolve()

    return None

def install_osslsigner():
    subprocess.check_call(['wget', '-N', 'https://downloads.sourceforge.net/project/osslsigncode/osslsigncode/osslsigncode-'+OSSLSIGNCODE_VER+'.tar.gz'])
    subprocess.check_call(['wget', '-N', 'https://bitcoincore.org/cfields/osslsigncode-Backports-to-'+OSSLSIGNCODE_VER+'.patch'])
    subprocess.check_call(["echo 'a8c4e9cafba922f89de0df1f2152e7be286aba73f78505169bc351a7938dd911 osslsigncode-Backports-to-"+OSSLSIGNCODE_VER+".patch' | sha256sum -c"], shell=True)
    subprocess.check_call(["echo 'f9a8cdb38b9c309326764ebc937cba1523a3a751a7ab05df3ecc99d18ae466c9 osslsigncode-"+OSSLSIGNCODE_VER+".tar.gz' | sha256sum -c"], shell=True)
    subprocess.check_call(['tar', '-xzf', 'osslsigncode-'+OSSLSIGNCODE_VER+'.tar.gz'])
    subprocess.check_call(['patch -p1 < ../osslsigncode-Backports-to-'+OSSLSIGNCODE_VER+'.patch'], shell=True, cwd=OSSLSIGNCODE_DIR)
    subprocess.check_call(['./configure', '--without-gsf', '--without-curl', '--disable-dependency-tracking'], cwd=OSSLSIGNCODE_DIR)
    subprocess.check_call(['make'], cwd=OSSLSIGNCODE_DIR)
    return Path(OSSLSIGNCODE_DIR, 'osslsigncode').resolve()


def install_libssl_dev(apt):
    dist_str = platform.dist()
    dist_type = dist_str[0]
    dist_no = int(dist_str[1].replace('.', ''))
    if dist_type == 'Ubuntu' and dist_no < 1800 or dist_type == 'Debian' and dist_no < 900:
        apt.add_requirements('libssl-dev')
    else:
        apt.add_requirements('libssl1.0-dev')


def install_linux_deps(args):
    apt = Installer(backend='apt', quiet=args.quiet)
    apt.add_requirements('ruby', 'git', 'make')
    if args.kvm:
        apt.add_requirements('apt-cacher-ng', 'python-vm-builder', 'qemu-kvm', 'qemu-utils')
    elif args.docker:
        if subprocess.call(['docker', '--version']) != 0:
            if not apt.try_to_install('docker.io') and not apt.try_to_install('docker-ce'):
                print('Cannot find any way to install docker', file=sys.stderr)
                exit(1)
    else:
        apt.add_requirements('apt-cacher-ng', 'lxc', 'debootstrap')

    should_make_ossl = False
    if args.codesign and args.windows:
        args.osslsigncode_path = find_osslsigncode(args.osslsigncode_path)
        if not args.osslsigncode_path:
            should_make_ossl = True
            apt.add_requirements('tar', 'wget', 'patch', 'autoconf')
            install_libssl_dev(apt)

    apt.batch_install()

    if should_make_ossl:
        args.osslsigncode_path = install_osslsigner()

def create_bin_symlink(link, target):
    bin_path = Path("bin", link)
    if not bin_path.exists():
        bin_path.symlink_to(target)

def install_mac_deps(args):
    if not args.docker:
        print('Mac can only work with docker, re-run with --docker flag.', file=sys.stderr)
        exit(1)

    if subprocess.call(['docker', '--version']) != 0:
        print('Please install docker manually, e.g. with `brew cask install docker`.', file=sys.stderr)
        exit(1)

    brew = Installer(backend='brew')
    brew.add_requirements('ruby', 'coreutils')
    brew.batch_install()

    create_bin_symlink("date", "/usr/local/bin/gdate")
    create_bin_symlink("sha256sum", "/usr/local/bin/gsha256sum")

def install_deps(args):
    system_str = platform.system()
    if system_str == 'Linux':
        install_linux_deps(args)
    elif system_str == 'Darwin':
        install_mac_deps(args)
    else:
        print("Unsupported system '%s'." % system_str, file=sys.stderr)
        exit(1)


def setup(args):
    install_deps(args)

    if not Path('unit-e-sigs').is_dir():
        subprocess.check_call(['git', 'clone', '<EMAIL>:dtr-org/unit-e-sigs.git'])
    if not Path('gitian-builder').is_dir():
        subprocess.check_call(['git', 'clone', 'https://github.com/devrandom/gitian-builder.git'])
    if not Path(args.git_dir).is_dir():
        subprocess.check_call(['git', 'clone', args.url, args.git_dir])
    make_image_prog = ['bin/make-base-vm', '--suite', 'bionic', '--arch', 'amd64']
    if args.docker:
        make_image_prog += ['--docker']
    elif not args.kvm:
        make_image_prog += ['--lxc']
    subprocess.check_call(make_image_prog, cwd='gitian-builder')
    if args.is_bionic and not args.kvm and not args.docker:
        subprocess.check_call(['sudo', 'sed', '-i', 's/lxcbr0/br0/', '/etc/default/lxc-net'])
        print('Reboot is required')
        exit(0)

def gitian_descriptors(args, platform_str):
    return '../work/gitian-descriptors/gitian-' + platform_str + '.yml'

def build(args):
    os.makedirs('unit-e-binaries/' + args.version, exist_ok=True)
    print('\nBuilding Dependencies\n')
    with cd('gitian-builder'):
        os.makedirs('inputs', exist_ok=True)

        subprocess.check_call(['make', '-C', Path('../', args.git_dir, 'depends'), 'download', 'SOURCES_PATH=' + os.getcwd() + '/cache/common'])

        if args.linux:
            print('\nCompiling ' + args.version + ' Linux')
            subprocess.check_call(['bin/gbuild', '-j', args.jobs, '-m', args.memory, '--commit', 'unit-e='+args.commit, '--url', 'unit-e='+args.url, gitian_descriptors(args, 'linux')])
            subprocess.check_call(['bin/gsign', '-p', args.sign_prog, '--signer', args.signer, '--release', args.version+'-linux', '--destination', '../unit-e-sigs/', gitian_descriptors(args, 'linux')])
            subprocess.check_call('mv build/out/unit-e-*.tar.gz build/out/src/unit-e-*.tar.gz ../unit-e-binaries/'+args.version, shell=True)

        if args.windows:
            print('\nCompiling ' + args.version + ' Windows')
            subprocess.check_call(['bin/gbuild', '-j', args.jobs, '-m', args.memory, '--commit', 'unit-e='+args.commit, '--url', 'unit-e='+args.url, gitian_descriptors(args, 'win')])
            subprocess.check_call(['bin/gsign', '-p', args.sign_prog, '--signer', args.signer, '--release', args.version+'-win-unsigned', '--destination', '../unit-e-sigs/', gitian_descriptors(args, 'win')])
            subprocess.check_call('mv build/out/unit-e-*-win-unsigned.tar.gz inputs/', shell=True)
            subprocess.check_call('mv build/out/unit-e-*.zip build/out/unit-e-*.exe ../unit-e-binaries/'+args.version, shell=True)

        if args.macos:
            print('\nCompiling ' + args.version + ' MacOS')
            subprocess.check_call(['bin/gbuild', '-j', args.jobs, '-m', args.memory, '--commit', 'unit-e='+args.commit, '--url', 'unit-e='+args.url, gitian_descriptors(args, 'osx')])
            subprocess.check_call(['bin/gsign', '-p', args.sign_prog, '--signer', args.signer, '--release', args.version+'-osx-unsigned', '--destination', '../unit-e-sigs/', gitian_descriptors(args, 'osx')])
            subprocess.check_call('mv build/out/unit-e-*.tar.gz ../unit-e-binaries/'+args.version, shell=True)

    if args.commit_files:
        print('\nCommitting '+args.version+' Unsigned Sigs\n')
        with cd('unit-e-sigs'):
            if args.linux:
                subprocess.check_call(['git', 'add', args.version+'-linux/'+args.signer])
            if args.windows:
                subprocess.check_call(['git', 'add', args.version+'-win-unsigned/'+args.signer])
            if args.macos:
                subprocess.check_call(['git', 'add', args.version+'-osx-unsigned/'+args.signer])
            subprocess.check_call(['git', 'commit', '-m', 'Add '+args.version+' unsigned sigs for '+args.signer])

def get_signatures_path(platform_str, version):
    return Path('unit-e-sigs', version + '-detached', 'unit-e-'+platform_str+'-signatures.tar.gz').resolve()


def codesign_windows(osslsign_path, version, win_code_cert_path, win_code_key_path):
    gitian_dir = Path('gitian-builder').resolve()
    signatures_tarball = get_signatures_path('win', version)
    if signatures_tarball.is_file():
        print('Signatures already present at:', signatures_tarball, '\nI cowardly refuse to continue', file=sys.stderr)
        exit(1)

    signatures_tarball.parent.mkdir(exist_ok=True)

    print('\nSigning ' + version + ' Windows')
    with tempfile.TemporaryDirectory() as build_dir:
        subprocess.check_call(['cp', 'inputs/unit-e-' + version + '-win-unsigned.tar.gz', Path(build_dir, 'unit-e-win-unsigned.tar.gz')], cwd=gitian_dir)
        subprocess.check_call(['tar', '-xzf', 'unit-e-win-unsigned.tar.gz'], cwd=build_dir)

        for fp in Path(build_dir).glob('unsigned/*.exe'):
            subprocess.check_call([osslsign_path, 'sign', '-certs', win_code_cert_path, '-in', fp, '-out', str(fp)+'-signed', '-key', win_code_key_path, '-askpass'])
            subprocess.check_call([osslsign_path, 'extract-signature', '-pem', '-in', str(fp)+'-signed', '-out', str(fp)+'.pem'])

        subprocess.check_call(['tar', '-czf', signatures_tarball, *[path.name for path in Path(build_dir, 'unsigned').glob('*.exe.pem')]], cwd=Path(build_dir, 'unsigned'))


def codesign(args):
    if not args.windows:
        print('Warning: codesigning requested, but windows not in the --os flag.', file=sys.stderr)
        return

    codesign_windows(args.osslsigncode_path, args.version, args.win_code_cert_path, args.win_code_key_path)

    if args.commit_files:
        print('\nCommitting '+args.version+' Detached Sigs\n')
        subprocess.check_call(['git', 'add', Path(args.version + '-detached', 'unit-e-win-signatures.tar.gz')], cwd='unit-e-sigs')
        subprocess.check_call(['git', 'commit', '-a', '-m', 'Add '+args.version+' detached signatures by '+args.signer], cwd='unit-e-sigs')



def sign(args):
    if not args.windows:
        print('Warning: signing requested, but windows not in the --os flag.', file=sys.stderr)
        return

    gitian_dir = Path('gitian-builder').resolve()

    subprocess.check_call(['wget', '-N', '-P', 'inputs', 'https://downloads.sourceforge.net/project/osslsigncode/osslsigncode/osslsigncode-'+OSSLSIGNCODE_VER+'.tar.gz'], cwd=gitian_dir)
    subprocess.check_call(['wget', '-N', '-P', 'inputs', 'https://bitcoincore.org/cfields/osslsigncode-Backports-to-'+OSSLSIGNCODE_VER+'.patch'], cwd=gitian_dir)

    signatures_tarball = get_signatures_path('win', args.version)
    if not signatures_tarball.is_file():
        print('Signatures not present at:', signatures_tarball, file=sys.stderr)
        exit(1)

    print('\nSigning ' + args.version + ' Windows')
    subprocess.check_call(['cp', signatures_tarball, 'inputs/unit-e-win-signatures.tar.gz'], cwd=gitian_dir)
    subprocess.check_call(['cp', 'inputs/unit-e-' + args.version + '-win-unsigned.tar.gz', 'inputs/unit-e-win-unsigned.tar.gz'], cwd=gitian_dir)
    subprocess.check_call(['bin/gbuild', '-i', '--commit', 'signature=master', gitian_descriptors(args, 'win-signer')], cwd=gitian_dir)
    subprocess.check_call(['bin/gsign', '-p', args.sign_prog, '--signer', args.signer, '--release', args.version+'-win-signed', '--destination', '../unit-e-sigs/', gitian_descriptors(args, 'win-signer')], cwd=gitian_dir)
    subprocess.check_call('mv build/out/unit-e-*win64-setup.exe ../unit-e-binaries/'+args.version, shell=True, cwd=gitian_dir)
    subprocess.check_call('mv build/out/unit-e-*win32-setup.exe ../unit-e-binaries/'+args.version, shell=True, cwd=gitian_dir)

    if args.commit_files:
        print('\nCommitting '+args.version+' Signed Sigs\n')
        subprocess.check_call(['git', 'add', args.version+'-win-signed/'+args.signer], cwd='unit-e-sigs')
        subprocess.check_call(['git', 'commit', '-a', '-m', 'Add '+args.version+' signed binary sigs for '+args.signer], cwd='unit-e-sigs')


def verify(args):
    """ Verify the builds. Exits with error in case any of the signatures fail to verify and if any of the signatures are missing. """
    gitian_builder = Path('gitian-builder')
    sigs_path = Path('unit-e-sigs')

    builds_were_missing = False

    for (sig_path_suffix, descriptor_suffix, build_name) in [
            ('linux', 'linux', 'Linux'),
            ('win-unsigned', 'win', 'Windows'),
            ('osx-unsigned', 'osx', 'MacOS'),
            ('win-signed', 'win-signer', 'Signed Windows'),
    ]:
        build_sig_dir = args.version + '-' + sig_path_suffix
        if Path(sigs_path, build_sig_dir).is_dir():
            print('\nVerifying v{} {}\n'.format(args.version, build_name))
            descriptor = gitian_descriptors(args, descriptor_suffix)
            subprocess.check_call(['bin/gverify', '-v', '-d', '../unit-e-sigs/', '-r', build_sig_dir, descriptor], cwd=gitian_builder)
        else:
            print('\nSkipping v{} {} as it is not present\n'.format(args.version, build_name))
            builds_were_missing = True

    if builds_were_missing:
        print('Some builds were missing, please refer to previous logs.', file=sys.stderr)
        exit(1)


def prepare_git_dir(args):
    with cd(args.git_dir):
        if args.pull:
            subprocess.check_call(['git', 'fetch', args.url, 'refs/pull/'+args.version+'/merge'])
            os.chdir('../gitian-builder/inputs/unit-e')
            subprocess.check_call(['git', 'fetch', args.url, 'refs/pull/'+args.version+'/merge'])
            args.commit = subprocess.check_output(['git', 'show', '-s', '--format=%H', 'FETCH_HEAD'], universal_newlines=True, encoding='utf8').strip()
            args.version = 'pull-' + args.version
        print(args.commit)
        if not args.skip_checkout:
            subprocess.check_call(['git', 'fetch'])
            subprocess.check_call(['git', 'checkout', args.commit])

# Use only keyword-only arguments as defined in PEP 3102 to avoid accidentally swapping of arguments
def prepare_gitian_descriptors(*, source, target, hosts=None):
    descriptor_source_dir = Path(source)
    descriptor_dir = Path(target)
    if not descriptor_source_dir.is_dir():
        raise Exception("Gitian descriptor dir '%s' does not exist" % descriptor_source_dir)
    descriptor_dir.mkdir(parents=True, exist_ok=True)
    for descriptor_path in descriptor_source_dir.glob("*.yml"):
        filename = descriptor_path.relative_to(descriptor_source_dir)
        descriptor_in = descriptor_source_dir / filename
        descriptor_out = descriptor_dir / filename
        with descriptor_out.open("w") as file_out, descriptor_in.open() as file_in:
            for line in file_in:
                if hosts and line.startswith('  HOSTS='):
                    file_out.write('  HOSTS="%s"\n' % hosts)
                else:
                    file_out.write(line)

def create_work_dir(work_dir):
    work_dir = Path(work_dir)
    if Path(work_dir).exists() and not Path(work_dir).is_dir():
        raise Exception("Work dir '%s' exists but is not a directory." % work_dir)
    if not work_dir.exists():
        print("Creating working directory '%s'" % work_dir)
        work_dir.mkdir()
    return work_dir.resolve()

def main():
    parser = argparse.ArgumentParser(usage='%(prog)s [options]')
    parser.add_argument('-c', '--commit', action='store_true', dest='commit', help='Indicate that the version argument is for a commit or branch')
    parser.add_argument('-p', '--pull', action='store_true', dest='pull', help='Indicate that the version argument is the number of a github repository pull request')
    parser.add_argument('-u', '--url', dest='url', default='<EMAIL>:dtr-org/unit-e.git', help='Specify the URL of the repository. Default is %(default)s')
    parser.add_argument('-g', '--git-dir', dest='git_dir', default='unit-e', help='Specify the name of the directory where the unit-e git repo is checked out. This is relative to the working directory (see --work-dir option)')
    parser.add_argument('-v', '--verify', action='store_true', dest='verify', help='Verify the Gitian build')
    parser.add_argument('-b', '--build', action='store_true', dest='build', help='Do a Gitian build')
    parser.add_argument('-C', '--codesign', action='store_true', dest='codesign', help='Make detached signatures for Windows and MacOS')
    parser.add_argument('-s', '--sign', action='store_true', dest='sign', help='Make signed binaries for Windows')
    parser.add_argument('-B', '--buildsign', action='store_true', dest='buildsign', help='Build both signed and unsigned binaries')
    parser.add_argument('-o', '--os', dest='os', default='lwm', help='Specify which Operating Systems the build is for. Default is %(default)s. l for Linux, w for Windows, m for MacOS')
    parser.add_argument('-j', '--jobs', dest='jobs', default='2', help='Number of processes to use. Default %(default)s')
    parser.add_argument('-m', '--memory', dest='memory', default='2000', help='Memory to allocate in MiB. Default %(default)s')
    parser.add_argument('-k', '--kvm', action='store_true', dest='kvm', help='Use KVM instead of LXC')
    parser.add_argument('-d', '--docker', action='store_true', dest='docker', help='Use Docker instead of LXC')
    parser.add_argument('-S', '--setup', action='store_true', dest='setup', help='Set up the Gitian building environment. Uses LXC. If you want to use KVM, use the --kvm option. Only works on Debian-based systems (Ubuntu, Debian)')
    parser.add_argument('-D', '--detach-sign', action='store_true', dest='detach_sign', help='Create the assert file for detached signing. Will not commit anything.')
    parser.add_argument('-n', '--no-commit', action='store_false', dest='commit_files', help='Do not commit anything to git')
    parser.add_argument('-q', '--quiet', action='store_true', dest='quiet', help='Do not prompt user for confirmations')
    parser.add_argument('--osslsign', dest='osslsigncode_path', help='Path to osslsigncode executable.')
    parser.add_argument('--win-code-cert', dest='win_code_cert_path', help='Path to code signing certificate.')
    parser.add_argument('--win-code-key', dest='win_code_key_path', help='Path to key corresponding to code signing certificate.')
    parser.add_argument('--signer', dest='signer', help='GPG signer to sign each build assert file. Required to build, sign or verify')
    parser.add_argument('--version', dest='version', help='Version number, commit, or branch to build. If building a commit or branch, the -c option must be specified. Required to build, sign or verify')
    parser.add_argument('--skip-checkout', dest='skip_checkout', action='store_true', help='Skip checkout of git repo. Use with care as it might leave you in an inconsistent state.')
    parser.add_argument('--hosts', dest='hosts', help='Specify hosts for which is built. See the gitian descriptors for valid values.')
    parser.add_argument('-w', '--work-dir', dest='work_dir', help='Working directory where data is checked out and the build takes place. The directory will be created if it does not exist.')
    args = parser.parse_args()

    if not args.work_dir:
        print("Please provide a working directory (--work-dir <path>)", file=sys.stderr)
        exit(1)

    if args.win_code_cert_path:
        args.win_code_cert_path = Path(args.win_code_cert_path).resolve()

    if args.win_code_key_path:
        args.win_code_key_path = Path(args.win_code_key_path).resolve()

    work_dir = create_work_dir(args.work_dir)
    with cd(work_dir):
        print("Using working directory '%s'" % work_dir)

        os.environ["PATH"] = str(Path("bin").resolve()) + ":" + os.environ["PATH"]

        args.linux = 'l' in args.os
        args.windows = 'w' in args.os
        args.macos = 'm' in args.os

        args.is_bionic = platform.dist() == ('Ubuntu', '18.04', 'bionic')

        if args.buildsign:
            args.build = True
            args.sign = True

        if args.kvm and args.docker:
            raise Exception('Error: cannot have both kvm and docker')

        args.sign_prog = 'true' if args.detach_sign else 'gpg --detach-sign'

        # Set environment variable USE_LXC or USE_DOCKER, let gitian-builder know that we use lxc or docker
        if args.docker:
            os.environ['USE_DOCKER'] = '1'
            os.environ['USE_LXC'] = ''
            os.environ['USE_VBOX'] = ''
        elif not args.kvm:
            os.environ['USE_LXC'] = '1'
            os.environ['USE_DOCKER'] = ''
            os.environ['USE_VBOX'] = ''
            if not 'GITIAN_HOST_IP' in os.environ.keys():
                os.environ['GITIAN_HOST_IP'] = '10.0.3.1'
            if not 'LXC_GUEST_IP' in os.environ.keys():
                os.environ['LXC_GUEST_IP'] = '10.0.3.5'

        # Disable for MacOS if no SDK found
        if args.macos and not Path('gitian-builder/inputs/MacOSX10.11.sdk.tar.gz').is_file():
            print('Cannot build for MacOS, SDK does not exist. Will build for other OSes')
            args.macos = False

        if args.setup:
            setup(args)

        if not args.build and not args.sign and not args.verify and not args.codesign:
            return

        if not args.linux and not args.windows and not args.macos:
            raise RuntimeError('No platform specified. Exiting.')

        script_name = Path(sys.argv[0]).name

        # Signer and version shouldn't be empty
        if not args.signer:
            print(script_name+': Missing signer.')
            print('Try '+script_name+' --help for more information')
            exit(1)

        if not args.version:
            print(script_name+': Missing version.')
            print('Try '+script_name+' --help for more information')
            exit(1)

        # Check that path to osslsigncode executable is known
        if args.windows and args.codesign:
            if not args.setup:
                args.osslsigncode_path = find_osslsigncode(args.osslsigncode_path)

            if not args.osslsigncode_path:
                print('Cannot find osslsigncode. Please provide it with --osslsign or run --setup to make it.', file=sys.stderr)
                exit(1)

            if not args.win_code_cert_path or not Path(args.win_code_cert_path).is_file():
                print('Please provide code signing certificate path (--win-code-cert)', file=sys.stderr)
                exit(1)

            if not args.win_code_key_path or not Path(args.win_code_key_path).is_file():
                print('Please provide code signing key path (--win-code-key)', file=sys.stderr)
                exit(1)


        # Add leading 'v' for tags
        if args.commit and args.pull:
            raise Exception('Cannot have both commit and pull')
        args.commit = ('' if args.commit else 'v') + args.version

        if args.pull and args.skip_checkout:
            raise Exception('Cannot pull and skip-checkout at the same time')

        prepare_git_dir(args)
        prepare_gitian_descriptors(source=args.git_dir + "/contrib/gitian-descriptors/",
                                   target="work/gitian-descriptors", hosts=args.hosts)

        if args.build:
            build(args)

        if args.codesign:
            codesign(args)

        if args.sign:
            sign(args)

        if args.verify:
            verify(args)

if __name__ == '__main__':
    main()

#!/usr/bin/env python3
# Copyright (c) 2018-2019 The Bitcoin Core developers
# Copyright (c) 2019 The Unit-e developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://opensource.org/licenses/MIT.

import argparse
import os
import subprocess
import sys

import platform

import tempfile

from pathlib import Path
from contextlib import contextmanager

OSSLSIGNCODE_VER = '1.7.1'
OSSLSIGNCODE_DIR = 'osslsigncode-'+OSSLSIGNCODE_VER

@contextmanager
def cd(destination_dir):
    original_dir = os.getcwd()
    os.chdir(destination_dir)
    try:
        yield
    finally:
        os.chdir(original_dir)

class Installer:
    """ Wrapper around native package installer, supports apt-get and brew, only
        installs packages which aren't installed yet."""
    def __init__(self, backend=None, quiet=False):
        if backend != "apt" and backend != "brew":
            raise ValueError("Invalid value for backend argument: '%'. Valid values are `apt` and `brew`.")
        self.backend = backend
        self.updated = False
        self.to_install = []
        if quiet and self.backend == "apt":
            self.flags = ['-qq']
        else:
            self.flags = []

    def backend_command(self, subcommand):
        if self.backend == 'apt':
            if subcommand == 'ls':
                command = ['dpkg', '-s']
            else:
                command = ['sudo', 'apt-get', subcommand] + self.flags
        elif self.backend == 'brew':
            command = ['brew', subcommand]
        return command

    def update(self):
        if not self.updated:
            self.updated = True
            subprocess.check_call(self.backend_command('update'))

    def try_to_install(self, *programs):
        self.update()
        print(self.backend + ': installing', ", ".join(programs))
        return subprocess.call(self.backend_command('install') + list(programs)) == 0

    def batch_install(self):
        if not self.to_install:
            print(self.backend + ': nothing to install')
            return

        if not self.try_to_install(*self.to_install):
            print('Could not install packages.', file=sys.stderr)
            exit(1)
        self.to_install = []

    def is_installed(self, program):
        return subprocess.call(self.backend_command('ls') + [program], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0

    def add_requirements(self, *programs):
        for program in programs:
            if not self.is_installed(program):
                self.to_install.append(program)


def verify_user_specified_osslsigncode(user_spec_path):
    if not Path(user_spec_path).is_file():
        raise Exception('provided osslsign does not exists: {}'.format(user_spec_path))

    try:
        if subprocess.call([user_spec_path, '--version'], stderr=subprocess.DEVNULL) != 255:
            raise Exception('cannot execute provided osslsigncode: {}'.format(user_spec_path))
    except subprocess.CalledProcessError as e:
        raise Exception('unexpected exception raised while executing provided osslsigncode: {}'.format(str(e)))

    return Path(user_spec_path).resolve()

def find_osslsigncode(user_spec_path):
    if user_spec_path:
        return verify_user_specified_osslsigncode(user_spec_path)

    which_ossl_proc = subprocess.Popen(['which', 'osslsigncode'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
    if which_ossl_proc.wait() == 0:
        ossl_path = which_ossl_proc.communicate()[0].split()[0].decode()
        if subprocess.call([ossl_path, '--version'], stderr=subprocess.DEVNULL) == 255:
            return ossl_path

    expected_path = Path(OSSLSIGNCODE_DIR, 'osslsigncode')
    if expected_path.is_file() and subprocess.call([expected_path, '--version'], stderr=subprocess.DEVNULL) == 255:
        return expected_path.resolve()

    return None

def install_osslsigner():
    subprocess.check_call(['wget', '-N', 'https://downloads.sourceforge.net/project/osslsigncode/osslsigncode/osslsigncode-'+OSSLSIGNCODE_VER+'.tar.gz'])
    subprocess.check_call(['wget', '-N', 'https://bitcoincore.org/cfields/osslsigncode-Backports-to-'+OSSLSIGNCODE_VER+'.patch'])
    subprocess.check_call(["echo 'a8c4e9cafba922f89de0df1f2152e7be286aba73f78505169bc351a7938dd911 osslsigncode-Backports-to-"+OSSLSIGNCODE_VER+".patch' | sha256sum -c"], shell=True)
    subprocess.check_call(["echo 'f9a8cdb38b9c309326764ebc937cba1523a3a751a7ab05df3ecc99d18ae466c9 osslsigncode-"+OSSLSIGNCODE_VER+".tar.gz' | sha256sum -c"], shell=True)
    subprocess.check_call(['tar', '-xzf', 'osslsigncode-'+OSSLSIGNCODE_VER+'.tar.gz'])
    subprocess.check_call(['patch -p1 < ../osslsigncode-Backports-to-'+OSSLSIGNCODE_VER+'.patch'], shell=True, cwd=OSSLSIGNCODE_DIR)
    subprocess.check_call(['./configure', '--without-gsf', '--without-curl', '--disable-dependency-tracking'], cwd=OSSLSIGNCODE_DIR)
    subprocess.check_call(['make'], cwd=OSSLSIGNCODE_DIR)
    return Path(OSSLSIGNCODE_DIR, 'osslsigncode').resolve()


def install_libssl_dev(apt):
    dist_str = platform.dist()
    dist_type = dist_str[0]
    dist_no = int(dist_str[1].replace('.', ''))
    if dist_type == 'Ubuntu' and dist_no < 1800 or dist_type == 'Debian' and dist_no < 900:
        apt.add_requirements('libssl-dev')
    else:
        apt.add_requirements('libssl1.0-dev')


def install_linux_deps(args):
    apt = Installer(backend='apt', quiet=args.quiet)
    apt.add_requirements('ruby', 'git', 'make')
    if args.kvm:
        apt.add_requirements('apt-cacher-ng', 'python-vm-builder', 'qemu-kvm', 'qemu-utils')
    elif args.docker:
        if subprocess.call(['docker', '--version']) != 0:
            if not apt.try_to_install('docker.io') and not apt.try_to_install('docker-ce'):
                print('Cannot find any way to install docker', file=sys.stderr)
                exit(1)
    else:
        apt.add_requirements('apt-cacher-ng', 'lxc', 'debootstrap')

    should_make_ossl = False
    if args.codesign and args.windows:
        args.osslsigncode_path = find_osslsigncode(args.osslsigncode_path)
        if not args.osslsigncode_path:
            should_make_ossl = True
            apt.add_requirements('tar', 'wget', 'patch', 'autoconf')
            install_libssl_dev(apt)

    apt.batch_install()

    if should_make_ossl:
        args.osslsigncode_path = install_osslsigner()

def create_bin_symlink(link, target):
    bin_path = Path("bin", link)
    if not bin_path.exists():
        bin_path.symlink_to(target)

def install_mac_deps(args):
    if not args.docker:
        print('Mac can only work with docker, re-run with --docker flag.', file=sys.stderr)
        exit(1)

    if subprocess.call(['docker', '--version']) != 0:
        print('Please install docker manually, e.g. with `brew cask install docker`.', file=sys.stderr)
        exit(1)

    brew = Installer(backend='brew')
    brew.add_requirements('ruby', 'coreutils')
    brew.batch_install()

    create_bin_symlink("date", "/usr/local/bin/gdate")
    create_bin_symlink("sha256sum", "/usr/local/bin/gsha256sum")

def install_deps(args):
    system_str = platform.system()
    if system_str == 'Linux':
        install_linux_deps(args)
    elif system_str == 'Darwin':
        install_mac_deps(args)
    else:
        print("Unsupported system '%s'." % system_str, file=sys.stderr)
        exit(1)


def setup(args):
    install_deps(args)

    if not Path('unit-e-sigs').is_dir():
        subprocess.check_call(['git', 'clone', '<EMAIL>:dtr-org/unit-e-sigs.git'])
    if not Path('gitian-builder').is_dir():
        subprocess.check_call(['git', 'clone', 'https://github.com/devrandom/gitian-builder.git'])
    if not Path(args.git_dir).is_dir():
        subprocess.check_call(['git', 'clone', args.url, args.git_dir])
    make_image_prog = ['bin/make-base-vm', '--suite', 'bionic', '--arch', 'amd64']
    if args.docker:
        make_image_prog += ['--docker']
    elif not args.kvm:
        make_image_prog += ['--lxc']
    subprocess.check_call(make_image_prog, cwd='gitian-builder')
    if args.is_bionic and not args.kvm and not args.docker:
        subprocess.check_call(['sudo', 'sed', '-i', 's/lxcbr0/br0/', '/etc/default/lxc-net'])
        print('Reboot is required')
        exit(0)

def gitian_descriptors(args, platform_str):
    return '../work/gitian-descriptors/gitian-' + platform_str + '.yml'

def build(args):
    os.makedirs('unit-e-binaries/' + args.version, exist_ok=True)
    print('\nBuilding Dependencies\n')
    with cd('gitian-builder'):
        os.makedirs('inputs', exist_ok=True)

        subprocess.check_call(['make', '-C', Path('../', args.git_dir, 'depends'), 'download', 'SOURCES_PATH=' + os.getcwd() + '/cache/common'])

        if args.linux:
            print('\nCompiling ' + args.version + ' Linux')
            subprocess.check_call(['bin/gbuild', '-j', args.jobs, '-m', args.memory, '--commit', 'unit-e='+args.commit, '--url', 'unit-e='+args.url, gitian_descriptors(args, 'linux')])
            subprocess.check_call(['bin/gsign', '-p', args.sign_prog, '--signer', args.signer, '--release', args.version+'-linux', '--destination', '../unit-e-sigs/', gitian_descriptors(args, 'linux')])
            subprocess.check_call('mv build/out/unit-e-*.tar.gz build/out/src/unit-e-*.tar.gz ../unit-e-binaries/'+args.version, shell=True)

        if args.windows:
            print('\nCompiling ' + args.version + ' Windows')
            subprocess.check_call(['bin/gbuild', '-j', args.jobs, '-m', args.memory, '--commit', 'unit-e='+args.commit, '--url', 'unit-e='+args.url, gitian_descriptors(args, 'win')])
            subprocess.check_call(['bin/gsign', '-p', args.sign_prog, '--signer', args.signer, '--release', args.version+'-win-unsigned', '--destination', '../unit-e-sigs/', gitian_descriptors(args, 'win')])
            subprocess.check_call('mv build/out/unit-e-*-win-unsigned.tar.gz inputs/', shell=True)
            subprocess.check_call('mv build/out/unit-e-*.zip build/out/unit-e-*.exe ../unit-e-binaries/'+args.version, shell=True)

        if args.macos:
            print('\nCompiling ' + args.version + ' MacOS')
            subprocess.check_call(['bin/gbuild', '-j', args.jobs, '-m', args.memory, '--commit', 'unit-e='+args.commit, '--url', 'unit-e='+args.url, gitian_descriptors(args, 'osx')])
            subprocess.check_call(['bin/gsign', '-p', args.sign_prog, '--signer', args.signer, '--release', args.version+'-osx-unsigned', '--destination', '../unit-e-sigs/', gitian_descriptors(args, 'osx')])
            subprocess.check_call('mv build/out/unit-e-*.tar.gz ../unit-e-binaries/'+args.version, shell=True)

    if args.commit_files:
        print('\nCommitting '+args.version+' Unsigned Sigs\n')
        with cd('unit-e-sigs'):
            if args.linux:
                subprocess.check_call(['git', 'add', args.version+'-linux/'+args.signer])
            if args.windows:
                subprocess.check_call(['git', 'add', args.version+'-win-unsigned/'+args.signer])
            if args.macos:
                subprocess.check_call(['git', 'add', args.version+'-osx-unsigned/'+args.signer])
            subprocess.check_call(['git', 'commit', '-m', 'Add '+args.version+' unsigned sigs for '+args.signer])

def get_signatures_path(platform_str, version):
    return Path('unit-e-sigs', version + '-detached', 'unit-e-'+platform_str+'-signatures.tar.gz').resolve()


def codesign_windows(osslsign_path, version, win_code_cert_path, win_code_key_path):
    gitian_dir = Path('gitian-builder').resolve()
    signatures_tarball = get_signatures_path('win', version)
    if signatures_tarball.is_file():
        print('Signatures already present at:', signatures_tarball, '\nI cowardly refuse to continue', file=sys.stderr)
        exit(1)

    signatures_tarball.parent.mkdir(exist_ok=True)

    print('\nSigning ' + version + ' Windows')
    with tempfile.TemporaryDirectory() as build_dir:
        subprocess.check_call(['cp', 'inputs/unit-e-' + version + '-win-unsigned.tar.gz', Path(build_dir, 'unit-e-win-unsigned.tar.gz')], cwd=gitian_dir)
        subprocess.check_call(['tar', '-xzf', 'unit-e-win-unsigned.tar.gz'], cwd=build_dir)

        for fp in Path(build_dir).glob('unsigned/*.exe'):
            subprocess.check_call([osslsign_path, 'sign', '-certs', win_code_cert_path, '-in', fp, '-out', str(fp)+'-signed', '-key', win_code_key_path, '-askpass'])
            subprocess.check_call([osslsign_path, 'extract-signature', '-pem', '-in', str(fp)+'-signed', '-out', str(fp)+'.pem'])

        subprocess.check_call(['tar', '-czf', signatures_tarball, *[path.name for path in Path(build_dir, 'unsigned').glob('*.exe.pem')]], cwd=Path(build_dir, 'unsigned'))


def codesign(args):
    if not args.windows:
        print('Warning: codesigning requested, but windows not in the --os flag.', file=sys.stderr)
        return

    codesign_windows(args.osslsigncode_path, args.version, args.win_code_cert_path, args.win_code_key_path)

    if args.commit_files:
        print('\nCommitting '+args.version+' Detached Sigs\n')
        subprocess.check_call(['git', 'add', Path(args.version + '-detached', 'unit-e-win-signatures.tar.gz')], cwd='unit-e-sigs')
        subprocess.check_call(['git', 'commit', '-a', '-m', 'Add '+args.version+' detached signatures by '+args.signer], cwd='unit-e-sigs')



def sign(args):
    if not args.windows:
        print('Warning: signing requested, but windows not in the --os flag.', file=sys.stderr)
        return

    gitian_dir = Path('gitian-builder').resolve()

    subprocess.check_call(['wget', '-N', '-P', 'inputs', 'https://downloads.sourceforge.net/project/osslsigncode/osslsigncode/osslsigncode-'+OSSLSIGNCODE_VER+'.tar.gz'], cwd=gitian_dir)
    subprocess.check_call(['wget', '-N', '-P', 'inputs', 'https://bitcoincore.org/cfields/osslsigncode-Backports-to-'+OSSLSIGNCODE_VER+'.patch'], cwd=gitian_dir)

    signatures_tarball = get_signatures_path('win', args.version)
    if not signatures_tarball.is_file():
        print('Signatures not present at:', signatures_tarball, file=sys.stderr)
        exit(1)

    print('\nSigning ' + args.version + ' Windows')
    subprocess.check_call(['cp', signatures_tarball, 'inputs/unit-e-win-signatures.tar.gz'], cwd=gitian_dir)
    subprocess.check_call(['cp', 'inputs/unit-e-' + args.version + '-win-unsigned.tar.gz', 'inputs/unit-e-win-unsigned.tar.gz'], cwd=gitian_dir)
    subprocess.check_call(['bin/gbuild', '-i', '--commit', 'signature=master', gitian_descriptors(args, 'win-signer')], cwd=gitian_dir)
    subprocess.check_call(['bin/gsign', '-p', args.sign_prog, '--signer', args.signer, '--release', args.version+'-win-signed', '--destination', '../unit-e-sigs/', gitian_descriptors(args, 'win-signer')], cwd=gitian_dir)
    subprocess.check_call('mv build/out/unit-e-*win64-setup.exe ../unit-e-binaries/'+args.version, shell=True, cwd=gitian_dir)
    subprocess.check_call('mv build/out/unit-e-*win32-setup.exe ../unit-e-binaries/'+args.version, shell=True, cwd=gitian_dir)

    if args.commit_files:
        print('\nCommitting '+args.version+' Signed Sigs\n')
        subprocess.check_call(['git', 'add', args.version+'-win-signed/'+args.signer], cwd='unit-e-sigs')
        subprocess.check_call(['git', 'commit', '-a', '-m', 'Add '+args.version+' signed binary sigs for '+args.signer], cwd='unit-e-sigs')


def verify(args):
    """ Verify the builds. Exits with error in case any of the signatures fail to verify and if any of the signatures are missing. """
    gitian_builder = Path('gitian-builder')
    sigs_path = Path('unit-e-sigs')

    builds_were_missing = False

    for (sig_path_suffix, descriptor_suffix, build_name) in [
            ('linux', 'linux', 'Linux'),
            ('win-unsigned', 'win', 'Windows'),
            ('osx-unsigned', 'osx', 'MacOS'),
            ('win-signed', 'win-signer', 'Signed Windows'),
    ]:
        build_sig_dir = args.version + '-' + sig_path_suffix
        if Path(sigs_path, build_sig_dir).is_dir():
            print('\nVerifying v{} {}\n'.format(args.version, build_name))
            descriptor = gitian_descriptors(args, descriptor_suffix)
            subprocess.check_call(['bin/gverify', '-v', '-d', '../unit-e-sigs/', '-r', build_sig_dir, descriptor], cwd=gitian_builder)
        else:
            print('\nSkipping v{} {} as it is not present\n'.format(args.version, build_name))
            builds_were_missing = True

    if builds_were_missing:
        print('Some builds were missing, please refer to previous logs.', file=sys.stderr)
        exit(1)


def prepare_git_dir(args):
    with cd(args.git_dir):
        if args.pull:
            subprocess.check_call(['git', 'fetch', args.url, 'refs/pull/'+args.version+'/merge'])
            os.chdir('../gitian-builder/inputs/unit-e')
            subprocess.check_call(['git', 'fetch', args.url, 'refs/pull/'+args.version+'/merge'])
            args.commit = subprocess.check_output(['git', 'show', '-s', '--format=%H', 'FETCH_HEAD'], universal_newlines=True, encoding='utf8').strip()
            args.version = 'pull-' + args.version
        print(args.commit)
        if not args.skip_checkout:
            subprocess.check_call(['git', 'fetch'])
            subprocess.check_call(['git', 'checkout', args.commit])

# Use only keyword-only arguments as defined in PEP 3102 to avoid accidentally swapping of arguments
def prepare_gitian_descriptors(*, source, target, hosts=None):
    descriptor_source_dir = Path(source)
    descriptor_dir = Path(target)
    if not descriptor_source_dir.is_dir():
        raise Exception("Gitian descriptor dir '%s' does not exist" % descriptor_source_dir)
    descriptor_dir.mkdir(parents=True, exist_ok=True)
    for descriptor_path in descriptor_source_dir.glob("*.yml"):
        filename = descriptor_path.relative_to(descriptor_source_dir)
        descriptor_in = descriptor_source_dir / filename
        descriptor_out = descriptor_dir / filename
        with descriptor_out.open("w") as file_out, descriptor_in.open() as file_in:
            for line in file_in:
                if hosts and line.startswith('  HOSTS='):
                    file_out.write('  HOSTS="%s"\n' % hosts)
                else:
                    file_out.write(line)

def create_work_dir(work_dir):
    work_dir = Path(work_dir)
    if Path(work_dir).exists() and not Path(work_dir).is_dir():
        raise Exception("Work dir '%s' exists but is not a directory." % work_dir)
    if not work_dir.exists():
        print("Creating working directory '%s'" % work_dir)
        work_dir.mkdir()
    return work_dir.resolve()

def main():
    parser = argparse.ArgumentParser(usage='%(prog)s [options]')
    parser.add_argument('-c', '--commit', action='store_true', dest='commit', help='Indicate that the version argument is for a commit or branch')
    parser.add_argument('-p', '--pull', action='store_true', dest='pull', help='Indicate that the version argument is the number of a github repository pull request')
    parser.add_argument('-u', '--url', dest='url', default='<EMAIL>:dtr-org/unit-e.git', help='Specify the URL of the repository. Default is %(default)s')
    parser.add_argument('-g', '--git-dir', dest='git_dir', default='unit-e', help='Specify the name of the directory where the unit-e git repo is checked out. This is relative to the working directory (see --work-dir option)')
    parser.add_argument('-v', '--verify', action='store_true', dest='verify', help='Verify the Gitian build')
    parser.add_argument('-b', '--build', action='store_true', dest='build', help='Do a Gitian build')
    parser.add_argument('-C', '--codesign', action='store_true', dest='codesign', help='Make detached signatures for Windows and MacOS')
    parser.add_argument('-s', '--sign', action='store_true', dest='sign', help='Make signed binaries for Windows')
    parser.add_argument('-B', '--buildsign', action='store_true', dest='buildsign', help='Build both signed and unsigned binaries')
    parser.add_argument('-o', '--os', dest='os', default='lwm', help='Specify which Operating Systems the build is for. Default is %(default)s. l for Linux, w for Windows, m for MacOS')
    parser.add_argument('-j', '--jobs', dest='jobs', default='2', help='Number of processes to use. Default %(default)s')
    parser.add_argument('-m', '--memory', dest='memory', default='2000', help='Memory to allocate in MiB. Default %(default)s')
    parser.add_argument('-k', '--kvm', action='store_true', dest='kvm', help='Use KVM instead of LXC')
    parser.add_argument('-d', '--docker', action='store_true', dest='docker', help='Use Docker instead of LXC')
    parser.add_argument('-S', '--setup', action='store_true', dest='setup', help='Set up the Gitian building environment. Uses LXC. If you want to use KVM, use the --kvm option. Only works on Debian-based systems (Ubuntu, Debian)')
    parser.add_argument('-D', '--detach-sign', action='store_true', dest='detach_sign', help='Create the assert file for detached signing. Will not commit anything.')
    parser.add_argument('-n', '--no-commit', action='store_false', dest='commit_files', help='Do not commit anything to git')
    parser.add_argument('-q', '--quiet', action='store_true', dest='quiet', help='Do not prompt user for confirmations')
    parser.add_argument('--osslsign', dest='osslsigncode_path', help='Path to osslsigncode executable.')
    parser.add_argument('--win-code-cert', dest='win_code_cert_path', help='Path to code signing certificate.')
    parser.add_argument('--win-code-key', dest='win_code_key_path', help='Path to key corresponding to code signing certificate.')
    parser.add_argument('--signer', dest='signer', help='GPG signer to sign each build assert file. Required to build, sign or verify')
    parser.add_argument('--version', dest='version', help='Version number, commit, or branch to build. If building a commit or branch, the -c option must be specified. Required to build, sign or verify')
    parser.add_argument('--skip-checkout', dest='skip_checkout', action='store_true', help='Skip checkout of git repo. Use with care as it might leave you in an inconsistent state.')
    parser.add_argument('--hosts', dest='hosts', help='Specify hosts for which is built. See the gitian descriptors for valid values.')
    parser.add_argument('-w', '--work-dir', dest='work_dir', help='Working directory where data is checked out and the build takes place. The directory will be created if it does not exist.')
    args = parser.parse_args()

    if not args.work_dir:
        print("Please provide a working directory (--work-dir <path>)", file=sys.stderr)
        exit(1)

    if args.win_code_cert_path:
        args.win_code_cert_path = Path(args.win_code_cert_path).resolve()

    if args.win_code_key_path:
        args.win_code_key_path = Path(args.win_code_key_path).resolve()

    work_dir = create_work_dir(args.work_dir)
    with cd(work_dir):
        print("Using working directory '%s'" % work_dir)

        os.environ["PATH"] = str(Path("bin").resolve()) + ":" + os.environ["PATH"]

        args.linux = 'l' in args.os
        args.windows = 'w' in args.os
        args.macos = 'm' in args.os

        args.is_bionic = platform.dist() == ('Ubuntu', '18.04', 'bionic')

        if args.buildsign:
            args.build = True
            args.sign = True

        if args.kvm and args.docker:
            raise Exception('Error: cannot have both kvm and docker')

        args.sign_prog = 'true' if args.detach_sign else 'gpg --detach-sign'

        # Set environment variable USE_LXC or USE_DOCKER, let gitian-builder know that we use lxc or docker
        if args.docker:
            os.environ['USE_DOCKER'] = '1'
            os.environ['USE_LXC'] = ''
            os.environ['USE_VBOX'] = ''
        elif not args.kvm:
            os.environ['USE_LXC'] = '1'
            os.environ['USE_DOCKER'] = ''
            os.environ['USE_VBOX'] = ''
            if not 'GITIAN_HOST_IP' in os.environ.keys():
                os.environ['GITIAN_HOST_IP'] = '10.0.3.1'
            if not 'LXC_GUEST_IP' in os.environ.keys():
                os.environ['LXC_GUEST_IP'] = '10.0.3.5'

        # Disable for MacOS if no SDK found
        if args.macos and not Path('gitian-builder/inputs/MacOSX10.11.sdk.tar.gz').is_file():
            print('Cannot build for MacOS, SDK does not exist. Will build for other OSes')
            args.macos = False

        if args.setup:
            setup(args)

        if not args.build and not args.sign and not args.verify and not args.codesign:
            return

        if not args.linux and not args.windows and not args.macos:
            raise RuntimeError('No platform specified. Exiting.')

        script_name = Path(sys.argv[0]).name

        # Signer and version shouldn't be empty
        if not args.signer:
            print(script_name+': Missing signer.')
            print('Try '+script_name+' --help for more information')
            exit(1)

        if not args.version:
            print(script_name+': Missing version.')
            print('Try '+script_name+' --help for more information')
            exit(1)

        # Check that path to osslsigncode executable is known
        if args.windows and args.codesign:
            if not args.setup:
                args.osslsigncode_path = find_osslsigncode(args.osslsigncode_path)

            if not args.osslsigncode_path:
                print('Cannot find osslsigncode. Please provide it with --osslsign or run --setup to make it.', file=sys.stderr)
                exit(1)

            if not args.win_code_cert_path or not Path(args.win_code_cert_path).is_file():
                print('Please provide code signing certificate path (--win-code-cert)', file=sys.stderr)
                exit(1)

            if not args.win_code_key_path or not Path(args.win_code_key_path).is_file():
                print('Please provide code signing key path (--win-code-key)', file=sys.stderr)
                exit(1)


        # Add leading 'v' for tags
        if args.commit and args.pull:
            raise Exception('Cannot have both commit and pull')
        args.commit = ('' if args.commit else 'v') + args.version

        if args.pull and args.skip_checkout:
            raise Exception('Cannot pull and skip-checkout at the same time')

        prepare_git_dir(args)
        prepare_gitian_descriptors(source=args.git_dir + "/contrib/gitian-descriptors/",
                                   target="work/gitian-descriptors", hosts=args.hosts)

        if args.build:
            build(args)

        if args.codesign:
            codesign(args)

        if args.sign:
            sign(args)

        if args.verify:
            verify(args)

if __name__ == '__main__':
    main()
#!/usr/bin/env python3 # Copyright (c) 2018-2019 The Bitcoin Core developers # Copyright (c) 2019 The Unit-e developers # Distributed under the MIT software license, see the accompanying # file COPYING or https://opensource.org/licenses/MIT. Wrapper around native package installer, supports apt-get and brew, only         installs packages which aren't installed yet. Verify the builds. Exits with error in case any of the signatures fail to verify and if any of the signatures are missing. # Use only keyword-only arguments as defined in PEP 3102 to avoid accidentally swapping of arguments # Set environment variable USE_LXC or USE_DOCKER, let gitian-builder know that we use lxc or docker # Disable for MacOS if no SDK found # Signer and version shouldn't be empty # Check that path to osslsigncode executable is known # Add leading 'v' for tags