Skip to content

Commit d81c39a

Browse files
authored
feat: allow for multi-base builds (#598)
Signed-off-by: Callahan Kovacs <callahan.kovacs@canonical.com>
1 parent 8dae7e2 commit d81c39a

File tree

4 files changed

+46
-28
lines changed

4 files changed

+46
-28
lines changed

craft_application/models/project.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
UniqueStrList,
4444
VersionStr,
4545
)
46-
from craft_application.util import is_valid_architecture
4746

4847

4948
@dataclasses.dataclass
@@ -131,13 +130,14 @@ def _vectorise_architectures(cls, values: str | list[str]) -> list[str]:
131130
@pydantic.field_validator("build_on", "build_for", mode="after")
132131
@classmethod
133132
def _validate_architectures(cls, values: list[str]) -> list[str]:
134-
"""Validate the architecture entries."""
135-
for architecture in values:
136-
if architecture != "all" and not is_valid_architecture(architecture):
137-
raise errors.CraftValidationError(
138-
f"Invalid architecture: {architecture!r} "
139-
"must be a valid debian architecture."
140-
)
133+
"""Validate the architecture entries.
134+
135+
Entries must be a valid debian architecture or 'all'. Architectures may
136+
be preceded by an optional base prefix formatted as '[<base>:]<arch>'.
137+
138+
:raises ValueError: If any of the bases or architectures are not valid.
139+
"""
140+
[craft_platforms.parse_base_and_architecture(arch) for arch in values]
141141

142142
return values
143143

@@ -167,8 +167,10 @@ def from_platforms(cls, platforms: craft_platforms.Platforms) -> dict[str, Self]
167167
return result
168168

169169

170-
def _populate_platforms(platforms: dict[str, Any]) -> dict[str, Any]:
171-
"""Populate empty platform entries.
170+
def _expand_shorthand_platforms(platforms: dict[str, Any]) -> dict[str, Any]:
171+
"""Expand shorthand platform entries into standard form.
172+
173+
Assumes the platform label is a valid as a build-on and build-for entry.
172174
173175
:param platforms: The platform data.
174176
@@ -211,8 +213,8 @@ def _warn_deprecation(self) -> Self:
211213
@pydantic.field_validator("platforms", mode="before")
212214
@classmethod
213215
def _populate_platforms(cls, platforms: dict[str, Any]) -> dict[str, Any]:
214-
"""Populate empty platform entries."""
215-
return _populate_platforms(platforms)
216+
"""Expand shorthand platform entries into standard form."""
217+
return _expand_shorthand_platforms(platforms)
216218

217219
@pydantic.field_validator("platforms", mode="after")
218220
@classmethod
@@ -233,9 +235,9 @@ def _validate_platforms_all_keyword(
233235

234236
# validate `all` inside each platform:
235237
for platform in platforms.values():
236-
if platform.build_on and "all" in platform.build_on:
238+
if platform and platform.build_on and "all" in platform.build_on:
237239
raise ValueError("'all' cannot be used for 'build-on'")
238-
if platform.build_for and "all" in platform.build_for:
240+
if platform and platform.build_for and "all" in platform.build_for:
239241
is_all_used = True
240242

241243
# validate `all` across all platforms:
@@ -337,8 +339,8 @@ class Project(base.CraftBaseModel):
337339
@pydantic.field_validator("platforms", mode="before")
338340
@classmethod
339341
def _populate_platforms(cls, platforms: dict[str, Platform]) -> dict[str, Platform]:
340-
"""Populate empty platform entries."""
341-
return _populate_platforms(platforms)
342+
"""Expand shorthand platform entries into standard form."""
343+
return _expand_shorthand_platforms(platforms)
342344

343345
@property
344346
def effective_base(self) -> Any: # noqa: ANN401 app specific classes can improve

docs/reference/changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
Changelog
55
*********
66

7+
4.7.0 (2024-Dec-19)
8+
-------------------
9+
10+
Application
11+
===========
12+
13+
- Allow applications to implement multi-base build plans.
14+
715
4.6.0 (2024-Dec-13)
816
-------------------
917

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ dependencies = [
88
"craft-cli>=2.12.0",
99
"craft-grammar>=2.0.0",
1010
"craft-parts>=2.1.1",
11-
"craft-platforms>=0.3.1",
11+
"craft-platforms>=0.5.0",
1212
"craft-providers>=2.0.4",
1313
"Jinja2~=3.1",
1414
"snap-helpers>=0.4.2",

tests/unit/models/test_project.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
Project,
3636
constraints,
3737
)
38-
from craft_application.util import platforms
3938

4039
PROJECTS_DIR = pathlib.Path(__file__).parent / "project_models"
4140
PARTS_DICT = {"my-part": {"plugin": "nil"}}
@@ -170,15 +169,15 @@ def test_build_info_from_platforms(incoming, expected):
170169
Platform(build_on=[arch], build_for=[arch]),
171170
id=arch,
172171
)
173-
for arch in platforms._ARCH_TRANSLATIONS_DEB_TO_PLATFORM
172+
for arch in craft_platforms.DebianArchitecture
174173
),
175174
*(
176175
pytest.param(
177176
{"build-on": arch},
178177
Platform(build_on=[arch]),
179178
id=f"build-on-only-{arch}",
180179
)
181-
for arch in platforms._ARCH_TRANSLATIONS_DEB_TO_PLATFORM
180+
for arch in craft_platforms.DebianArchitecture
182181
),
183182
pytest.param(
184183
{"build-on": "amd64", "build-for": "riscv64"},
@@ -562,30 +561,39 @@ def test_unmarshal_invalid_repositories(
562561

563562

564563
@pytest.mark.parametrize("model", [Project, BuildPlanner])
565-
def test_platform_invalid_arch(model, basic_project_dict):
566-
basic_project_dict["platforms"] = {"unknown": None}
564+
@pytest.mark.parametrize("platform_label", ["unknown", "ubuntu@24.04:unknown"])
565+
def test_platform_invalid_arch(model, platform_label, basic_project_dict):
566+
basic_project_dict["platforms"] = {platform_label: None}
567567
project_path = pathlib.Path("myproject.yaml")
568568

569569
with pytest.raises(CraftValidationError) as error:
570570
model.from_yaml_data(basic_project_dict, project_path)
571571

572572
assert error.value.args[0] == (
573-
"Invalid architecture: 'unknown' must be a valid debian architecture."
573+
"Bad myproject.yaml content:\n"
574+
f"- 'unknown' is not a valid DebianArchitecture (in field 'platforms.{platform_label}.build-on')\n"
575+
f"- 'unknown' is not a valid DebianArchitecture (in field 'platforms.{platform_label}.build-for')"
574576
)
575577

576578

577579
@pytest.mark.parametrize("model", [Project, BuildPlanner])
580+
@pytest.mark.parametrize("arch", ["unknown", "ubuntu@24.04:unknown"])
578581
@pytest.mark.parametrize("field_name", ["build-on", "build-for"])
579-
def test_platform_invalid_build_arch(model, field_name, basic_project_dict):
580-
basic_project_dict["platforms"] = {"amd64": {field_name: ["unknown"]}}
582+
def test_platform_invalid_build_arch(model, arch, field_name, basic_project_dict):
583+
basic_project_dict["platforms"] = {"amd64": {field_name: [arch]}}
581584
project_path = pathlib.Path("myproject.yaml")
582585

583586
with pytest.raises(CraftValidationError) as error:
584587
model.from_yaml_data(basic_project_dict, project_path)
585588

586-
assert error.value.args[0] == (
587-
"Invalid architecture: 'unknown' must be a valid debian architecture."
588-
)
589+
error_lines = [
590+
"Bad myproject.yaml content:",
591+
"- field 'build-on' required in 'platforms.amd64' configuration",
592+
f"- 'unknown' is not a valid DebianArchitecture (in field 'platforms.amd64.{field_name}')",
593+
]
594+
if field_name == "build-on":
595+
error_lines.pop(1)
596+
assert error.value.args[0] == "\n".join(error_lines)
589597

590598

591599
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)