Skip to content

Commit f048eb7

Browse files
authored
Merge pull request #6253 from cjerdonek/issue-6252
Fix an IndexError crash when a legacy build of a wheel fails.
2 parents fb944ab + 6cdecce commit f048eb7

File tree

4 files changed

+135
-22
lines changed

4 files changed

+135
-22
lines changed

news/6252.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix an ``IndexError`` crash when a legacy build of a wheel fails.

src/pip/_internal/wheel.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,42 @@ def should_use_ephemeral_cache(
779779
return True
780780

781781

782+
def get_legacy_build_wheel_path(
783+
names, # type: List[str]
784+
temp_dir, # type: str
785+
req, # type: InstallRequirement
786+
command_args, # type: List[str]
787+
command_output, # type: str
788+
):
789+
# type: (...) -> Optional[str]
790+
"""
791+
Return the path to the wheel in the temporary build directory.
792+
"""
793+
# Sort for determinism.
794+
names = sorted(names)
795+
if not names:
796+
# call_subprocess() ensures that the command output ends in a newline.
797+
msg = (
798+
'Failed building wheel for {} with args: {}\n'
799+
'Command output:\n{}'
800+
'-----------------------------------------'
801+
).format(req.name, command_args, command_output)
802+
logger.error(msg)
803+
return None
804+
if len(names) > 1:
805+
# call_subprocess() ensures that the command output ends in a newline.
806+
msg = (
807+
'Found more than one file after building wheel for {} '
808+
'with args: {}\n'
809+
'Names: {}\n'
810+
'Command output:\n{}'
811+
'-----------------------------------------'
812+
).format(req.name, command_args, names, command_output)
813+
logger.warning(msg)
814+
815+
return os.path.join(temp_dir, names[0])
816+
817+
782818
class WheelBuilder(object):
783819
"""Build wheels from a RequirementSet."""
784820

@@ -888,14 +924,21 @@ def _build_one_legacy(self, req, tempd, python_tag=None):
888924
wheel_args += ["--python-tag", python_tag]
889925

890926
try:
891-
call_subprocess(wheel_args, cwd=req.setup_py_dir,
892-
show_stdout=False, spinner=spinner)
927+
output = call_subprocess(wheel_args, cwd=req.setup_py_dir,
928+
show_stdout=False, spinner=spinner)
893929
except Exception:
894930
spinner.finish("error")
895931
logger.error('Failed building wheel for %s', req.name)
896932
return None
897-
# listdir's return value is sorted to be deterministic
898-
return os.path.join(tempd, sorted(os.listdir(tempd))[0])
933+
names = os.listdir(tempd)
934+
wheel_path = get_legacy_build_wheel_path(
935+
names=names,
936+
temp_dir=tempd,
937+
req=req,
938+
command_args=wheel_args,
939+
command_output=output,
940+
)
941+
return wheel_path
899942

900943
def _clean_one(self, req):
901944
base_args = self._base_setup_args(req)

tests/lib/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
pyversion_tuple = sys.version_info
2222

2323

24+
def assert_paths_equal(actual, expected):
25+
os.path.normpath(actual) == os.path.normpath(expected)
26+
27+
2428
def path_to_url(path):
2529
"""
2630
Convert a path to URI. The path will be made absolute and

tests/unit/test_wheel.py

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pip._internal.req.req_install import InstallRequirement
1616
from pip._internal.utils.compat import WINDOWS
1717
from pip._internal.utils.misc import unpack_file
18-
from tests.lib import DATA_DIR
18+
from tests.lib import DATA_DIR, assert_paths_equal
1919

2020

2121
@pytest.mark.parametrize(
@@ -38,25 +38,13 @@ def test_contains_egg_info(s, expected):
3838
assert result == expected
3939

4040

41-
@pytest.mark.parametrize(
42-
"base_name, autobuilding, cache_available, expected",
43-
[
44-
('pendulum-2.0.4', False, False, False),
45-
# The following cases test autobuilding=True.
46-
# Test _contains_egg_info() returning True.
47-
('pendulum-2.0.4', True, True, False),
48-
('pendulum-2.0.4', True, False, True),
49-
# Test _contains_egg_info() returning False.
50-
('pendulum', True, True, True),
51-
('pendulum', True, False, True),
52-
],
53-
)
54-
def test_should_use_ephemeral_cache__issue_6197(
55-
base_name, autobuilding, cache_available, expected,
56-
):
41+
def make_test_install_req(base_name=None):
5742
"""
58-
Regression test for: https://github.com/pypa/pip/issues/6197
43+
Return an InstallRequirement object for testing purposes.
5944
"""
45+
if base_name is None:
46+
base_name = 'pendulum-2.0.4'
47+
6048
req = Requirement('pendulum')
6149
link_url = (
6250
'https://files.pythonhosted.org/packages/aa/{base_name}.tar.gz'
@@ -76,6 +64,30 @@ def test_should_use_ephemeral_cache__issue_6197(
7664
link=link,
7765
source_dir='/tmp/pip-install-9py5m2z1/pendulum',
7866
)
67+
68+
return req
69+
70+
71+
@pytest.mark.parametrize(
72+
"base_name, autobuilding, cache_available, expected",
73+
[
74+
('pendulum-2.0.4', False, False, False),
75+
# The following cases test autobuilding=True.
76+
# Test _contains_egg_info() returning True.
77+
('pendulum-2.0.4', True, True, False),
78+
('pendulum-2.0.4', True, False, True),
79+
# Test _contains_egg_info() returning False.
80+
('pendulum', True, True, True),
81+
('pendulum', True, False, True),
82+
],
83+
)
84+
def test_should_use_ephemeral_cache__issue_6197(
85+
base_name, autobuilding, cache_available, expected,
86+
):
87+
"""
88+
Regression test for: https://github.com/pypa/pip/issues/6197
89+
"""
90+
req = make_test_install_req(base_name=base_name)
7991
assert not req.is_wheel
8092
assert req.link.is_artifact
8193

@@ -87,6 +99,59 @@ def test_should_use_ephemeral_cache__issue_6197(
8799
assert ephem_cache is expected
88100

89101

102+
def call_get_legacy_build_wheel_path(caplog, names):
103+
req = make_test_install_req()
104+
wheel_path = wheel.get_legacy_build_wheel_path(
105+
names=names,
106+
temp_dir='/tmp/abcd',
107+
req=req,
108+
command_args=['arg1', 'arg2'],
109+
command_output='output line 1\noutput line 2\n',
110+
)
111+
return wheel_path
112+
113+
114+
def test_get_legacy_build_wheel_path(caplog):
115+
actual = call_get_legacy_build_wheel_path(caplog, names=['name'])
116+
assert_paths_equal(actual, '/tmp/abcd/name')
117+
assert not caplog.records
118+
119+
120+
def test_get_legacy_build_wheel_path__no_names(caplog):
121+
actual = call_get_legacy_build_wheel_path(caplog, names=[])
122+
assert actual is None
123+
assert len(caplog.records) == 1
124+
record = caplog.records[0]
125+
assert record.levelname == 'ERROR'
126+
assert record.message.splitlines() == [
127+
"Failed building wheel for pendulum with args: ['arg1', 'arg2']",
128+
"Command output:",
129+
"output line 1",
130+
"output line 2",
131+
"-----------------------------------------",
132+
]
133+
134+
135+
def test_get_legacy_build_wheel_path__multiple_names(caplog):
136+
# Deliberately pass the names in non-sorted order.
137+
actual = call_get_legacy_build_wheel_path(
138+
caplog, names=['name2', 'name1'],
139+
)
140+
assert_paths_equal(actual, '/tmp/abcd/name1')
141+
assert len(caplog.records) == 1
142+
record = caplog.records[0]
143+
assert record.levelname == 'WARNING'
144+
assert record.message.splitlines() == [
145+
("Found more than one file after building wheel for pendulum "
146+
"with args: ['arg1', 'arg2']"),
147+
"Names: ['name1', 'name2']",
148+
"Command output:",
149+
"output line 1",
150+
"output line 2",
151+
"-----------------------------------------",
152+
]
153+
154+
90155
@pytest.mark.parametrize("console_scripts",
91156
["pip = pip._internal.main:pip",
92157
"pip:pip = pip._internal.main:pip"])

0 commit comments

Comments
 (0)