diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c226f0a69..573468693 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 - name: Install python packages and dependencies @@ -60,11 +60,11 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install apt build dependencies diff --git a/craft_parts/callbacks.py b/craft_parts/callbacks.py index 096063a56..6982ab9c2 100644 --- a/craft_parts/callbacks.py +++ b/craft_parts/callbacks.py @@ -19,6 +19,7 @@ import itertools import logging from collections import namedtuple +from pathlib import Path from typing import Callable, Iterable, List, Optional, Set, Union from craft_parts import errors @@ -30,9 +31,13 @@ FilterCallback = Callable[[ProjectInfo], Iterable[str]] ExecutionCallback = Callable[[ProjectInfo], None] StepCallback = Callable[[StepInfo], bool] -Callback = Union[FilterCallback, ExecutionCallback, StepCallback] +ConfigureOverlayCallback = Callable[[Path, ProjectInfo], None] +Callback = Union[ + FilterCallback, ExecutionCallback, StepCallback, ConfigureOverlayCallback +] _STAGE_PACKAGE_FILTERS: List[CallbackHook] = [] +_OVERLAY_HOOKS: List[CallbackHook] = [] _PROLOGUE_HOOKS: List[CallbackHook] = [] _EPILOGUE_HOOKS: List[CallbackHook] = [] _PRE_STEP_HOOKS: List[CallbackHook] = [] @@ -54,6 +59,22 @@ def register_stage_packages_filter(func: FilterCallback) -> None: _STAGE_PACKAGE_FILTERS.append(CallbackHook(func, None)) +def register_configure_overlay(func: ConfigureOverlayCallback) -> None: + """Register a callback function to configure the mounted overlay. + + This "hook" is called after the overlay's package cache layer is mounted, but + *before* the package list is refreshed. It can be used to configure the + overlay's system, typically to install extra package repositories for Apt. + Note that when the hook is called the overlay is mounted but *not* chroot'ed + into. + + :param func: The callback function that will be called with the location of + the overlay mount and the project info. + """ + _ensure_not_defined(func, _OVERLAY_HOOKS) + _OVERLAY_HOOKS.append(CallbackHook(func, None)) + + def register_prologue(func: ExecutionCallback) -> None: """Register an execution prologue callback function. @@ -101,6 +122,7 @@ def register_post_step( def unregister_all() -> None: """Clear all existing registered callback functions.""" _STAGE_PACKAGE_FILTERS[:] = [] + _OVERLAY_HOOKS[:] = [] _PROLOGUE_HOOKS[:] = [] _EPILOGUE_HOOKS[:] = [] _PRE_STEP_HOOKS[:] = [] @@ -122,6 +144,16 @@ def get_stage_packages_filters(project_info: ProjectInfo) -> Optional[Set[str]]: ) +def run_configure_overlay(overlay_dir: Path, project_info: ProjectInfo) -> None: + """Run all registered 'configure overlay' callbacks. + + :param overlay_dir: The location where the overlay is mounted. + :param project_info: The project information to be sent to callback functions. + """ + for hook in _OVERLAY_HOOKS: + hook.function(overlay_dir, project_info) + + def run_prologue(project_info: ProjectInfo) -> None: """Run all registered execution prologue callbacks. diff --git a/craft_parts/executor/executor.py b/craft_parts/executor/executor.py index 618410ec3..bbde0cd6b 100644 --- a/craft_parts/executor/executor.py +++ b/craft_parts/executor/executor.py @@ -95,6 +95,9 @@ def prologue(self) -> None: # overlay packages. if any(p.spec.overlay_packages for p in self._part_list): with overlays.PackageCacheMount(self._overlay_manager) as ctx: + callbacks.run_configure_overlay( + self._project_info.overlay_mount_dir, self._project_info + ) ctx.refresh_packages_list() callbacks.run_prologue(self._project_info) diff --git a/docs/changelog.rst b/docs/changelog.rst index bc0af4210..909f236c1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,11 @@ Changelog - Tox and packaging updates - Documentation updates +1.19.5 (2023-05-23) +------------------- + +- Revert pyproject.toml change (breaks semantic versioning) + 1.19.4 (2023-05-19) ------------------- diff --git a/tests/conftest.py b/tests/conftest.py index 8c210b318..7a9b41d05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -131,9 +131,9 @@ def fake_snapd(): socket_path_patcher = mock.patch( "craft_parts.packages.snaps.get_snapd_socket_path_template" ) + escaped_path = snapd_fake_socket_path.replace("/", "%2F") mock_socket_path = socket_path_patcher.start() - mock_socket_path.return_value = f'\ - http+unix://{snapd_fake_socket_path.replace("/", "%2F")}/v2/{{}}' + mock_socket_path.return_value = f"http+unix://{escaped_path}/v2/{{}}" thread = server.start_fake_server(snapd_fake_socket_path) diff --git a/tests/unit/executor/test_executor.py b/tests/unit/executor/test_executor.py index 656aeb1b9..52ba17c9f 100644 --- a/tests/unit/executor/test_executor.py +++ b/tests/unit/executor/test_executor.py @@ -199,6 +199,40 @@ def test_prologue_overlay_packages(self, enable_overlay_feature, new_dir, mocker with ExecutionContext(executor=e): assert not mock_mount.called + def test_configure_overlay(self, enable_overlay_feature, new_dir, mocker): + """Check that the configure_overlay callback is called when mounting the overlay's package cache.""" + + mocker.patch.object(overlays.OverlayManager, "mount_pkg_cache") + mocker.patch.object(overlays.OverlayManager, "unmount") + + # This list will contain a record of the calls that are made, in order. + call_order = [] + + def configure_overlay(overlay_dir: Path, project_info: ProjectInfo) -> None: + call_order.append(f"configure_overlay: {overlay_dir} {project_info.custom}") + + def refresh_packages_list() -> None: + call_order.append("refresh_packages_list") + + callbacks.register_configure_overlay(configure_overlay) + mocker.patch.object( + overlays.PackageCacheMount, + "refresh_packages_list", + side_effect=refresh_packages_list, + ) + + p1 = Part("p1", {"plugin": "nil", "overlay-packages": ["fake-pkg"]}) + info = ProjectInfo(application_name="test", cache_dir=new_dir, custom="custom") + e = Executor(project_info=info, part_list=[p1]) + + with ExecutionContext(executor=e): + # The `configure_overlay()` callback must've been called _before_ + # refresh_packages_list(). + assert call_order == [ + f"configure_overlay: {info.overlay_mount_dir} custom", + "refresh_packages_list", + ] + def test_capture_stdout(self, capfd, new_dir): def cbf(info): print(f"prologue {info.custom}") diff --git a/tests/unit/test_callbacks.py b/tests/unit/test_callbacks.py index b4fd6986e..54f4107f0 100644 --- a/tests/unit/test_callbacks.py +++ b/tests/unit/test_callbacks.py @@ -59,6 +59,16 @@ def _callback_filter_2(info: ProjectInfo) -> Generator[str, None, None]: return (i for i in ["d", "e", "f"]) +def _callback_overlay_1(overlay_dir: Path, info: ProjectInfo) -> None: + greet = getattr(info, "greet") + print(f"{overlay_dir} {greet} 1") + + +def _callback_overlay_2(overlay_dir: Path, info: ProjectInfo) -> None: + greet = getattr(info, "greet") + print(f"{overlay_dir} {greet} 2") + + class TestCallbackRegistration: """Test different scenarios of callback function registration.""" @@ -133,6 +143,19 @@ def test_register_stage_packages_filters(self): # But we can register a different one callbacks.register_stage_packages_filter(_callback_filter_2) + def test_register_configure_overlay(self): + callbacks.register_configure_overlay(_callback_overlay_1) + + # A callback function shouldn't be registered again + with pytest.raises(errors.CallbackRegistrationError) as raised: + callbacks.register_configure_overlay(_callback_overlay_1) + assert raised.value.message == ( + "callback function '_callback_overlay_1' is already registered." + ) + + # But we can register a different one + callbacks.register_configure_overlay(_callback_overlay_2) + def test_register_both_pre_and_post(self): callbacks.register_pre_step(_callback_1) callbacks.register_post_step(_callback_1) @@ -144,6 +167,8 @@ def test_register_both_prologue_and_epilogue(self): def test_unregister_all(self): callbacks.register_stage_packages_filter(_callback_filter_1) callbacks.register_stage_packages_filter(_callback_filter_2) + callbacks.register_configure_overlay(_callback_overlay_1) + callbacks.register_configure_overlay(_callback_overlay_2) callbacks.register_pre_step(_callback_1) callbacks.register_post_step(_callback_1) callbacks.register_prologue(_callback_3) @@ -151,6 +176,8 @@ def test_unregister_all(self): callbacks.unregister_all() callbacks.register_stage_packages_filter(_callback_filter_1) callbacks.register_stage_packages_filter(_callback_filter_2) + callbacks.register_configure_overlay(_callback_overlay_1) + callbacks.register_configure_overlay(_callback_overlay_2) callbacks.register_pre_step(_callback_1) callbacks.register_post_step(_callback_1) callbacks.register_prologue(_callback_3) @@ -241,3 +268,14 @@ def test_filter_package_list(self, capfd, funcs, result, message): out, err = capfd.readouterr() assert not err assert out == message + + def test_configure_callback(self, capfd): + callbacks.register_configure_overlay(_callback_overlay_1) + callbacks.register_configure_overlay(_callback_overlay_2) + + overlay_dir = Path("/overlay/mount") + callbacks.run_configure_overlay(overlay_dir, self._project_info) + + out, err = capfd.readouterr() + assert not err + assert out == "/overlay/mount hello 1\n/overlay/mount hello 2\n"