Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/concepts/bootstrapper-architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ Bootstrap Phase Flow

Every package passes through a sequence of phases. Source packages
traverse the full pipeline; prebuilt wheels skip the build phases.
``pypi-prebuilt`` and ``hook-prebuilt`` use the selected source resolver to
download a fresh wheel in ``PrepareSource``. The legacy ``pre_built`` setting
keeps its existing wheel download path. Both routes update the local mirror
and record the package as prebuilt in the graph and build order.
When a local wheel is already available, bootstrap reuses it before asking
the selected provider to resolve a fresh wheel.

.. code-block:: text

Expand Down
10 changes: 10 additions & 0 deletions docs/concepts/package-settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ cannot be combined with package-level ``download_source`` or
``resolver_dist``. An explicit ``pre_built: false`` also conflicts;
an omitted ``pre_built`` does not.

``PackageBuildInfo.pre_built`` follows the selected ``source`` resolver's
``resolves_prebuilt_wheel`` capability. Without ``source``, it reads the
legacy variant ``pre_built`` setting. Configured prebuilt resolvers download
fresh wheels through their ``download()`` contract; legacy prebuilt packages
keep the wheel downloader.

.. versionchanged:: 0.102.0
Configured prebuilt source resolvers now select the prebuilt pipeline
during bootstrap and later builds.

.. versionchanged:: 0.102.0
Source and legacy settings mixed across package and variant levels
now raise a validation error when package settings load.
Expand Down
16 changes: 14 additions & 2 deletions docs/concepts/resolver-architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ the cache already contains a matching version.
The configured wheel cache server is not a version-resolution fallback.
After a version has been selected, the bootstrapper checks the cache
server separately for a previously built wheel.
For prebuilt packages, the local wheel server can supply a cached wheel
before a fresh resolution through the selected source provider.

Provider Hierarchy
------------------
Expand Down Expand Up @@ -132,7 +134,15 @@ The separate ``step prepare-source`` invocation accepts ``--source-kind`` and
``step download-source-archive --output-format json`` exposes that result's
actual path and kind without changing its default path-only output.
Without ``--source-kind``, it keeps the legacy archive behavior.
Integrating prebuilt detection into the bootstrapper remains a separate step.
Configured prebuilt resolvers select the prebuilt bootstrap path. Fresh
external wheels use their ``download()`` contract and are staged in the
prebuilt wheel directory. Bootstrap, build, and download-sequence use the
direct wheel downloader for local mirror URLs, so they do not rerun the
configured download hook. Test-mode fallback also downloads directly.
When a fresh configured wheel shares a filename with an older cached wheel,
the local mirror and later build replay use the fresh staged wheel.
Legacy build downloads still pass through the build wheel directory before
the mirror moves them to downloads.
See :ref:`download_source_hook` for the return contract and compatibility rules.

.. versionchanged:: 0.102.0
Expand All @@ -143,7 +153,9 @@ See :ref:`download_source_hook` for the return contract and compatibility rules.
``build_sdist: tarball``. PyPI sdist resolvers
repack a tarball. Both configured strategies take precedence over an installed
``build_sdist`` override and log a warning when it is ignored. Legacy
settings continue to use the override.
settings continue to use the override. Configured prebuilt profiles use
their source provider after a local cache check, and local mirror URLs
bypass configured download hooks during replay.

Version Filtering Window
-------------------------
Expand Down
63 changes: 39 additions & 24 deletions src/fromager/bootstrap_requirement_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import threading
import typing

import requests.exceptions
from packaging.requirements import Requirement
from packaging.utils import NormalizedName, canonicalize_name
from packaging.version import Version
from resolvelib.resolvers import ResolverException

from . import resolver, sources, wheels
from .dependency_graph import DependencyGraph
Expand Down Expand Up @@ -171,13 +173,17 @@ def _resolve_and_extend(
parent_req=parent_req,
)

source_resolver = self.ctx.package_build_info(req).source_resolver
configured_prebuilt = (
source_resolver is not None and source_resolver.resolves_prebuilt_wheel
)
if cached_resolution:
logger.debug(
f"resolved from previous bootstrap: {len(cached_resolution)} version(s)"
)
results = cached_resolution
elif pre_built:
# Resolve prebuilt wheel
elif pre_built and not configured_prebuilt:
# Legacy prebuilt resolution also serves test-mode source-build fallback.
wheel_server_urls = wheels.get_wheel_server_urls(
self.ctx, req, cache_wheel_server_url=resolver.PYPI_SERVER_URL
)
Expand All @@ -188,30 +194,39 @@ def _resolve_and_extend(
req_type=req_type,
)
else:
# Resolve source (sdist)
provider = sources.get_source_provider(
ctx=self.ctx,
req=req,
sdist_server_url=resolver.PYPI_SERVER_URL,
req_type=req_type,
)
max_age_cutoff = resolver._compute_max_age_cutoff(self.ctx)
age_fallback = (
resolver.AgeFallback.NEWEST
if self.multiple_versions
else resolver.AgeFallback.ALL
)
results = resolver.find_all_matching_from_provider(
provider,
req,
max_age_cutoff=max_age_cutoff,
age_fallback=age_fallback,
)
results = []
if configured_prebuilt and self.ctx.wheel_server_url:
try:
results = wheels.resolve_all_cached_wheels(
ctx=self.ctx,
req=req,
cache_server_url=self.ctx.wheel_server_url,
)
except ResolverException:
logger.debug("no matching wheel in local cache")
except requests.exceptions.RequestException as err:
logger.warning("could not check local wheel cache: %s", err)

if not results:
logger.warning(
"resolver returned no results (%s version mode)",
"multiple" if self.multiple_versions else "single",
# The configured source provider resolves either source archives
# or prebuilt wheels, according to its selected capability.
provider = sources.get_source_provider(
ctx=self.ctx,
req=req,
sdist_server_url=resolver.PYPI_SERVER_URL,
req_type=req_type,
)
max_age_cutoff = resolver._compute_max_age_cutoff(self.ctx)
age_fallback = (
resolver.AgeFallback.NEWEST
if self.multiple_versions
else resolver.AgeFallback.ALL
)
results = resolver.find_all_matching_from_provider(
provider,
req,
max_age_cutoff=max_age_cutoff,
age_fallback=age_fallback,
)

if results:
Expand Down
8 changes: 6 additions & 2 deletions src/fromager/bootstrapper/_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,8 +470,12 @@ def _download_prebuilt(
resolved_version: Version,
wheel_url: str,
) -> tuple[pathlib.Path, pathlib.Path]:
result = _cache.bg_prepare_prebuilt(
self.ctx, req, req_type, resolved_version, wheel_url
result = _cache.bg_prepare_test_mode_prebuilt(
self.ctx,
req,
req_type,
resolved_version,
wheel_url,
)
assert result.wheel_filename is not None
assert result.unpack_dir is not None
Expand Down
26 changes: 25 additions & 1 deletion src/fromager/bootstrapper/_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,34 @@ def bg_prepare_prebuilt(
resolved_version: Version,
wheel_url: str,
) -> PreparedSourceData:
"""Background-safe prebuilt download: no Bootstrapper state accessed."""
"""Download a prebuilt wheel and make it available to the local mirror."""
# Thread-safe: paths include {req.name}-{resolved_version} (unique per package),
Comment thread
rd4398 marked this conversation as resolved.
# mkdir uses exist_ok=True (atomic), and update_wheel_mirror() is already locked.
logger.info(f"using pre-built wheel for {req_type} requirement")
wheel_filename = wheels.download_prebuilt_wheel(
ctx=ctx,
req=req,
version=resolved_version,
download_url=wheel_url,
direct_download_directory=ctx.wheels_prebuilt,
)
unpack_dir = _create_unpack_dir(ctx.work_dir, req, resolved_version)
if ctx.package_build_info(req).source_resolver is not None:
server.update_wheel_mirror(ctx, preferred_prebuilt_wheel=wheel_filename)
Comment thread
LalatenduMohanty marked this conversation as resolved.
else:
server.update_wheel_mirror(ctx)
return PreparedSourceData(wheel_filename=wheel_filename, unpack_dir=unpack_dir)


def bg_prepare_test_mode_prebuilt(
ctx: context.WorkContext,
req: Requirement,
req_type: RequirementType,
resolved_version: Version,
wheel_url: str,
) -> PreparedSourceData:
"""Download a test-mode fallback wheel directly and update the mirror."""
logger.info(f"using pre-built wheel for {req_type} requirement")
wheel_filename = wheels.download_wheel(req, wheel_url, ctx.wheels_prebuilt)
unpack_dir = _create_unpack_dir(ctx.work_dir, req, resolved_version)
server.update_wheel_mirror(ctx)
Expand Down
47 changes: 35 additions & 12 deletions src/fromager/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,17 +362,18 @@ def _build(
if prebuilt:
if not wheel_filename:
logger.info("downloading prebuilt wheel")
wheel_filename = wheels.download_wheel(
wheel_filename = wheels.download_prebuilt_wheel(
ctx=wkctx,
req=req,
wheel_url=source_download_url,
output_directory=wkctx.wheels_build,
version=resolved_version,
download_url=source_download_url,
direct_download_directory=wkctx.wheels_build,
)
else:
# already downloaded prebuilt wheel
use_exiting_wheel = True

# Run hooks for prebuilt wheels. At this point wheel_filename should
# be set either from _is_wheel_built() or download_wheel().
# Run hooks for both cached and freshly downloaded prebuilt wheels.
hooks.run_prebuilt_wheel_hooks(
ctx=wkctx,
req=req,
Expand Down Expand Up @@ -446,11 +447,19 @@ def _build(
root_logger.removeHandler(file_handler)
file_handler.close()

server.update_wheel_mirror(wkctx)
if (
prebuilt
and pbi.source_resolver is not None
and wheel_filename.parent == wkctx.wheels_prebuilt
):
server.update_wheel_mirror(wkctx, preferred_prebuilt_wheel=wheel_filename)
else:
server.update_wheel_mirror(wkctx)

# After we update the wheel mirror, the built file has
# moved to a new directory.
wheel_filename = wkctx.wheels_downloads / wheel_filename.name
# The mirror moves wheels from the build directory to downloads;
# configured prebuilt wheels stay in the prebuilt directory.
Comment thread
rd4398 marked this conversation as resolved.
if wheel_filename.parent != wkctx.wheels_prebuilt:
wheel_filename = wkctx.wheels_downloads / wheel_filename.name

return BuildSequenceEntry(
name=canonicalize_name(req.name),
Expand Down Expand Up @@ -529,8 +538,8 @@ def _is_wheel_built(
logger.info("found candidate wheel %s", url)
try:
wheel_basename = downloads.extract_filename_from_url(url)
_, _, build_tag_from_name, wheel_tags = parse_wheel_filename(
wheel_basename
normalized_name, _, build_tag_from_name, wheel_tags = (
parse_wheel_filename(wheel_basename)
)
except Exception:
logger.debug("could not parse candidate wheel %s", url, exc_info=True)
Expand Down Expand Up @@ -567,9 +576,23 @@ def _is_wheel_built(
continue

wheel_filename: pathlib.Path | None = None
if wkctx.wheel_server_url and url.startswith(wkctx.wheel_server_url):
if wheels.is_local_wheel_url(url, wkctx.wheel_server_url):
logging.debug("found wheel on local server")
wheel_filename = wkctx.wheels_downloads / wheel_basename
mirror_wheel = wkctx.wheel_server_dir / normalized_name / wheel_basename
if mirror_wheel.is_symlink():
mirror_target = mirror_wheel.resolve()
if (
mirror_target.parent
in (
wkctx.wheels_downloads,
wkctx.wheels_prebuilt,
)
and mirror_target.is_file()
):
# The mirror may point to a fresh prebuilt wheel while
# an older copy remains in downloads.
wheel_filename = mirror_target
if not wheel_filename.exists():
logger.info("wheel not found in local cache, preparing to download")
wheel_filename = None
Expand Down
Loading
Loading