#!/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')) UNSUPPORTED_PYTHON = ( (2,6), (2,7), (3,3), (3,4), (3,5), (3,6), ) 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(): py_36_params = { 'date': 'May 30, 2022', 'blog_link': ( 'https://aws.amazon.com/blogs/developer/' 'python-support-policy-updates-for-aws-sdks-and-tools/' ) } return { # Example for future deprecations # (3, 6): py_36_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 _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] python = os.path.join(install_dir, bin_path(), 'python') setup_requires_dir = os.path.join(PACKAGES_DIR, 'setup') with cd(setup_requires_dir): _install_setup_deps(python, '.') with cd(PACKAGES_DIR): run( '{} -m pip install {} --find-links file://{} {}'.format( python, INSTALL_ARGS, PACKAGES_DIR, cli_tarball ) ) def _install_setup_deps(python, 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( ( '{} -m pip install --no-binary :all: --no-cache-dir --no-index ' '--find-links file://{} {}' ).format(python, setup_package_dir, setuptools_scm_tarball) ) wheel_tarball = _get_package_tarball(setup_package_dir, 'wheel') run( ( '{} -m pip install --no-binary :all: --no-cache-dir --no-index ' '--find-links file://{} {}' ).format(python, 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.7 " "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.7 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) create_virtualenv = _create_virtualenv_internal if __name__ == '__main__': main()