1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
|
#!/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 shutil
import subprocess
import sys
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()
|