#!/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-build-isolation --no-binary :all:' 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 %s %s' % ( 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()