Skip to content

Commit e132012

Browse files
authored
fix(pip): allow for different extras for different target platforms (#3385)
With this PR we first evaluate the markers in the requirements files before going any further to aggregate them and process further. This makes the separation of logic a little bit more clear. I wanted to do this before I add more tests to after debugging the failures observed when enabling pipstar. Whilst cleaning up further I realized that I can fix the handling of packages where some platforms may end up not needing extras whilst others do. This is achieved by reusing the same code that allows us to have different versions per platform. Work towards #2949 Fixes #3374
1 parent 4fb634e commit e132012

File tree

5 files changed

+261
-74
lines changed

5 files changed

+261
-74
lines changed

python/private/pypi/hub_builder.bzl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,8 +566,13 @@ def _whl_repo(
566566
for p in src.target_platforms
567567
]
568568

569+
# TODO @aignas 2025-11-02: once we have pipstar enabled we can add extra
570+
# targets to each hub for each extra combination and solve this more cleanly as opposed to
571+
# duplicating whl_library repositories.
572+
target_platforms = src.target_platforms if is_multiple_versions else []
573+
569574
return struct(
570-
repo_name = whl_repo_name(src.filename, src.sha256),
575+
repo_name = whl_repo_name(src.filename, src.sha256, *target_platforms),
571576
args = args,
572577
config_setting = whl_config_setting(
573578
version = python_version,

python/private/pypi/parse_requirements.bzl

Lines changed: 38 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def parse_requirements(
8888
evaluate_markers = evaluate_markers or (lambda _ctx, _requirements: {})
8989
options = {}
9090
requirements = {}
91+
reqs_with_env_markers = {}
9192
for file, plats in requirements_by_platform.items():
9293
logger.trace(lambda: "Using {} for {}".format(file, plats))
9394
contents = ctx.read(file)
@@ -96,16 +97,41 @@ def parse_requirements(
9697
# needed for the whl_library declarations later.
9798
parse_result = parse_requirements_txt(contents)
9899

100+
tokenized_options = []
101+
for opt in parse_result.options:
102+
for p in opt.split(" "):
103+
tokenized_options.append(p)
104+
105+
pip_args = tokenized_options + extra_pip_args
106+
for plat in plats:
107+
requirements[plat] = parse_result.requirements
108+
for entry in parse_result.requirements:
109+
requirement_line = entry[1]
110+
111+
# output all of the requirement lines that have a marker
112+
if ";" in requirement_line:
113+
reqs_with_env_markers.setdefault(requirement_line, []).append(plat)
114+
options[plat] = pip_args
115+
116+
# This may call to Python, so execute it early (before calling to the
117+
# internet below) and ensure that we call it only once.
118+
resolved_marker_platforms = evaluate_markers(ctx, reqs_with_env_markers)
119+
logger.trace(lambda: "Evaluated env markers from:\n{}\n\nTo:\n{}".format(
120+
reqs_with_env_markers,
121+
resolved_marker_platforms,
122+
))
123+
124+
requirements_by_platform = {}
125+
for plat, parse_results in requirements.items():
99126
# Replicate a surprising behavior that WORKSPACE builds allowed:
100127
# Defining a repo with the same name multiple times, but only the last
101128
# definition is respected.
102129
# The requirement lines might have duplicate names because lines for extras
103130
# are returned as just the base package name. e.g., `foo[bar]` results
104131
# in an entry like `("foo", "foo[bar] == 1.0 ...")`.
105-
# Lines with different markers are not condidered duplicates.
106132
requirements_dict = {}
107133
for entry in sorted(
108-
parse_result.requirements,
134+
parse_results,
109135
# Get the longest match and fallback to original WORKSPACE sorting,
110136
# which should get us the entry with most extras.
111137
#
@@ -114,33 +140,22 @@ def parse_requirements(
114140
# should do this now.
115141
key = lambda x: (len(x[1].partition("==")[0]), x),
116142
):
117-
req = requirement(entry[1])
118-
requirements_dict[(req.name, req.version, req.marker)] = entry
143+
req_line = entry[1]
144+
req = requirement(req_line)
119145

120-
tokenized_options = []
121-
for opt in parse_result.options:
122-
for p in opt.split(" "):
123-
tokenized_options.append(p)
146+
if req.marker and plat not in resolved_marker_platforms.get(req_line, []):
147+
continue
124148

125-
pip_args = tokenized_options + extra_pip_args
126-
for plat in plats:
127-
requirements[plat] = requirements_dict.values()
128-
options[plat] = pip_args
149+
requirements_dict[req.name] = entry
129150

130-
requirements_by_platform = {}
131-
reqs_with_env_markers = {}
132-
for target_platform, reqs_ in requirements.items():
133-
extra_pip_args = options[target_platform]
151+
extra_pip_args = options[plat]
134152

135-
for distribution, requirement_line in reqs_:
153+
for distribution, requirement_line in requirements_dict.values():
136154
for_whl = requirements_by_platform.setdefault(
137155
normalize_name(distribution),
138156
{},
139157
)
140158

141-
if ";" in requirement_line:
142-
reqs_with_env_markers.setdefault(requirement_line, []).append(target_platform)
143-
144159
for_req = for_whl.setdefault(
145160
(requirement_line, ",".join(extra_pip_args)),
146161
struct(
@@ -151,20 +166,7 @@ def parse_requirements(
151166
extra_pip_args = extra_pip_args,
152167
),
153168
)
154-
for_req.target_platforms.append(target_platform)
155-
156-
# This may call to Python, so execute it early (before calling to the
157-
# internet below) and ensure that we call it only once.
158-
#
159-
# NOTE @aignas 2024-07-13: in the future, if this is something that we want
160-
# to do, we could use Python to parse the requirement lines and infer the
161-
# URL of the files to download things from. This should be important for
162-
# VCS package references.
163-
env_marker_target_platforms = evaluate_markers(ctx, reqs_with_env_markers)
164-
logger.trace(lambda: "Evaluated env markers from:\n{}\n\nTo:\n{}".format(
165-
reqs_with_env_markers,
166-
env_marker_target_platforms,
167-
))
169+
for_req.target_platforms.append(plat)
168170

169171
index_urls = {}
170172
if get_index_urls:
@@ -183,8 +185,7 @@ def parse_requirements(
183185
for name, reqs in sorted(requirements_by_platform.items()):
184186
requirement_target_platforms = {}
185187
for r in reqs.values():
186-
target_platforms = env_marker_target_platforms.get(r.requirement_line, r.target_platforms)
187-
for p in target_platforms:
188+
for p in r.target_platforms:
188189
requirement_target_platforms[p] = None
189190

190191
item = struct(
@@ -197,7 +198,6 @@ def parse_requirements(
197198
reqs = reqs,
198199
index_urls = index_urls,
199200
platforms = platforms,
200-
env_marker_target_platforms = env_marker_target_platforms,
201201
extract_url_srcs = extract_url_srcs,
202202
logger = logger,
203203
),
@@ -221,18 +221,13 @@ def _package_srcs(
221221
index_urls,
222222
platforms,
223223
logger,
224-
env_marker_target_platforms,
225224
extract_url_srcs):
226225
"""A function to return sources for a particular package."""
227226
srcs = {}
228227
for r in sorted(reqs.values(), key = lambda r: r.requirement_line):
229-
if ";" in r.requirement_line:
230-
target_platforms = env_marker_target_platforms.get(r.requirement_line, [])
231-
else:
232-
target_platforms = r.target_platforms
233228
extra_pip_args = tuple(r.extra_pip_args)
234229

235-
for target_platform in target_platforms:
230+
for target_platform in r.target_platforms:
236231
if platforms and target_platform not in platforms:
237232
fail("The target platform '{}' could not be found in {}".format(
238233
target_platform,

python/private/pypi/whl_repo_name.bzl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
load("//python/private:normalize_name.bzl", "normalize_name")
1919
load(":parse_whl_name.bzl", "parse_whl_name")
2020

21-
def whl_repo_name(filename, sha256):
21+
def whl_repo_name(filename, sha256, *target_platforms):
2222
"""Return a valid whl_library repo name given a distribution filename.
2323
2424
Args:
2525
filename: {type}`str` the filename of the distribution.
2626
sha256: {type}`str` the sha256 of the distribution.
27+
*target_platforms: {type}`list[str]` the extra suffixes to append.
28+
Only used when we need to support different extras per version.
2729
2830
Returns:
2931
a string that can be used in {obj}`whl_library`.
@@ -59,6 +61,8 @@ def whl_repo_name(filename, sha256):
5961
elif version:
6062
parts.insert(1, version)
6163

64+
parts.extend([p.partition("_")[-1] for p in target_platforms])
65+
6266
return "_".join(parts)
6367

6468
def pypi_repo_name(whl_name, *target_platforms):

0 commit comments

Comments
 (0)