Repository: canonical/charmcraft

Issue:
Multi-base charm cannot be uploaded to GitHub Actions artifact
### Bug Description

Packing a charm with
```yaml
platforms:
  ubuntu@22.04:amd64:
```

creates a *.charm file that contains the `:` character (e.g. `mysql-router-k8s_ubuntu@22.04:amd64.charm`), which cannot be uploaded as a GitHub Actions artifact or to a GitHub release (example: https://github.com/canonical/mysql-router-k8s-operator/actions/runs/12428934287/job/34701456930?pr=341#step:13:15)

Request: rename the *.charm file so that it does not contain the `:` character

### To Reproduce

```
git clone https://github.com/canonical/mysql-router-k8s-operator
cd mysql-router-k8s-operator
git checkout 6c459ad4d9979942e50693d9394c59330c7138c3
charmcraft pack -v
```

### Environment

Ubuntu 22.04
```
$ snap list
Name              Version                  Rev    Tracking            Publisher          Notes
charmcraft        3.2.2.post133+g455880a4  5682   3.x/edge            canonical✓         classic
core18            20240920                 2846   latest/stable       canonical✓         base
core20            20240911                 2434   latest/stable       canonical✓         base
core22            20241119                 1722   latest/stable       canonical✓         base
core24            20240920                 609    latest/stable       canonical✓         base
juju              3.6.1                    29241  3/stable            canonical✓         -
lxd               5.0.4-497fe1e            31333  5.0/stable/…        canonical✓         -
microk8s          v1.29.11                 7435   1.29-strict/stable  canonical✓         -
snapcraft         8.5.1                    13181  latest/stable       canonical✓         classic
snapd             2.66.1                   23258  latest/stable       canonical✓         snapd
```

### charmcraft.yaml

```yaml
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.

type: charm
platforms:
  ubuntu@22.04:amd64:
  ubuntu@22.04:arm64:
# Files implicitly created by charmcraft without a part:
# - dispatch (https://github.com/canonical/charmcraft/pull/1898)
# - manifest.yaml
#   (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/services/package.py#L259)
# Files implicitly copied/"primed" by charmcraft without a part:
# - actions.yaml, config.yaml, metadata.yaml
#   (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/services/package.py#L290-L293
#   https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/services/package.py#L156-L157)
parts:
  # "poetry-deps" part name is a magic constant
  # https://github.com/canonical/craft-parts/pull/901
  poetry-deps:
    plugin: nil
    build-packages:
      - curl
    override-build: |
      python3 -m pip install --upgrade pip==24.3.1  # renovate: charmcraft-pip-latest
      curl -sSL https://install.python-poetry.org | python3 - --version 1.8.4  # renovate: charmcraft-poetry-latest
      ln -sf "$HOME/.local/bin/poetry" /usr/local/bin/poetry
  # "charm-poetry" part name is arbitrary; use for consistency
  # Avoid using "charm" part name since that has special meaning to charmcraft
  charm-poetry:
    # By default, the `poetry` plugin creates/primes these directories:
    # - lib, src
    #   (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/parts/plugins/_poetry.py#L76-L78)
    # - venv
    #   (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/parts/plugins/_poetry.py#L95
    #   https://github.com/canonical/craft-parts/blob/afb0d652eb330b6aaad4f40fbd6e5357d358de47/craft_parts/plugins/base.py#L270)
    plugin: poetry
    source: .
    after:
      - poetry-deps
    poetry-export-extra-args: ['--only', 'main,charm-libs']
    build-snaps:
      - rustup
    build-packages:
      - libffi-dev
      - libssl-dev
      - pkg-config
    override-build: |
      rustup default stable
      craftctl default
      # Include requirements.txt in *.charm artifact for easier debugging
      cp requirements.txt "$CRAFT_PART_INSTALL/requirements.txt"
  # "files" part name is arbitrary; use for consistency
  files:
    plugin: dump
    source: .
    build-packages:
      - git
    override-build: |
      # Workaround to add unique identifier (git hash) to charm version while specification
      # DA053 - Charm versioning
      # (https://docs.google.com/document/d/1Jv1jhWLl8ejK3iJn7Q3VbCIM9GIhp8926bgXpdtx-Sg/edit?pli=1)
      # is pending review.
      python3 -c 'import pathlib; import shutil; import subprocess; git_hash=subprocess.run(["git", "describe", "--always", "--dirty"], capture_output=True, check=True, encoding="utf-8").stdout; file = pathlib.Path("charm_version"); shutil.copy(file, pathlib.Path("charm_version.backup")); version = file.read_text().strip(); file.write_text(f"{version}+{git_hash}")'

      craftctl default
    prime:
      - LICENSE
      - charm_version
      - workload_version
      - scripts
      - templates
```


### Relevant log output

```shell
Packing charm mysql-router-k8s_ubuntu@22.04:amd64.charm 
Packed mysql-router-k8s_ubuntu@22.04:amd64.charm
```


Discussion:

Patch:
diff --git a/charmcraft/services/package.py b/charmcraft/services/package.py
index fd0d314f..74206b30 100644
--- a/charmcraft/services/package.py
+++ b/charmcraft/services/package.py
@@ -28,9 +28,8 @@ from typing import TYPE_CHECKING, cast
 import craft_application
 import craft_platforms
 import yaml
-from craft_application import services, util
+from craft_application import services
 from craft_cli import emit
-from craft_providers import bases
 
 import charmcraft
 from charmcraft import const, errors, models, utils
@@ -121,24 +120,8 @@ class PackageService(services.PackageService):
 
     def get_charm_path(self, dest_dir: pathlib.Path) -> pathlib.Path:
         """Get a charm file name for the appropriate set of run-on bases."""
-        if self._platform:
-            return dest_dir / f"{self._project.name}_{self._platform}.charm"
-        build_plan = models.CharmcraftBuildPlanner.model_validate(
-            self._project.marshal()
-        ).get_build_plan()
-        platform = utils.get_os_platform()
-        build_on_base = bases.BaseName(name=platform.system, version=platform.release)
-        host_arch = util.get_host_architecture()
-        for build_info in build_plan:
-            print(build_info)
-            if build_info.build_on != host_arch:
-                continue
-            if build_info.base == build_on_base:
-                return dest_dir / f"{self._project.name}_{build_info.platform}.charm"
-
-        raise errors.CraftError(
-            "Current machine is not a valid build platform for this charm.",
-        )
+        platform = self._platform.replace(":", "-")
+        return dest_dir / f"{self._project.name}_{platform}.charm"
 
     @property
     def metadata(self) -> BundleMetadata | CharmMetadata:


Test patch:
diff --git a/tests/spread/smoketests/multi-base/basic/expected-charms.txt b/tests/spread/smoketests/multi-base/basic/expected-charms.txt
index f2a23cf1..1bc60acb 100644
--- a/tests/spread/smoketests/multi-base/basic/expected-charms.txt
+++ b/tests/spread/smoketests/multi-base/basic/expected-charms.txt
@@ -1,3 +1,3 @@
-test-charm_ubuntu@20.04:amd64.charm
-test-charm_ubuntu@22.04:amd64.charm
+test-charm_ubuntu@20.04-amd64.charm
+test-charm_ubuntu@22.04-amd64.charm
 test-charm_noble-amd64.charm
diff --git a/tests/spread/smoketests/multi-base/one-platform/expected-charms.txt b/tests/spread/smoketests/multi-base/one-platform/expected-charms.txt
index 62b1f70e..c2478943 100644
--- a/tests/spread/smoketests/multi-base/one-platform/expected-charms.txt
+++ b/tests/spread/smoketests/multi-base/one-platform/expected-charms.txt
@@ -1,1 +1,1 @@
-test-charm_ubuntu@22.04:amd64.charm
+test-charm_ubuntu@22.04-amd64.charm
diff --git a/tests/unit/services/test_package.py b/tests/unit/services/test_package.py
index e77d194e..b0f6d0c0 100644
--- a/tests/unit/services/test_package.py
+++ b/tests/unit/services/test_package.py
@@ -86,16 +86,39 @@ def test_get_metadata(package_service, simple_charm: BasesCharm, metadata):
 
 
 @pytest.mark.parametrize(
-    ("bases", "expected_name"),
+    ("build_plan", "expected_name"),
     [
-        (
-            [SIMPLE_BUILD_BASE],
+        pytest.param(
+            [
+                BuildInfo(
+                    platform="distro-1-test64",
+                    build_on="riscv64",
+                    build_for="riscv64",
+                    base=BaseName("ubuntu", "24.04"),
+                )
+            ],
             "charmy-mccharmface_distro-1-test64.charm",
+            id="simple",
+        ),
+        pytest.param(
+            [
+                BuildInfo(
+                    platform="ubuntu@24.04:riscv64",
+                    build_on="riscv64",
+                    build_for="riscv64",
+                    base=BaseName("ubuntu", "24.04"),
+                )
+            ],
+            "charmy-mccharmface_ubuntu@24.04-riscv64.charm",
+            id="multi-base",
         ),
     ],
 )
-def test_get_charm_path(fake_path, package_service, bases, expected_name):
+def test_get_charm_path(fake_path, package_service, build_plan, expected_name):
     fake_prime_dir = fake_path / "prime"
+    package_service._build_plan = build_plan
+    package_service._platform = build_plan[0].platform
+
     charm_path = package_service.get_charm_path(fake_prime_dir)
 
     assert charm_path == fake_prime_dir / expected_name