#!/usr/bin/env python
"""Script to create self contained install.

The goal of this script is simple:

  * Create a self contained install of the CLI that
  has requires no external resources during installation.

It does this by using all the normal python tooling
(virtualenv, pip) but provides a simple, easy to use
interface for those not familiar with the python
ecosystem.

"""
import os
import sys
import subprocess
import shutil
import tempfile
import zipfile
from contextlib import contextmanager


EXTRA_RUNTIME_DEPS = [
    # Use an up to date virtualenv/pip/setuptools on > 2.6.
    ('virtualenv', '16.7.8'),
    ('jmespath', '0.10.0'),
]
BUILDTIME_DEPS = [
    ('setuptools-scm', '3.3.3'),
    ('wheel', '0.33.6'),
]
PIP_DOWNLOAD_ARGS = '--no-binary :all:'
# The constraints file is used to lock the version of rsa needed
# to be 3.4.2 until we can drop py27 support. This lets us ensure
# we're distributing a copy that works on all supported platforms.
CONSTRAINTS_FILE = os.path.join(
    os.path.dirname(os.path.abspath(__file__)),
    'assets', 'constraints-bundled.txt'
)


class BadRCError(Exception):
    pass


@contextmanager
def cd(dirname):
    original = os.getcwd()
    os.chdir(dirname)
    try:
        yield
    finally:
        os.chdir(original)


def run(cmd):
    sys.stdout.write("Running cmd: %s\n" % cmd)
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
    stdout, stderr = p.communicate()
    rc = p.wait()
    if p.returncode != 0:
        raise BadRCError("Bad rc (%s) for cmd '%s': %s" % (
            rc, cmd, stderr + stdout))
    return stdout


def create_scratch_dir():
    # This creates the dir where all the bundling occurs.
    # First we need a top level dir.
    dirname = tempfile.mkdtemp(prefix='bundle')
    # Then we need to create a dir where all the packages
    # will come from.
    os.mkdir(os.path.join(dirname, 'packages'))
    os.mkdir(os.path.join(dirname, 'packages', 'setup'))
    return dirname


def download_package_tarballs(dirname, packages):
    with cd(dirname):
        for package, package_version in packages:
            run('%s -m pip download %s==%s %s' % (
                sys.executable, package, package_version, PIP_DOWNLOAD_ARGS
            ))


def download_cli_deps(scratch_dir):
    awscli_dir = os.path.dirname(
        os.path.dirname(os.path.abspath(__file__)))
    with cd(scratch_dir):
        run('pip download -c %s %s %s' % (
            CONSTRAINTS_FILE, PIP_DOWNLOAD_ARGS, awscli_dir))


def _remove_cli_zip(scratch_dir):
    clidir = [f for f in os.listdir(scratch_dir) if f.startswith('awscli')]
    assert len(clidir) == 1
    os.remove(os.path.join(scratch_dir, clidir[0]))


def add_cli_sdist(scratch_dir):
    awscli_dir = os.path.dirname(
        os.path.dirname(os.path.abspath(__file__)))
    if os.path.exists(os.path.join(awscli_dir, 'dist')):
        shutil.rmtree(os.path.join(awscli_dir, 'dist'))
    with cd(awscli_dir):
        run('%s setup.py sdist' % sys.executable)
        filename = os.listdir('dist')[0]
        shutil.move(os.path.join('dist', filename),
                    os.path.join(scratch_dir, filename))


def create_bootstrap_script(scratch_dir):
    install_script = os.path.join(
        os.path.dirname(os.path.abspath(__file__)), 'install')
    shutil.copy(install_script, os.path.join(scratch_dir, 'install'))


def zip_dir(scratch_dir):
    basename = 'awscli-bundle.zip'
    dirname, tmpdir = os.path.split(scratch_dir)
    final_dir_name = os.path.join(dirname, 'awscli-bundle')
    if os.path.isdir(final_dir_name):
        shutil.rmtree(final_dir_name)
    shutil.move(scratch_dir, final_dir_name)
    with cd(dirname):
        with zipfile.ZipFile(basename, 'w', zipfile.ZIP_DEFLATED) as zipped:
            for root, dirnames, filenames in os.walk('awscli-bundle'):
                for filename in filenames:
                    zipped.write(os.path.join(root, filename))
    return os.path.join(dirname, basename)


def verify_preconditions():
    # The pip version looks like:
    # 'pip 1.4.1 from ....'
    pip_version = run(
        '%s -m pip --version' % sys.executable).strip().split()[1]
    # Virtualenv version just has the version string: '1.14.5\n'
    virtualenv_version = run(
        '%s -m virtualenv --version' % sys.executable).strip()
    _min_version_required('9.0.1', pip_version, 'pip')
    _min_version_required('15.1.0', virtualenv_version, 'virtualenv')


def _min_version_required(min_version, actual_version, name):
    # precondition: min_version is major.minor.patch
    #               actual_version is major.minor.patch
    min_split = min_version.split('.')
    actual_split = actual_version.decode('utf-8').split('.')
    for min_version_part, actual_version_part in zip(min_split, actual_split):
        if int(actual_version_part) >= int(min_version_part):
            return
    raise ValueError("%s requires at least version %s, but version %s was "
                     "found." % (name, min_version, actual_version))


def main():
    verify_preconditions()
    scratch_dir = create_scratch_dir()
    package_dir = os.path.join(scratch_dir, 'packages')
    print("Bundle dir at: %s" % scratch_dir)
    download_package_tarballs(
        package_dir,
        packages=EXTRA_RUNTIME_DEPS,
    )

    # Some packages require setup time dependencies, and so we will need to
    # manually install them. We isolate them to a particular directory so we
    # can run the install before the things they're dependent on. We have to do
    # this because pip won't actually find them since it doesn't handle build
    # dependencies.
    setup_dir = os.path.join(package_dir, 'setup')
    download_package_tarballs(
        setup_dir,
        packages=BUILDTIME_DEPS,
    )
    download_cli_deps(package_dir)
    add_cli_sdist(package_dir)
    create_bootstrap_script(scratch_dir)
    zip_filename = zip_dir(scratch_dir)
    print("Zipped bundle installer is at: %s" % zip_filename)


if __name__ == '__main__':
    main()
