#!/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),
(3,7),
(3,8),
)
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 {} {}'.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 setuptools_scm which is needed for
# python-dateutils. We're also now installing setuptools since its no
# longer installed alongside pip for 3.12+.
for package in ['setuptools-', 'wheel', 'setuptools_scm']:
# these are actually wheels, but the bundle lookup logic is the same
tarball = _get_package_tarball(setup_package_dir, package)
run(
'{} -m pip install {} --find-links {} {}'.format(
python, INSTALL_ARGS, PACKAGES_DIR, 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.9 "
"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.9 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: We announced the end-of-support for the AWS CLI v1: '
'https://aws.amazon.com/blogs/developer/'
'cli-v1-maintenance-mode-announcement/',
'\nFor dates, additional details, and information on how to '
'migrate, please refer to the linked announcement.',
'\nWe recommend that you migrate to AWS CLI v2, see the',
'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()