Skip to content

Commit

Permalink
feat: expose partitions to the lifecycle service (#292)
Browse files Browse the repository at this point in the history
Signed-off-by: Sergio Schvezov <[email protected]>
Co-authored-by: Alex Lowe <[email protected]>
  • Loading branch information
sergiusens and lengau authored Apr 1, 2024
1 parent 80a9ecb commit a348885
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 2 deletions.
21 changes: 20 additions & 1 deletion craft_application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def __init__(
# When build_secrets are enabled, this contains the secret info to pass to
# managed instances.
self._secrets: secrets.BuildSecrets | None = None
self._partitions: list[str] | None = None
# Cached project object, allows only the first time we load the project
# to specify things like the project directory.
# This is set as a private attribute in order to discourage real application
Expand Down Expand Up @@ -219,6 +220,7 @@ def _configure_services(self, provider_name: str | None) -> None:
cache_dir=self.cache_dir,
work_dir=self._work_dir,
build_plan=self._build_plan,
partitions=self._partitions,
)
self.services.set_kwargs(
"provider",
Expand Down Expand Up @@ -288,6 +290,9 @@ def get_project(
GrammarAwareProject.validate_grammar(yaml_data)

build_on = host_arch

# Setup partitions, some projects require the yaml data, most will not
self._partitions = self._setup_partitions(yaml_data)
yaml_data = self._transform_project_yaml(yaml_data, build_on, build_for)
self.__project = self.app.ProjectClass.from_yaml_data(yaml_data, project_path)

Expand Down Expand Up @@ -608,18 +613,32 @@ def _transform_project_yaml(
def _expand_environment(self, yaml_data: dict[str, Any]) -> None:
"""Perform expansion of project environment variables."""
environment_vars = self._get_project_vars(yaml_data)
project_dirs = craft_parts.ProjectDirs(
work_dir=self._work_dir, partitions=self._partitions
)

info = craft_parts.ProjectInfo(
application_name=self.app.name, # not used in environment expansion
cache_dir=pathlib.Path(), # not used in environment expansion
project_name=yaml_data.get("name", ""),
project_dirs=craft_parts.ProjectDirs(work_dir=self._work_dir),
project_dirs=project_dirs,
project_vars=environment_vars,
partitions=self._partitions,
)

self._set_global_environment(info)

craft_parts.expand_environment(yaml_data, info=info)

def _setup_partitions(self, yaml_data: dict[str, Any]) -> list[str] | None:
"""Return partitions to be used.
When returning you will also need to ensure that the feature is enabled
on Application instantiation craft_parts.Features(partitions_enabled=True)
"""
_ = yaml_data
return None

def _get_project_vars(self, yaml_data: dict[str, Any]) -> dict[str, str]:
"""Return a dict with project variables to be expanded."""
pvars: dict[str, str] = {}
Expand Down
3 changes: 3 additions & 0 deletions craft_application/services/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,14 @@ def __init__(
work_dir: Path | str,
cache_dir: Path | str,
build_plan: list[models.BuildInfo],
partitions: list[str] | None = None,
**lifecycle_kwargs: Any, # noqa: ANN401 - eventually used in an Any
) -> None:
super().__init__(app, services, project=project)
self._work_dir = work_dir
self._cache_dir = cache_dir
self._build_plan = build_plan
self._partitions = partitions
self._manager_kwargs = lifecycle_kwargs
self._lcm: LifecycleManager = None # type: ignore[assignment]

Expand Down Expand Up @@ -190,6 +192,7 @@ def _init_lifecycle_manager(self) -> LifecycleManager:
project_vars_part_name=self._project.adopt_info,
project_vars=self._project_vars,
track_stage_packages=True,
partitions=self._partitions,
**self._manager_kwargs,
)
except PartsError as err:
Expand Down
14 changes: 13 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,25 @@ def full_build_plan(mocker) -> list[models.BuildInfo]:
return build_plan


@pytest.fixture()
def enable_partitions() -> Iterator[craft_parts.Features]:
"""Enable the partitions feature in craft_parts for the relevant test."""
enable_overlay = craft_parts.Features().enable_overlay

craft_parts.Features.reset()
yield craft_parts.Features(enable_overlay=enable_overlay, enable_partitions=True)
craft_parts.Features.reset()


@pytest.fixture()
def enable_overlay() -> Iterator[craft_parts.Features]:
"""Enable the overlay feature in craft_parts for the relevant test."""
if not os.getenv("CI") and not shutil.which("fuse-overlayfs"):
pytest.skip("fuse-overlayfs not installed, skipping overlay tests.")

enable_partitions = craft_parts.Features().enable_partitions
craft_parts.Features.reset()
yield craft_parts.Features(enable_overlay=True)
yield craft_parts.Features(enable_overlay=True, enable_partitions=enable_partitions)
craft_parts.Features.reset()


Expand Down
20 changes: 20 additions & 0 deletions tests/unit/services/test_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,26 @@ def test_init_with_feature_package_repositories(
assert service._lcm._project_info.package_repositories == package_repositories


@pytest.mark.usefixtures("enable_partitions")
def test_init_with_partitions(
app_metadata, fake_project, fake_services, tmp_path, fake_build_plan
):
service = lifecycle.LifecycleService(
app_metadata,
fake_services,
project=fake_project,
work_dir=tmp_path,
cache_dir=tmp_path,
platform=None,
build_plan=fake_build_plan,
partitions=["default", "mypartition"],
)
assert service._lcm is None
service.setup()
assert service._lcm is not None
assert service._lcm._project_info.partitions == ["default", "mypartition"]


def test_prime_dir(lifecycle_service, tmp_path):
prime_dir = lifecycle_service.prime_dir

Expand Down
61 changes: 61 additions & 0 deletions tests/unit/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -1178,3 +1178,64 @@ def test_process_grammar_no_match(grammar_app, mocker):
project = grammar_app.get_project()
# "source" is empty because "i386" doesn't match any of the grammar statements.
assert project.parts["mypart"]["source"] is None


class FakePartitionsApplication(FakeApplication):
"""A partition using FakeApplication."""

@override
def _setup_partitions(self, yaml_data) -> list[str]:
_ = yaml_data
return ["default", "mypartition"]


@pytest.fixture()
def environment_partitions_project(monkeypatch, tmp_path):
project_dir = tmp_path / "project"
project_dir.mkdir()
project_path = project_dir / "testcraft.yaml"
project_path.write_text(
dedent(
"""
name: myproject
version: 1.2.3
parts:
mypart:
plugin: nil
source-tag: v$CRAFT_PROJECT_VERSION
override-stage: |
touch $CRAFT_STAGE/default
touch $CRAFT_MYPARTITION_STAGE/partition
override-prime: |
touch $CRAFT_PRIME/default
touch $CRAFT_MYPARTITION_PRIME/partition
"""
)
)
monkeypatch.chdir(project_dir)

return project_path


@pytest.mark.usefixtures("enable_partitions")
@pytest.mark.usefixtures("environment_partitions_project")
def test_partition_application_expand_environment(app_metadata, fake_services):
app = FakePartitionsApplication(app_metadata, fake_services)
project = app.get_project(build_for=get_host_architecture())

assert craft_parts.Features().enable_partitions is True
# Make sure the project is loaded correctly (from the cwd)
assert project is not None
assert project.parts["mypart"]["source-tag"] == "v1.2.3"
assert project.parts["mypart"]["override-stage"] == dedent(
f"""\
touch {app.project_dir}/stage/default
touch {app.project_dir}/partitions/mypartition/stage/partition
"""
)
assert project.parts["mypart"]["override-prime"] == dedent(
f"""\
touch {app.project_dir}/prime/default
touch {app.project_dir}/partitions/mypartition/prime/partition
"""
)

0 comments on commit a348885

Please sign in to comment.