Skip to content

Commit 49e7dbf

Browse files
Refactor bootstrap() Method
Refactoring bootstrap() Method in src/fromager/bootstrapper.py. Currently it has 10+ Responsibilities (230 lines). Signed-off-by: Lalatendu Mohanty <lmohanty@redhat.com>
1 parent 81d7024 commit 49e7dbf

2 files changed

Lines changed: 320 additions & 123 deletions

File tree

src/fromager/bootstrapper.py

Lines changed: 235 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import dataclasses
34
import json
45
import logging
56
import operator
@@ -40,6 +41,21 @@
4041
SeenKey = tuple[NormalizedName, tuple[str, ...], str, typing.Literal["sdist", "wheel"]]
4142

4243

44+
@dataclasses.dataclass
45+
class SourceBuildResult:
46+
"""Result of building a package from source.
47+
48+
Used to return multiple values from _build_from_source().
49+
"""
50+
51+
wheel_filename: pathlib.Path | None
52+
sdist_filename: pathlib.Path | None
53+
unpack_dir: pathlib.Path
54+
sdist_root_dir: pathlib.Path
55+
build_env: build_environment.BuildEnvironment
56+
source_url_type: str
57+
58+
4359
class Bootstrapper:
4460
def __init__(
4561
self,
@@ -194,105 +210,33 @@ def bootstrap(self, req: Requirement, req_type: RequirementType) -> Version:
194210
)
195211
# Remember that this is a prebuilt wheel, and where we got it.
196212
source_url_type = str(SourceType.PREBUILT)
213+
sdist_filename = None
214+
sdist_root_dir = None
215+
build_env = None
197216
else:
198-
# Look a few places for an existing wheel that matches what we need,
199-
# using caches for locations where we might have built the wheel
200-
# before.
201-
202-
# Check if we have previously built a wheel and still have it on the
203-
# local filesystem.
204-
if not wheel_filename and not cached_wheel_filename:
205-
cached_wheel_filename, unpacked_cached_wheel = (
206-
self._look_for_existing_wheel(
207-
req,
208-
resolved_version,
209-
self.ctx.wheels_build,
210-
)
211-
)
212-
213-
# Check if we have previously downloaded a wheel and still have it
214-
# on the local filesystem.
215-
if not wheel_filename and not cached_wheel_filename:
216-
cached_wheel_filename, unpacked_cached_wheel = (
217-
self._look_for_existing_wheel(
218-
req,
219-
resolved_version,
220-
self.ctx.wheels_downloads,
221-
)
222-
)
223-
224-
# Look for a wheel on the cache server and download it if there is
225-
# one.
226-
if not wheel_filename and not cached_wheel_filename:
227-
cached_wheel_filename, unpacked_cached_wheel = (
228-
self._download_wheel_from_cache(req, resolved_version)
229-
)
230-
231-
if not unpacked_cached_wheel:
232-
# We didn't find anything so we are going to have to build the
233-
# wheel in order to process its installation dependencies.
234-
logger.debug("no cached wheel, downloading sources")
235-
source_filename = sources.download_source(
236-
ctx=self.ctx,
237-
req=req,
238-
version=resolved_version,
239-
download_url=source_url,
240-
)
241-
sdist_root_dir = sources.prepare_source(
242-
ctx=self.ctx,
243-
req=req,
244-
source_filename=source_filename,
245-
version=resolved_version,
246-
)
247-
else:
248-
logger.debug(f"have cached wheel in {unpacked_cached_wheel}")
249-
sdist_root_dir = unpacked_cached_wheel / unpacked_cached_wheel.stem
250-
251-
assert sdist_root_dir is not None
252-
253-
if sdist_root_dir.parent.parent != self.ctx.work_dir:
254-
raise ValueError(
255-
f"'{sdist_root_dir}/../..' should be {self.ctx.work_dir}"
256-
)
257-
unpack_dir = sdist_root_dir.parent
258-
259-
build_env = build_environment.BuildEnvironment(
260-
ctx=self.ctx,
261-
parent_dir=sdist_root_dir.parent,
217+
# Look for an existing wheel in caches (3 levels: build, downloads,
218+
# cache server) before building from source.
219+
cached_wheel_filename, unpacked_cached_wheel = self._find_cached_wheel(
220+
req, resolved_version
262221
)
263222

264-
# need to call this function irrespective of whether we had the wheel cached
265-
# so that the build dependencies can be bootstrapped
266-
self._prepare_build_dependencies(req, sdist_root_dir, build_env)
223+
# Build from source (download, prepare, build wheel/sdist)
224+
build_result = self._build_from_source(
225+
req=req,
226+
resolved_version=resolved_version,
227+
source_url=source_url,
228+
build_sdist_only=build_sdist_only,
229+
cached_wheel_filename=cached_wheel_filename,
230+
unpacked_cached_wheel=unpacked_cached_wheel,
231+
)
267232

268-
if cached_wheel_filename:
269-
logger.debug(
270-
f"getting install requirements from cached "
271-
f"wheel {cached_wheel_filename.name}"
272-
)
273-
# prefer existing wheel even in sdist_only mode
274-
# skip building even if it is a non-fromager built wheel
275-
wheel_filename = cached_wheel_filename
276-
build_sdist_only = False
277-
elif build_sdist_only:
278-
# get install dependencies from sdist and pyproject_hooks (only top-level and install)
279-
logger.debug(
280-
f"getting install requirements from sdist "
281-
f"{req.name}=={resolved_version} ({req_type})"
282-
)
283-
wheel_filename = None
284-
sdist_filename = self._build_sdist(
285-
req, resolved_version, sdist_root_dir, build_env
286-
)
287-
else:
288-
# build wheel (build requirements, full build mode)
289-
logger.debug(
290-
f"building wheel {req.name}=={resolved_version} "
291-
f"to get install requirements ({req_type})"
292-
)
293-
wheel_filename, sdist_filename = self._build_wheel(
294-
req, resolved_version, sdist_root_dir, build_env
295-
)
233+
# Unpack result
234+
wheel_filename = build_result.wheel_filename
235+
sdist_filename = build_result.sdist_filename
236+
unpack_dir = build_result.unpack_dir
237+
sdist_root_dir = build_result.sdist_root_dir
238+
build_env = build_result.build_env
239+
source_url_type = build_result.source_url_type
296240

297241
hooks.run_post_bootstrap_hooks(
298242
ctx=self.ctx,
@@ -303,34 +247,15 @@ def bootstrap(self, req: Requirement, req_type: RequirementType) -> Version:
303247
wheel_filename=wheel_filename,
304248
)
305249

306-
if wheel_filename is not None:
307-
assert unpack_dir is not None
308-
logger.debug(
309-
"get install dependencies of wheel %s",
310-
wheel_filename.name,
311-
)
312-
install_dependencies = dependencies.get_install_dependencies_of_wheel(
313-
req=req,
314-
wheel_filename=wheel_filename,
315-
requirements_file_dir=unpack_dir,
316-
)
317-
elif sdist_filename is not None:
318-
assert sdist_root_dir is not None
319-
assert build_env is not None
320-
logger.debug(
321-
"get install dependencies of sdist from directory %s",
322-
sdist_root_dir,
323-
)
324-
install_dependencies = dependencies.get_install_dependencies_of_sdist(
325-
ctx=self.ctx,
326-
req=req,
327-
version=resolved_version,
328-
sdist_root_dir=sdist_root_dir,
329-
build_env=build_env,
330-
)
331-
else:
332-
# unreachable
333-
raise RuntimeError("wheel_filename and sdist_filename are None")
250+
install_dependencies = self._get_install_dependencies(
251+
req=req,
252+
resolved_version=resolved_version,
253+
wheel_filename=wheel_filename,
254+
sdist_filename=sdist_filename,
255+
sdist_root_dir=sdist_root_dir,
256+
build_env=build_env,
257+
unpack_dir=unpack_dir,
258+
)
334259

335260
logger.debug(
336261
"install dependencies: %s",
@@ -505,6 +430,193 @@ def _download_prebuilt(
505430
server.update_wheel_mirror(self.ctx)
506431
return (wheel_filename, unpack_dir)
507432

433+
def _find_cached_wheel(
434+
self,
435+
req: Requirement,
436+
resolved_version: Version,
437+
) -> tuple[pathlib.Path | None, pathlib.Path | None]:
438+
"""Look for cached wheel in 3 locations.
439+
440+
Checks for cached wheels in order:
441+
1. wheels_build directory (previously built)
442+
2. wheels_downloads directory (previously downloaded)
443+
3. Cache server (remote cache)
444+
445+
Returns:
446+
Tuple of (cached_wheel_filename, unpacked_cached_wheel).
447+
Both None if no cache hit.
448+
"""
449+
# Check if we have previously built a wheel and still have it on the
450+
# local filesystem.
451+
cached_wheel, unpacked = self._look_for_existing_wheel(
452+
req, resolved_version, self.ctx.wheels_build
453+
)
454+
if cached_wheel:
455+
return cached_wheel, unpacked
456+
457+
# Check if we have previously downloaded a wheel and still have it
458+
# on the local filesystem.
459+
cached_wheel, unpacked = self._look_for_existing_wheel(
460+
req, resolved_version, self.ctx.wheels_downloads
461+
)
462+
if cached_wheel:
463+
return cached_wheel, unpacked
464+
465+
# Look for a wheel on the cache server and download it if there is one.
466+
cached_wheel, unpacked = self._download_wheel_from_cache(req, resolved_version)
467+
if cached_wheel:
468+
return cached_wheel, unpacked
469+
470+
return None, None
471+
472+
def _get_install_dependencies(
473+
self,
474+
req: Requirement,
475+
resolved_version: Version,
476+
wheel_filename: pathlib.Path | None,
477+
sdist_filename: pathlib.Path | None,
478+
sdist_root_dir: pathlib.Path | None,
479+
build_env: build_environment.BuildEnvironment | None,
480+
unpack_dir: pathlib.Path | None,
481+
) -> list[Requirement]:
482+
"""Extract install dependencies from wheel or sdist.
483+
484+
Returns:
485+
List of install requirements.
486+
487+
Raises:
488+
RuntimeError: If both wheel_filename and sdist_filename are None.
489+
"""
490+
if wheel_filename is not None:
491+
assert unpack_dir is not None
492+
logger.debug(
493+
"get install dependencies of wheel %s",
494+
wheel_filename.name,
495+
)
496+
return list(
497+
dependencies.get_install_dependencies_of_wheel(
498+
req=req,
499+
wheel_filename=wheel_filename,
500+
requirements_file_dir=unpack_dir,
501+
)
502+
)
503+
elif sdist_filename is not None:
504+
assert sdist_root_dir is not None
505+
assert build_env is not None
506+
logger.debug(
507+
"get install dependencies of sdist from directory %s",
508+
sdist_root_dir,
509+
)
510+
return list(
511+
dependencies.get_install_dependencies_of_sdist(
512+
ctx=self.ctx,
513+
req=req,
514+
version=resolved_version,
515+
sdist_root_dir=sdist_root_dir,
516+
build_env=build_env,
517+
)
518+
)
519+
else:
520+
raise RuntimeError("wheel_filename and sdist_filename are None")
521+
522+
def _build_from_source(
523+
self,
524+
req: Requirement,
525+
resolved_version: Version,
526+
source_url: str,
527+
build_sdist_only: bool,
528+
cached_wheel_filename: pathlib.Path | None,
529+
unpacked_cached_wheel: pathlib.Path | None,
530+
) -> SourceBuildResult:
531+
"""Build package from source.
532+
533+
Handles:
534+
1. Download and prepare source (if not cached)
535+
2. Create build environment
536+
3. Prepare build dependencies
537+
4. Build wheel or sdist (based on flags and cache state)
538+
539+
Returns:
540+
SourceBuildResult with all build artifacts.
541+
542+
Raises:
543+
Various exceptions from download, prepare, or build steps.
544+
This is where test-mode will catch exceptions.
545+
"""
546+
# Download and prepare source (if no cached wheel)
547+
if not unpacked_cached_wheel:
548+
logger.debug("no cached wheel, downloading sources")
549+
source_filename = sources.download_source(
550+
ctx=self.ctx,
551+
req=req,
552+
version=resolved_version,
553+
download_url=source_url,
554+
)
555+
sdist_root_dir = sources.prepare_source(
556+
ctx=self.ctx,
557+
req=req,
558+
source_filename=source_filename,
559+
version=resolved_version,
560+
)
561+
else:
562+
logger.debug(f"have cached wheel in {unpacked_cached_wheel}")
563+
sdist_root_dir = unpacked_cached_wheel / unpacked_cached_wheel.stem
564+
565+
assert sdist_root_dir is not None
566+
567+
if sdist_root_dir.parent.parent != self.ctx.work_dir:
568+
raise ValueError(f"'{sdist_root_dir}/../..' should be {self.ctx.work_dir}")
569+
unpack_dir = sdist_root_dir.parent
570+
571+
build_env = build_environment.BuildEnvironment(
572+
ctx=self.ctx,
573+
parent_dir=sdist_root_dir.parent,
574+
)
575+
576+
# Prepare build dependencies (always needed)
577+
self._prepare_build_dependencies(req, sdist_root_dir, build_env)
578+
579+
# Decide what to build based on cache state and build mode
580+
wheel_filename: pathlib.Path | None
581+
sdist_filename: pathlib.Path | None
582+
583+
if cached_wheel_filename:
584+
logger.debug(
585+
f"getting install requirements from cached "
586+
f"wheel {cached_wheel_filename.name}"
587+
)
588+
# prefer existing wheel even in sdist_only mode
589+
wheel_filename = cached_wheel_filename
590+
sdist_filename = None
591+
elif build_sdist_only:
592+
logger.debug(
593+
f"getting install requirements from sdist "
594+
f"{req.name}=={resolved_version}"
595+
)
596+
wheel_filename = None
597+
sdist_filename = self._build_sdist(
598+
req, resolved_version, sdist_root_dir, build_env
599+
)
600+
else:
601+
logger.debug(
602+
f"building wheel {req.name}=={resolved_version} "
603+
f"to get install requirements"
604+
)
605+
wheel_filename, sdist_filename = self._build_wheel(
606+
req, resolved_version, sdist_root_dir, build_env
607+
)
608+
609+
source_url_type = sources.get_source_type(self.ctx, req)
610+
611+
return SourceBuildResult(
612+
wheel_filename=wheel_filename,
613+
sdist_filename=sdist_filename,
614+
unpack_dir=unpack_dir,
615+
sdist_root_dir=sdist_root_dir,
616+
build_env=build_env,
617+
source_url_type=source_url_type,
618+
)
619+
508620
def _look_for_existing_wheel(
509621
self,
510622
req: Requirement,

0 commit comments

Comments
 (0)