File: //usr/local/src/awscli-bundle/install
#!/usr/bin/env python
# We're using optparse because we need to support 2.6
# which doesn't have argparse. Given that argparse is
# a dependency that eventually gets installed, we could
# try to bootstrap, but using optparse is just easier.
from __future__ import print_function
import optparse
import os
import platform
import shutil
import subprocess
import sys
import tarfile
import tempfile
from contextlib import contextmanager
PACKAGES_DIR = os.path.join(
os.path.dirname(os.path.abspath(__file__)), 'packages')
INSTALL_DIR = os.path.expanduser(os.path.join(
'~', '.local', 'lib', 'aws'))
GTE_PY37 = sys.version_info[:2] >= (3, 7)
UNSUPPORTED_PYTHON = (
(2,6),
(2,7),
(3,3),
(3,4),
(3,5),
)
INSTALL_ARGS = (
'--no-binary :all: --no-build-isolation --no-cache-dir --no-index '
)
class BadRCError(Exception):
pass
class MultipleBundlesError(Exception):
pass
class PythonDeprecationWarning(Warning):
"""
Python version being used is scheduled to become unsupported
in an future release. See warning for specifics.
"""
pass
def _build_deprecations():
# Example Template
# py_27_params = {
# 'date': 'July 15, 2021',
# 'blog_link': 'https://aws.amazon.com/blogs/developer/announcing-end-'
# 'of-support-for-python-2-7-in-aws-sdk-for-python-and-'
# 'aws-cli-v1/'
# }
return {
# (2,7): py_27_params
}
DEPRECATED_PYTHON = _build_deprecations()
@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()
if p.returncode != 0:
output = (stdout + stderr).decode("utf-8")
raise BadRCError("Bad rc (%s) for cmd '%s': %s" % (
p.returncode, cmd, output))
return stdout
def bin_path():
"""
Get the system's binary path, either `bin` on reasonable
systems or `Scripts` on Windows.
"""
path = 'bin'
if platform.system() == 'Windows':
path = 'Scripts'
return path
def create_install_structure(working_dir, install_dir):
if not os.path.isdir(install_dir):
os.makedirs(install_dir)
create_virtualenv(location=install_dir, working_dir=working_dir)
def _create_virtualenv_internal(location, working_dir):
# On py3 we use the built in venv to create our virtualenv.
# There's a bug with sys.executable on external virtualenv
# that causes installation failures.
run('%s -m venv %s' % (sys.executable, location))
def _create_virtualenv_external(location, working_dir):
# working_dir is used (generally somewhere in /tmp) so that we
# don't modify the install/packages directories.
with cd(PACKAGES_DIR):
venv = _get_venv_package_tarball('.')
compressed = tarfile.open(venv)
compressed.extractall(path=working_dir)
compressed.close()
with cd(working_dir):
# We know that virtualenv is the only dir in this directory
# so we can listdir()[0] it.
with cd(os.listdir('.')[0]):
run(('%s virtualenv.py --no-download '
'--python %s %s') % (sys.executable,
sys.executable,
location))
def _get_package_tarball(package_dir, package_prefix):
package_filenames = sorted([p for p in os.listdir(package_dir)
if p.startswith(package_prefix)])
return package_filenames[-1]
def _get_venv_package_tarball(package_dir):
return _get_package_tarball(package_dir, 'virtualenv')
def create_working_dir():
d = tempfile.mkdtemp()
return d
def pip_install_packages(install_dir):
cli_tarball = [p for p in os.listdir(PACKAGES_DIR)
if p.startswith('awscli')]
if len(cli_tarball) != 1:
message = (
"Multiple versions of the CLI were found in %s. Please clear "
"out this directory before proceeding."
)
raise MultipleBundlesError(message % PACKAGES_DIR)
cli_tarball = cli_tarball[0]
pip_script = os.path.join(install_dir, bin_path(), 'pip')
setup_requires_dir = os.path.join(PACKAGES_DIR, 'setup')
with cd(setup_requires_dir):
_install_setup_deps(pip_script, '.')
with cd(PACKAGES_DIR):
run('%s install %s --find-links file://%s %s' % (
pip_script, INSTALL_ARGS, PACKAGES_DIR, cli_tarball))
def _install_setup_deps(pip_script, setup_package_dir):
# Some packages declare `setup_requires`, which is a list of dependencies
# to be used at setup time. These need to be installed before anything
# else, and pip doesn't manage them. We have to manage this ourselves
# so for now we're explicitly installing the one setup_requires package
# we need. This comes from python-dateutils.
setuptools_scm_tarball = _get_package_tarball(
setup_package_dir, 'setuptools_scm')
run('%s install --no-binary :all: --no-cache-dir --no-index '
'--find-links file://%s %s' % (
pip_script, setup_package_dir, setuptools_scm_tarball))
wheel_tarball = _get_package_tarball(
setup_package_dir, 'wheel')
run('%s install --no-binary :all: --no-cache-dir --no-index '
'--find-links file://%s %s' % (
pip_script, setup_package_dir, wheel_tarball))
def create_symlink(real_location, symlink_name):
if os.path.isfile(symlink_name):
print("Symlink already exists: %s" % symlink_name)
print("Removing symlink.")
os.remove(symlink_name)
symlink_dir_name = os.path.dirname(symlink_name)
if not os.path.isdir(symlink_dir_name):
os.makedirs(symlink_dir_name)
os.symlink(real_location, symlink_name)
return True
def main():
parser = optparse.OptionParser()
parser.add_option('-i', '--install-dir', help="The location to install "
"the AWS CLI. The default value is ~/.local/lib/aws",
default=INSTALL_DIR)
parser.add_option('-b', '--bin-location', help="If this argument is "
"provided, then a symlink will be created at this "
"location that points to the aws executable. "
"This argument is useful if you want to put the aws "
"executable somewhere already on your path, e.g. "
"-b /usr/local/bin/aws. This is an optional argument. "
"If you do not provide this argument you will have to "
"add INSTALL_DIR/bin to your PATH.")
py_version = sys.version_info[:2]
if py_version in UNSUPPORTED_PYTHON:
unsupported_python_msg = (
"Unsupported Python version detected: Python {}.{}\n"
"To continue using this installer you must use Python 3.6 "
"or later.\n"
"For more information see the following blog post: "
"https://aws.amazon.com/blogs/developer/announcing-end-"
"of-support-for-python-2-7-in-aws-sdk-for-python-and-"
"aws-cli-v1/\n"
).format(py_version[0], py_version[1])
print(unsupported_python_msg, file=sys.stderr)
sys.exit(1)
if py_version in DEPRECATED_PYTHON:
params = DEPRECATED_PYTHON[py_version]
deprecated_python_msg = (
"Deprecated Python version detected: Python {}.{}\n"
"Starting {}, the AWS CLI will no longer support "
"this version of Python. To continue receiving service updates, "
"bug fixes, and security updates please upgrade to Python 3.6 or "
"later. More information can be found here: {}"
).format(
py_version[0], py_version[1], params['date'], params['blog_link']
)
print(deprecated_python_msg, file=sys.stderr)
opts = parser.parse_args()[0]
working_dir = create_working_dir()
try:
create_install_structure(working_dir, opts.install_dir)
pip_install_packages(opts.install_dir)
real_location = os.path.join(opts.install_dir, bin_path(), 'aws')
if opts.bin_location and create_symlink(real_location,
opts.bin_location):
print("You can now run: %s --version" % opts.bin_location)
else:
print("You can now run: %s --version" % real_location)
print('\nNote: AWS CLI version 2, the latest major version '
'of the AWS CLI, is now stable and recommended for general '
'use. For more information, see the AWS CLI version 2 '
'installation instructions at: https://docs.aws.amazon.com/cli/'
'latest/userguide/install-cliv2.html')
finally:
shutil.rmtree(working_dir)
if GTE_PY37:
create_virtualenv = _create_virtualenv_internal
else:
create_virtualenv = _create_virtualenv_external
if __name__ == '__main__':
main()