1 """Orchestrator for building wheels from InstallRequirements.
8 from typing
import Iterable
, List
, Optional
, Tuple
10 from pip
._vendor
.packaging
.utils
import canonicalize_name
, canonicalize_version
11 from pip
._vendor
.packaging
.version
import InvalidVersion
, Version
13 from pip
._internal
.cache
import WheelCache
14 from pip
._internal
.exceptions
import InvalidWheelFilename
, UnsupportedWheel
15 from pip
._internal
.metadata
import FilesystemWheel
, get_wheel_distribution
16 from pip
._internal
.models
.link
import Link
17 from pip
._internal
.models
.wheel
import Wheel
18 from pip
._internal
.operations
.build
.wheel
import build_wheel_pep517
19 from pip
._internal
.operations
.build
.wheel_editable
import build_wheel_editable
20 from pip
._internal
.operations
.build
.wheel_legacy
import build_wheel_legacy
21 from pip
._internal
.req
.req_install
import InstallRequirement
22 from pip
._internal
.utils
.logging
import indent_log
23 from pip
._internal
.utils
.misc
import ensure_dir
, hash_file
24 from pip
._internal
.utils
.setuptools_build
import make_setuptools_clean_args
25 from pip
._internal
.utils
.subprocess
import call_subprocess
26 from pip
._internal
.utils
.temp_dir
import TempDirectory
27 from pip
._internal
.utils
.urls
import path_to_url
28 from pip
._internal
.vcs
import vcs
30 logger
= logging
.getLogger(__name__
)
32 _egg_info_re
= re
.compile(r
"([a-z0-9_.]+)-([a-z0-9_.!+-]+)", re
.IGNORECASE
)
34 BuildResult
= Tuple
[List
[InstallRequirement
], List
[InstallRequirement
]]
37 def _contains_egg_info(s
: str) -> bool:
38 """Determine whether the string looks like an egg_info.
40 :param s: The string to parse. E.g. foo-2.1
42 return bool(_egg_info_re
.search(s
))
46 req
: InstallRequirement
,
49 """Return whether an InstallRequirement should be built into a wheel."""
51 # never build requirements that are merely constraints
56 "Skipping %s, due to already being wheel.",
62 # i.e. pip wheel, not pip install
65 # From this point, this concerns the pip install command only
68 if not req
.source_dir
:
72 # we only build PEP 660 editable requirements
73 return req
.supports_pyproject_editable()
78 def should_build_for_wheel_command(
79 req
: InstallRequirement
,
81 return _should_build(req
, need_wheel
=True)
84 def should_build_for_install_command(
85 req
: InstallRequirement
,
87 return _should_build(req
, need_wheel
=False)
91 req
: InstallRequirement
,
94 Return whether a built InstallRequirement can be stored in the persistent
95 wheel cache, assuming the wheel cache is available, and _should_build()
96 has determined a wheel needs to be built.
98 if req
.editable
or not req
.source_dir
:
99 # never cache editable requirements
102 if req
.link
and req
.link
.is_vcs
:
103 # VCS checkout. Do not cache
104 # unless it points to an immutable commit hash.
105 assert not req
.editable
106 assert req
.source_dir
107 vcs_backend
= vcs
.get_backend_for_scheme(req
.link
.scheme
)
109 if vcs_backend
.is_immutable_rev_checkout(req
.link
.url
, req
.source_dir
):
114 base
, ext
= req
.link
.splitext()
115 if _contains_egg_info(base
):
118 # Otherwise, do not cache.
123 req
: InstallRequirement
,
124 wheel_cache
: WheelCache
,
126 """Return the persistent or temporary cache directory where the built
127 wheel need to be stored.
129 cache_available
= bool(wheel_cache
.cache_dir
)
131 if cache_available
and _should_cache(req
):
132 cache_dir
= wheel_cache
.get_path_for_link(req
.link
)
134 cache_dir
= wheel_cache
.get_ephem_path_for_link(req
.link
)
138 def _verify_one(req
: InstallRequirement
, wheel_path
: str) -> None:
139 canonical_name
= canonicalize_name(req
.name
or "")
140 w
= Wheel(os
.path
.basename(wheel_path
))
141 if canonicalize_name(w
.name
) != canonical_name
:
142 raise InvalidWheelFilename(
143 "Wheel has unexpected file name: expected {!r}, "
144 "got {!r}".format(canonical_name
, w
.name
),
146 dist
= get_wheel_distribution(FilesystemWheel(wheel_path
), canonical_name
)
147 dist_verstr
= str(dist
.version
)
148 if canonicalize_version(dist_verstr
) != canonicalize_version(w
.version
):
149 raise InvalidWheelFilename(
150 "Wheel has unexpected file name: expected {!r}, "
151 "got {!r}".format(dist_verstr
, w
.version
),
153 metadata_version_value
= dist
.metadata_version
154 if metadata_version_value
is None:
155 raise UnsupportedWheel("Missing Metadata-Version")
157 metadata_version
= Version(metadata_version_value
)
158 except InvalidVersion
:
159 msg
= f
"Invalid Metadata-Version: {metadata_version_value}"
160 raise UnsupportedWheel(msg
)
161 if metadata_version
>= Version("1.2") and not isinstance(dist
.version
, Version
):
162 raise UnsupportedWheel(
163 "Metadata 1.2 mandates PEP 440 version, "
164 "but {!r} is not".format(dist_verstr
)
169 req
: InstallRequirement
,
172 build_options
: List
[str],
173 global_options
: List
[str],
178 :return: The filename of the built wheel, or None if the build failed.
180 artifact
= "editable" if editable
else "wheel"
182 ensure_dir(output_dir
)
185 "Building %s for %s failed: %s",
192 # Install build deps into temporary directory (PEP 518)
194 wheel_path
= _build_one_inside_env(
195 req
, output_dir
, build_options
, global_options
, editable
197 if wheel_path
and verify
:
199 _verify_one(req
, wheel_path
)
200 except (InvalidWheelFilename
, UnsupportedWheel
) as e
:
201 logger
.warning("Built %s for %s is invalid: %s", artifact
, req
.name
, e
)
206 def _build_one_inside_env(
207 req
: InstallRequirement
,
209 build_options
: List
[str],
210 global_options
: List
[str],
213 with TempDirectory(kind
="wheel") as temp_dir
:
216 assert req
.metadata_directory
217 assert req
.pep517_backend
220 "Ignoring --global-option when building %s using PEP 517", req
.name
224 "Ignoring --build-option when building %s using PEP 517", req
.name
227 wheel_path
= build_wheel_editable(
229 backend
=req
.pep517_backend
,
230 metadata_directory
=req
.metadata_directory
,
234 wheel_path
= build_wheel_pep517(
236 backend
=req
.pep517_backend
,
237 metadata_directory
=req
.metadata_directory
,
241 wheel_path
= build_wheel_legacy(
243 setup_py_path
=req
.setup_py_path
,
244 source_dir
=req
.unpacked_source_directory
,
245 global_options
=global_options
,
246 build_options
=build_options
,
250 if wheel_path
is not None:
251 wheel_name
= os
.path
.basename(wheel_path
)
252 dest_path
= os
.path
.join(output_dir
, wheel_name
)
254 wheel_hash
, length
= hash_file(wheel_path
)
255 shutil
.move(wheel_path
, dest_path
)
257 "Created wheel for %s: filename=%s size=%d sha256=%s",
261 wheel_hash
.hexdigest(),
263 logger
.info("Stored in directory: %s", output_dir
)
265 except Exception as e
:
267 "Building wheel for %s failed: %s",
271 # Ignore return, we can't do anything else useful.
272 if not req
.use_pep517
:
273 _clean_one_legacy(req
, global_options
)
277 def _clean_one_legacy(req
: InstallRequirement
, global_options
: List
[str]) -> bool:
278 clean_args
= make_setuptools_clean_args(
280 global_options
=global_options
,
283 logger
.info("Running setup.py clean for %s", req
.name
)
286 clean_args
, command_desc
="python setup.py clean", cwd
=req
.source_dir
290 logger
.error("Failed cleaning build dir for %s", req
.name
)
295 requirements
: Iterable
[InstallRequirement
],
296 wheel_cache
: WheelCache
,
298 build_options
: List
[str],
299 global_options
: List
[str],
303 :return: The list of InstallRequirement that succeeded to build and
304 the list of InstallRequirement that failed to build.
311 "Building wheels for collected packages: %s",
312 ", ".join(req
.name
for req
in requirements
), # type: ignore
316 build_successes
, build_failures
= [], []
317 for req
in requirements
:
319 cache_dir
= _get_cache_dir(req
, wheel_cache
)
320 wheel_file
= _build_one(
326 req
.editable
and req
.permit_editable_wheels
,
329 # Record the download origin in the cache
330 if req
.download_info
is not None:
331 # download_info is guaranteed to be set because when we build an
332 # InstallRequirement it has been through the preparer before, but
334 wheel_cache
.record_download_origin(cache_dir
, req
.download_info
)
335 # Update the link for this.
336 req
.link
= Link(path_to_url(wheel_file
))
337 req
.local_file_path
= req
.link
.file_path
338 assert req
.link
.is_wheel
339 build_successes
.append(req
)
341 build_failures
.append(req
)
343 # notify success/failure
346 "Successfully built %s",
347 " ".join([req
.name
for req
in build_successes
]), # type: ignore
351 "Failed to build %s",
352 " ".join([req
.name
for req
in build_failures
]), # type: ignore
354 # Return a list of requirements that failed to build
355 return build_successes
, build_failures