Repository: canonical/charmcraft

Issue:
`CRAFT_SHARED_CACHE` broken or ignored in charmcraft 3.1
### Bug Description

`CRAFT_SHARED_CACHE` appears to be broken in charmcraft 3.1

charmcraft ignores wheels in `CRAFT_SHARED_CACHE` and doesn't appear to populate `CRAFT_SHARED_CACHE` after running a pack

### To Reproduce

Reproduce issue with `CRAFT_SHARED_CACHE` not being populated
```
$ git clone https://github.com/canonical/mysql-router-k8s-operator/
$ cd mysql-router-k8s-operator
$ sudo snap install charmcraft --channel candidate --classic
charmcraft (candidate) 3.1.1 from Canonical✓ installed
$ ls ~/snap/
lxd
$ tox run -e build-production -- -v
[...]
$ ls ~/snap/charmcraft/common/cache/charmcraft/ -a
.  ..
```

Reproduce issue with wheels in `CRAFT_SHARED_CACHE` being ignored
```
$ git clone https://github.com/canonical/mysql-router-k8s-operator/
$ cd mysql-router-k8s-operator
$ sudo snap install charmcraft --channel candidate --classic
charmcraft (candidate) 3.1.1 from Canonical✓ installed
$ tox run -e build-dev -- -v
[...]
# Charmcraft builds wheels from source when it should use already built wheels
```

### Environment

Ubuntu 22.04

### charmcraft.yaml

```shell
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.

type: charm
bases:
  - name: ubuntu
    channel: "22.04"
    architectures: [amd64]
  - name: ubuntu
    channel: "22.04"
    architectures: [arm64]
parts:
  charm:
    override-pull: |
      craftctl default
      if [[ ! -f requirements.txt ]]
      then
          echo 'ERROR: Use "tox run -e build-dev" instead of calling "charmcraft pack" directly' >&2
          exit 1
      fi
    charm-strict-dependencies: true
    charm-entrypoint: src/kubernetes_charm.py
    prime:
      - charm_version
      - workload_version
      - scripts
    build-packages:
      - libffi-dev
      - libssl-dev
      - pkg-config
      - rustc
      - cargo
```


### Relevant log output
For issue with `CRAFT_SHARED_CACHE` not being populated
[terminal_output_no_cache.txt](https://github.com/user-attachments/files/16372863/terminal_output_no_cache.txt)
[charmcraft-20240725-074836.719086.log](https://github.com/user-attachments/files/16372866/charmcraft-20240725-074836.719086.log)

For issue with wheels in `CRAFT_SHARED_CACHE` being ignored
[terminal_output_cache.txt](https://github.com/user-attachments/files/16373073/terminal_output_cache.txt)
[charmcraft-20240725-081921.214270.log](https://github.com/user-attachments/files/16373080/charmcraft-20240725-081921.214270.log)

Discussion:

Patch:
diff --git a/charmcraft/services/provider.py b/charmcraft/services/provider.py
index 05be9b1b..910c4b4e 100644
--- a/charmcraft/services/provider.py
+++ b/charmcraft/services/provider.py
@@ -18,9 +18,13 @@
 from __future__ import annotations
 
 import os
+import pathlib
 
+import craft_providers
 from craft_application import services
+from craft_providers import bases
 
+from charmcraft import env
 from charmcraft.const import EXPERIMENTAL_EXTENSIONS_ENV_VAR
 
 
@@ -35,3 +39,28 @@ class ProviderService(services.ProviderService):
         for env_key in ["http_proxy", "https_proxy", "no_proxy", EXPERIMENTAL_EXTENSIONS_ENV_VAR]:
             if env_key in os.environ:
                 self.environment[env_key] = os.environ[env_key]
+
+    def get_base(
+        self,
+        base_name: bases.BaseName | tuple[str, str],
+        *,
+        instance_name: str,
+        **kwargs: bool | str | None | pathlib.Path,
+    ) -> craft_providers.Base:
+        """Get the base configuration from a base name.
+
+        :param base_name: The base to lookup.
+        :param instance_name: A name to assign to the instance.
+        :param kwargs: Additional keyword arguments are sent directly to the base.
+
+        If no cache_path is included, adds one.
+        """
+        # Forward the shared cache path.
+        if "cache_path" not in kwargs:
+            kwargs["cache_path"] = env.get_host_shared_cache_path()
+        return super().get_base(
+            base_name,
+            instance_name=instance_name,
+            # craft-application annotation is incorrect
+            **kwargs,  # type: ignore[arg-type]
+        )


Test patch:
diff --git a/tests/unit/services/test_provider.py b/tests/unit/services/test_provider.py
new file mode 100644
index 00000000..ff358323
--- /dev/null
+++ b/tests/unit/services/test_provider.py
@@ -0,0 +1,78 @@
+# Copyright 2024 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# For further info, check https://github.com/canonical/charmcraft
+"""Unit tests for the provider service."""
+
+import pathlib
+
+import pytest
+from craft_providers import bases
+
+from charmcraft import models, services
+
+
+@pytest.fixture()
+def provider_service(
+    fake_path: pathlib.Path,
+    service_factory: services.CharmcraftServiceFactory,
+    default_build_plan: list[models.CharmBuildInfo],
+) -> services.ProviderService:
+    fake_cache_dir = fake_path / "cache"
+    fake_cache_dir.mkdir(parents=True)
+
+    service_factory.set_kwargs(
+        "provider",
+        work_dir=fake_path,
+        build_plan=default_build_plan,
+        provider_name="host",
+    )
+
+    return service_factory.provider
+
+
+@pytest.mark.parametrize(
+    "base_name",
+    [
+        bases.BaseName("ubuntu", "20.04"),
+        bases.BaseName("ubuntu", "22.04"),
+        bases.BaseName("ubuntu", "24.04"),
+        bases.BaseName("ubuntu", "devel"),
+        pytest.param(
+            bases.BaseName("centos", "7"),
+            marks=[
+                pytest.mark.xfail(
+                    raises=AssertionError,
+                    strict=True,
+                    reason="https://github.com/canonical/craft-providers/issues/608",
+                )
+            ],
+        ),
+        bases.BaseName("almalinux", "9"),
+    ],
+)
+def test_get_base_forwards_cache(
+    monkeypatch,
+    provider_service: services.ProviderService,
+    fake_path: pathlib.Path,
+    base_name: bases.BaseName,
+):
+    monkeypatch.setattr("charmcraft.env.get_host_shared_cache_path", lambda: fake_path / "cache")
+
+    base = provider_service.get_base(
+        base_name=base_name,
+        instance_name="charmcraft-test-instance",
+    )
+
+    assert base._cache_path == fake_path / "cache"