Skip to content

Commit d075021

Browse files
authored
Fix installing setup dependencies for bundled installer on newer versions of pip and Python (#9420)
1 parent 7278e05 commit d075021

File tree

5 files changed

+147
-45
lines changed

5 files changed

+147
-45
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "bugfix",
3+
"category": "bundled-installer",
4+
"description": "Fix installing setup dependencies on newer versions of pip"
5+
}

.github/workflows/run-bundle-test.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Run bundle test
2+
3+
on:
4+
push:
5+
pull_request:
6+
branches-ignore: [ master ]
7+
8+
jobs:
9+
test-bundle:
10+
runs-on: ${{ matrix.os }}
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
python-version: ["3.9", "3.10", "3.11", "3.12"]
15+
os: [ubuntu-latest, macOS-latest]
16+
steps:
17+
- uses: actions/checkout@v4
18+
- name: Set up Python ${{ matrix.python-version }}
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
- name: Install dependencies
23+
run: python scripts/ci/install
24+
- name: Install additional dependencies
25+
run: pip install virtualenv==16.3.0 setuptools-scm==3.3.3 # same as internal generate-bundle.ts
26+
- name: Test the bundle
27+
run: python scripts/ci/test-bundle

scripts/ci/test-bundle

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python
2+
# Don't run tests from the root repo dir.
3+
# We want to ensure we're importing from the installed
4+
# binary package not from the CWD.
5+
6+
import os
7+
import re
8+
from subprocess import check_output
9+
from awscli.testutils import cd
10+
11+
_dname = os.path.dirname
12+
13+
REPO_ROOT = _dname(_dname(_dname(os.path.abspath(__file__))))
14+
15+
16+
def run(command):
17+
print(f'Running {command}')
18+
return check_output(command, shell=True)
19+
20+
21+
def run_make_bundle():
22+
"""
23+
Builds the bundled installer, and returns its path
24+
"""
25+
output = run(f'{REPO_ROOT}/scripts/make-bundle')
26+
match = re.search(
27+
r'Zipped bundle installer is at: (.+?\.zip)', output.decode('utf-8')
28+
)
29+
if not match:
30+
raise RuntimeError("Could not find bundle path in make-bundle output")
31+
32+
return match.group(1)
33+
34+
35+
def install_from_bundle(zip_path):
36+
run(f'unzip -o {bundle_path}')
37+
path_without_zip = bundle_path[:-4]
38+
run(
39+
f'sudo {path_without_zip}/install -i /usr/local/aws -b /usr/local/bin/aws'
40+
)
41+
42+
43+
def verify_installation():
44+
version_output = run("aws --version")
45+
print(f"Installed AWS CLI version: {version_output}")
46+
47+
48+
if __name__ == "__main__":
49+
with cd(os.path.join(REPO_ROOT)):
50+
bundle_path = run_make_bundle()
51+
install_from_bundle(bundle_path)
52+
verify_installation()

scripts/install

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def pip_install_packages(install_dir):
150150

151151
with cd(PACKAGES_DIR):
152152
run(
153-
'{} -m pip install {} --find-links file://{} {}'.format(
153+
'{} -m pip install {} --find-links {} {}'.format(
154154
python, INSTALL_ARGS, PACKAGES_DIR, cli_tarball
155155
)
156156
)
@@ -160,24 +160,17 @@ def _install_setup_deps(python, setup_package_dir):
160160
# Some packages declare `setup_requires`, which is a list of dependencies
161161
# to be used at setup time. These need to be installed before anything
162162
# else, and pip doesn't manage them. We have to manage this ourselves
163-
# so for now we're explicitly installing the one setup_requires package
164-
# we need. This comes from python-dateutils.
165-
setuptools_scm_tarball = _get_package_tarball(
166-
setup_package_dir, 'setuptools_scm'
167-
)
168-
run(
169-
(
170-
'{} -m pip install --no-binary :all: --no-cache-dir --no-index '
171-
'--find-links file://{} {}'
172-
).format(python, setup_package_dir, setuptools_scm_tarball)
173-
)
174-
wheel_tarball = _get_package_tarball(setup_package_dir, 'wheel')
175-
run(
176-
(
177-
'{} -m pip install --no-binary :all: --no-cache-dir --no-index '
178-
'--find-links file://{} {}'
179-
).format(python, setup_package_dir, wheel_tarball)
180-
)
163+
# so for now we're explicitly installing setuptools_scm which is needed for
164+
# python-dateutils. We're also now installing setuptools since its no
165+
# longer installed alongside pip for 3.12+.
166+
for package in ['setuptools-', 'wheel', 'setuptools_scm']:
167+
# these are actually wheels, but the bundle lookup logic is the same
168+
tarball = _get_package_tarball(setup_package_dir, package)
169+
run(
170+
'{} -m pip install {} --find-links {} {}'.format(
171+
python, INSTALL_ARGS, PACKAGES_DIR, tarball
172+
)
173+
)
181174

182175

183176
def create_symlink(real_location, symlink_name):

scripts/make-bundle

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface for those not familiar with the python
1212
ecosystem.
1313
1414
"""
15+
1516
import os
1617
import sys
1718
import subprocess
@@ -31,10 +32,13 @@ PINNED_RUNTIME_DEPS = [
3132
# require extra build time dependencies. We are pinning it to
3233
# a version that does not need those.
3334
('colorama', '0.4.5'),
35+
# 2.0.0 of urllib3 started requiring hatchling as well
36+
('urllib3', '1.26.20'),
3437
]
3538
BUILDTIME_DEPS = [
39+
('setuptools', '75.4.0'), # start of >= 3.9
3640
('setuptools-scm', '3.3.3'),
37-
('wheel', '0.33.6'),
41+
('wheel', '0.45.1'), # 0.46.0+ requires packaging
3842
]
3943
PIP_DOWNLOAD_ARGS = '--no-build-isolation --no-binary :all:'
4044

@@ -54,14 +58,14 @@ def cd(dirname):
5458

5559

5660
def run(cmd):
57-
sys.stdout.write("Running cmd: %s\n" % cmd)
58-
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
59-
stderr=subprocess.PIPE)
61+
sys.stdout.write(f"Running cmd: {cmd}\n")
62+
p = subprocess.Popen(
63+
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
64+
)
6065
stdout, stderr = p.communicate()
6166
rc = p.wait()
6267
if p.returncode != 0:
63-
raise BadRCError("Bad rc (%s) for cmd '%s': %s" % (
64-
rc, cmd, stderr + stdout))
68+
raise BadRCError(f"Bad rc ({rc}) for cmd '{cmd}': {stderr + stdout}")
6569
return stdout
6670

6771

@@ -79,17 +83,33 @@ def create_scratch_dir():
7983
def download_package_tarballs(dirname, packages):
8084
with cd(dirname):
8185
for package, package_version in packages:
82-
run('%s -m pip download %s==%s %s' % (
83-
sys.executable, package, package_version, PIP_DOWNLOAD_ARGS
84-
))
86+
run(
87+
f'{sys.executable} -m pip download {package}=={package_version}'
88+
f' {PIP_DOWNLOAD_ARGS}'
89+
)
90+
91+
92+
def download_package_wheels(dirname, packages):
93+
with cd(dirname):
94+
for package, package_version in packages:
95+
run(
96+
f'{sys.executable} -m pip download {package}=={package_version}'
97+
f' --only-binary :all:'
98+
)
99+
100+
101+
def validate_that_wheels_are_universal(dirname):
102+
with cd(dirname):
103+
for wheel_path in os.listdir():
104+
if not wheel_path.endswith('py3-none-any.whl'):
105+
raise ValueError(f'Found a non universal wheel: {wheel_path}')
85106

86107

87108
def download_cli_deps(scratch_dir, packages):
88109
# pip download will always download a more recent version of a package
89110
# even if one exists locally. The list of packages supplied in `packages`
90111
# forces the use of a specific runtime dependency.
91-
awscli_dir = os.path.dirname(
92-
os.path.dirname(os.path.abspath(__file__)))
112+
awscli_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
93113
pinned_packages = " ".join(
94114
f"{name}=={version}" for (name, version) in packages
95115
)
@@ -104,20 +124,21 @@ def _remove_cli_zip(scratch_dir):
104124

105125

106126
def add_cli_sdist(scratch_dir):
107-
awscli_dir = os.path.dirname(
108-
os.path.dirname(os.path.abspath(__file__)))
127+
awscli_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
109128
if os.path.exists(os.path.join(awscli_dir, 'dist')):
110129
shutil.rmtree(os.path.join(awscli_dir, 'dist'))
111130
with cd(awscli_dir):
112-
run('%s setup.py sdist' % sys.executable)
131+
run(f'{sys.executable} setup.py sdist')
113132
filename = os.listdir('dist')[0]
114-
shutil.move(os.path.join('dist', filename),
115-
os.path.join(scratch_dir, filename))
133+
shutil.move(
134+
os.path.join('dist', filename), os.path.join(scratch_dir, filename)
135+
)
116136

117137

118138
def create_bootstrap_script(scratch_dir):
119139
install_script = os.path.join(
120-
os.path.dirname(os.path.abspath(__file__)), 'install')
140+
os.path.dirname(os.path.abspath(__file__)), 'install'
141+
)
121142
shutil.copy(install_script, os.path.join(scratch_dir, 'install'))
122143

123144

@@ -139,11 +160,11 @@ def zip_dir(scratch_dir):
139160
def verify_preconditions():
140161
# The pip version looks like:
141162
# 'pip 1.4.1 from ....'
142-
pip_version = run(
143-
'%s -m pip --version' % sys.executable).strip().split()[1]
163+
pip_version = run(f'{sys.executable} -m pip --version').strip().split()[1]
144164
# Virtualenv version just has the version string: '1.14.5\n'
145165
virtualenv_version = run(
146-
'%s -m virtualenv --version' % sys.executable).strip()
166+
f'{sys.executable} -m virtualenv --version'
167+
).strip()
147168
_min_version_required('9.0.1', pip_version, 'pip')
148169
_min_version_required('15.1.0', virtualenv_version, 'virtualenv')
149170

@@ -156,15 +177,17 @@ def _min_version_required(min_version, actual_version, name):
156177
for min_version_part, actual_version_part in zip(min_split, actual_split):
157178
if int(actual_version_part) >= int(min_version_part):
158179
return
159-
raise ValueError("%s requires at least version %s, but version %s was "
160-
"found." % (name, min_version, actual_version))
180+
raise ValueError(
181+
f'{name} requires at least version {min_version}, '
182+
f'but version {actual_version} was found.'
183+
)
161184

162185

163186
def main():
164187
verify_preconditions()
165188
scratch_dir = create_scratch_dir()
166189
package_dir = os.path.join(scratch_dir, 'packages')
167-
print("Bundle dir at: %s" % scratch_dir)
190+
print(f"Bundle dir at: {scratch_dir}")
168191
download_package_tarballs(
169192
package_dir,
170193
packages=EXTRA_RUNTIME_DEPS,
@@ -174,17 +197,19 @@ def main():
174197
# manually install them. We isolate them to a particular directory so we
175198
# can run the install before the things they're dependent on. We have to do
176199
# this because pip won't actually find them since it doesn't handle build
177-
# dependencies.
200+
# dependencies. We use wheels for this, to avoid bootstrapping setuptools
201+
# in 3.12+ where it's no longer included by default.
178202
setup_dir = os.path.join(package_dir, 'setup')
179-
download_package_tarballs(
203+
download_package_wheels(
180204
setup_dir,
181205
packages=BUILDTIME_DEPS,
182206
)
207+
validate_that_wheels_are_universal(setup_dir)
183208
download_cli_deps(package_dir, packages=PINNED_RUNTIME_DEPS)
184209
add_cli_sdist(package_dir)
185210
create_bootstrap_script(scratch_dir)
186211
zip_filename = zip_dir(scratch_dir)
187-
print("Zipped bundle installer is at: %s" % zip_filename)
212+
print(f"Zipped bundle installer is at: {zip_filename}")
188213

189214

190215
if __name__ == '__main__':

0 commit comments

Comments
 (0)