Skip to content

Commit 2921470

Browse files
committed
build-parallel: Add exclusive_build option for per-package exclusive builds
- Added exclusive_build (bool) to BuildOptions; defaults to False. - When set to True, the package is built alone, not in parallel with others. - Updated build_parallel logic and all relevant docstrings and config examples. - Replaced the previous parallel_builds enum with exclusive_build. - Updated the end-to-end test to cover exclusive build behavior.
1 parent 25acb2b commit 2921470

7 files changed

Lines changed: 88 additions & 6 deletions

File tree

e2e/build-parallel/cython.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build_options:
2+
exclusive_build: True

e2e/test_build_parallel.sh

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,42 @@ VERSION="1.14.0"
1515
# --sdists-repo="$OUTDIR/sdists-repo" \
1616
# --wheels-repo="$OUTDIR/wheels-repo" \
1717
# --work-dir="$OUTDIR/work-dir" \
18-
# --settings-dir="$SCRIPTDIR/changelog_settings" \
18+
# --settings-dir="$SCRIPTDIR/build-parallel" \
1919
# bootstrap "${DIST}==${VERSION}"
2020

2121
# Save the build order file but remove everything else.
2222
# cp "$OUTDIR/work-dir/graph.json" "$OUTDIR/"
2323

2424
# Copy the cached graph file to the working directory
25-
cp "$SCRIPTDIR/build-parallel-graph.json" "$OUTDIR/graph.json"
25+
cp "$SCRIPTDIR/build-parallel/graph.json" "$OUTDIR/graph.json"
2626

2727
# Build everything a first time
2828
log="$OUTDIR/build-logs/${DIST}-build.log"
2929
fromager \
30-
--debug \
3130
--log-file "$log" \
3231
--work-dir "$OUTDIR/work-dir" \
3332
--sdists-repo "$OUTDIR/sdists-repo" \
3433
--wheels-repo "$OUTDIR/wheels-repo" \
35-
--settings-dir="$SCRIPTDIR/changelog_settings" \
34+
--settings-dir="$SCRIPTDIR/build-parallel" \
3635
build-parallel "$OUTDIR/graph.json"
3736

37+
if ! grep -q "ready to build cython" "$log"; then
38+
echo "Did not find message indicating build of cython would start" 1>&2
39+
pass=false
40+
fi
41+
if ! grep -q "cython: requires exclusive build" "$log"; then
42+
echo "Did not find message indicating build of cython would run on its own" 1>&2
43+
pass=false
44+
fi
45+
3846
# Rebuild everything even if it already exists
3947
log="$OUTDIR/build-logs/${DIST}-build.log"
4048
fromager \
4149
--log-file "$log" \
4250
--work-dir "$OUTDIR/work-dir" \
4351
--sdists-repo "$OUTDIR/sdists-repo" \
4452
--wheels-repo "$OUTDIR/wheels-repo" \
45-
--settings-dir="$SCRIPTDIR/changelog_settings" \
53+
--settings-dir="$SCRIPTDIR/build-parallel" \
4654
build-parallel --force "$OUTDIR/graph.json"
4755

4856
find "$OUTDIR/wheels-repo/"
@@ -84,7 +92,7 @@ fromager \
8492
--work-dir "$OUTDIR/work-dir" \
8593
--sdists-repo "$OUTDIR/sdists-repo" \
8694
--wheels-repo "$OUTDIR/wheels-repo" \
87-
--settings-dir="$SCRIPTDIR/changelog_settings" \
95+
--settings-dir="$SCRIPTDIR/build-parallel" \
8896
build-parallel "$OUTDIR/graph.json"
8997

9098
find "$OUTDIR/wheels-repo/"

src/fromager/bootstrapper.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ def _build_sdist(
323323
sdist_root_dir: pathlib.Path,
324324
build_env: build_environment.BuildEnvironment,
325325
) -> pathlib.Path:
326+
sdist_filename: pathlib.Path | None = None
326327
try:
327328
find_sdist_result = finders.find_sdist(
328329
self.ctx, self.ctx.sdists_builds, req, str(resolved_version)
@@ -342,6 +343,12 @@ def _build_sdist(
342343
)
343344
except Exception as err:
344345
logger.warning(f"failed to build source distribution: {err}")
346+
# Re-raise the exception since we cannot continue without a sdist
347+
raise
348+
349+
if sdist_filename is None:
350+
raise RuntimeError(f"Failed to build or find sdist for {req}")
351+
345352
return sdist_filename
346353

347354
def _build_wheel(

src/fromager/commands/build.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,21 @@ def build_parallel(
594594
", ".join(n.canonicalized_name for n in buildable_nodes),
595595
)
596596

597+
# Check if any buildable node requires exclusive build (exclusive_build == True)
598+
exclusive_nodes = [
599+
node
600+
for node in buildable_nodes
601+
if wkctx.settings.package_build_info(
602+
node.canonicalized_name
603+
).exclusive_build
604+
]
605+
if exclusive_nodes:
606+
# Only build the first exclusive node this round
607+
buildable_nodes = [exclusive_nodes[0]]
608+
logger.info(
609+
f"{exclusive_nodes[0].canonicalized_name}: requires exclusive build, running it alone this round."
610+
)
611+
597612
# Build up to max_workers nodes concurrently (or all if max_workers is None)
598613
with concurrent.futures.ThreadPoolExecutor(
599614
max_workers=max_workers

src/fromager/packagesettings.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ class BuildOptions(pydantic.BaseModel):
197197
0.5: assume each parallel job requires 512 MB virtual memory
198198
"""
199199

200+
exclusive_build: bool = False
201+
"""If true, this package must be built on its own (not in parallel with other packages). Default: False."""
202+
200203

201204
class ProjectOverride(pydantic.BaseModel):
202205
"""Override pyproject.toml settings
@@ -317,6 +320,11 @@ class PackageSettings(pydantic.BaseModel):
317320
sdist_server_url: https://sdist.test/egg
318321
include_sdists: true
319322
include_wheels: false
323+
build_options:
324+
build_ext_parallel: False
325+
cpu_cores_per_job: 1
326+
memory_per_job_gb: 1.0
327+
exclusive_build: False
320328
variants:
321329
cpu:
322330
env:
@@ -809,6 +817,10 @@ def git_options(self) -> GitOptions:
809817
def project_override(self) -> ProjectOverride:
810818
return self._ps.project_override
811819

820+
@property
821+
def exclusive_build(self) -> bool:
822+
return self._ps.build_options.exclusive_build
823+
812824
def serialize(self, **kwargs) -> dict[str, typing.Any]:
813825
return self._ps.serialize(**kwargs)
814826

@@ -847,6 +859,9 @@ def from_string(
847859
# ignore legacy settings
848860
parsed.pop("pre_built", None)
849861
parsed.pop("packages", None)
862+
# Ensure changelog is correct type
863+
if "changelog" in parsed and not isinstance(parsed["changelog"], dict):
864+
parsed["changelog"] = {}
850865
try:
851866
return cls(**parsed)
852867
except Exception as err:

tests/test_packagesettings.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"build_ext_parallel": True,
3232
"cpu_cores_per_job": 4,
3333
"memory_per_job_gb": 4.0,
34+
"exclusive_build": False,
3435
},
3536
"changelog": {
3637
Version("1.0.1"): ["fixed bug"],
@@ -95,6 +96,7 @@
9596
"build_ext_parallel": False,
9697
"cpu_cores_per_job": 1,
9798
"memory_per_job_gb": 1.0,
99+
"exclusive_build": False,
98100
},
99101
"changelog": {},
100102
"config_settings": {},
@@ -595,3 +597,36 @@ def test_package_build_info_git_options(testdata_context: context.WorkContext):
595597
custom_settings = PackageSettings.from_string("custom-pkg", settings_yaml)
596598
assert custom_settings.git_options.submodules is True
597599
assert custom_settings.git_options.submodule_paths == ["vendor/lib"]
600+
601+
602+
def test_package_build_info_exclusive_build(testdata_context: context.WorkContext):
603+
"""Test that PackageBuildInfo correctly exposes exclusive_build from build_options."""
604+
# Test default package (should have exclusive_build=False by default)
605+
req = Requirement("test-empty-pkg==1.0.0")
606+
pbi = testdata_context.package_build_info(req)
607+
assert pbi.exclusive_build is False
608+
609+
# Test creating a package settings with exclusive_build=True
610+
settings_yaml = """
611+
build_options:
612+
exclusive_build: true
613+
"""
614+
custom_settings = PackageSettings.from_string("exclusive-pkg", settings_yaml)
615+
assert custom_settings.build_options.exclusive_build is True
616+
617+
# Test PackageBuildInfo properly accesses it through build_options
618+
import pathlib
619+
620+
from fromager.packagesettings import Settings, SettingsFile
621+
622+
# Create a temporary Settings object to test with
623+
settings = Settings(
624+
settings=SettingsFile(),
625+
package_settings=[custom_settings],
626+
variant="cpu",
627+
patches_dir=pathlib.Path("/tmp"),
628+
max_jobs=1,
629+
)
630+
631+
custom_pbi = settings.package_build_info("exclusive-pkg")
632+
assert custom_pbi.exclusive_build is True

0 commit comments

Comments
 (0)