Repository: canonical/charmcraft

Issue:
Charmcraft pack does not work when updating to 3.1.1 on osx
### Bug Description

Running `charmcraft pack` fails on a charm that used to build perfectly. I just upgraded from charmcraft 2.6 to 3.1.1 and the error started coming up.
```
cannot inject snap 'charmcraft' from host on a non-linux system
install the snap from the store by setting the 'channel' parameter
```



### To Reproduce

Run `charmcraft pack` on any charm.

### Environment

OSX 14.5

### charmcraft.yaml

```shell
type: charm

# Charm build options
bases:
- name: ubuntu
  channel: "20.04"
  architectures: [amd64]
- name: ubuntu
  channel: "20.04"
  architectures: [arm64]
- name: ubuntu
  channel: "22.04"
  architectures: [amd64]
- name: ubuntu
  channel: "22.04"
  architectures: [arm64]
parts:
  charm:
    build-packages:
    - git
    - libffi-dev
    - libssl-dev
    - rustc
    - cargo
    - pkg-config
```


### Relevant log output

```shell
2024-07-26 16:10:34.135 Starting charmcraft, version 3.1.1
2024-07-26 16:10:34.135 Log verbosity level set to BRIEF
2024-07-26 16:10:34.135 Configuring application...
2024-07-26 16:10:34.135 Preparing application...
2024-07-26 16:10:34.136 Build plan: platform=None, build_for=None
2024-07-26 16:10:34.139 Loading project file '/Users/X/Y/anbox-cloud-charms/charms/gateway-ops/charmcraft.yaml'
2024-07-26 16:10:34.142 Processing grammar (on arm64 for arm64)
2024-07-26 16:10:34.142 Processing grammar for build-packages: ['git', 'libffi-dev', 'libssl-dev', 'rustc', 'cargo', 'pkg-config']
2024-07-26 16:10:34.143 Running charmcraft:ubuntu-20.04-arm64 in arm64 instance...
2024-07-26 16:10:34.143 Preparing managed instance 'charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000'
2024-07-26 16:10:34.144 Using hostname 'charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000'
2024-07-26 16:10:34.144 Not loading snap config because charmcraft is not running as a snap.
2024-07-26 16:10:34.144 No snap config found.
2024-07-26 16:10:34.144 Using default provider 'multipass' on non-linux system.
2024-07-26 16:10:34.144 Executing on host: multipass version
2024-07-26 16:10:34.200 Launching managed ubuntu 20.04 instance...
2024-07-26 16:10:34.200 Executing on host: multipass list --format json
2024-07-26 16:10:34.758 Executing on host: multipass launch snapcraft:20.04 --name charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 --cpus 2 --memory 2G --disk 64G
2024-07-26 16:14:11.751 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- test -f /etc/craft-instance.conf
2024-07-26 16:14:11.886 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mktemp
2024-07-26 16:14:11.951 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown ubuntu:ubuntu /tmp/tmp.R74rfbhnhq
2024-07-26 16:14:12.010 Created temporary file '/tmp/tmp.R74rfbhnhq' inside instance.
2024-07-26 16:14:12.111 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown root:root /tmp/tmp.R74rfbhnhq
2024-07-26 16:14:12.170 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chmod 0644 /tmp/tmp.R74rfbhnhq
2024-07-26 16:14:12.237 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mv /tmp/tmp.R74rfbhnhq /etc/craft-instance.conf
2024-07-26 16:14:12.322 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- cat /etc/os-release
2024-07-26 16:14:12.409 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- test -f /etc/craft-instance.conf
2024-07-26 16:14:12.487 Executing on host: multipass transfer charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000:/etc/craft-instance.conf /Users/X/tmpf9n2wxmm.tmp-craft/tmp331tcifs
2024-07-26 16:14:12.560 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- test -f /etc/craft-instance.conf
2024-07-26 16:14:12.634 Executing on host: multipass transfer charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000:/etc/craft-instance.conf /Users/X/tmpz0lnwyo9.tmp-craft/tmphqkatn20
2024-07-26 16:14:12.705 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mktemp
2024-07-26 16:14:12.777 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown ubuntu:ubuntu /tmp/tmp.oprXhJECf5
2024-07-26 16:14:12.829 Created temporary file '/tmp/tmp.oprXhJECf5' inside instance.
2024-07-26 16:14:12.893 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown root:root /tmp/tmp.oprXhJECf5
2024-07-26 16:14:12.957 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chmod 0644 /tmp/tmp.oprXhJECf5
2024-07-26 16:14:13.019 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mv /tmp/tmp.oprXhJECf5 /etc/craft-instance.conf
2024-07-26 16:14:13.071 No cache path set, not mounting cache directories.
2024-07-26 16:14:13.071 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mktemp
2024-07-26 16:14:13.124 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown ubuntu:ubuntu /tmp/tmp.kzGXcJXciV
2024-07-26 16:14:13.186 Created temporary file '/tmp/tmp.kzGXcJXciV' inside instance.
2024-07-26 16:14:13.245 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown root:root /tmp/tmp.kzGXcJXciV
2024-07-26 16:14:13.305 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chmod 0644 /tmp/tmp.kzGXcJXciV
2024-07-26 16:14:13.369 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mv /tmp/tmp.kzGXcJXciV /etc/environment
2024-07-26 16:14:13.423 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mktemp
2024-07-26 16:14:13.485 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown ubuntu:ubuntu /tmp/tmp.LAKJISGL1u
2024-07-26 16:14:13.540 Created temporary file '/tmp/tmp.LAKJISGL1u' inside instance.
2024-07-26 16:14:13.608 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown root:root /tmp/tmp.LAKJISGL1u
2024-07-26 16:14:13.670 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chmod 0644 /tmp/tmp.LAKJISGL1u
2024-07-26 16:14:13.733 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mv /tmp/tmp.LAKJISGL1u /etc/apt/apt.conf.d/20auto-upgrades
2024-07-26 16:14:13.780 Waiting for environment to be ready...
2024-07-26 16:14:13.780 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- systemctl is-system-running
2024-07-26 16:14:13.840 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mktemp
2024-07-26 16:14:13.894 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown ubuntu:ubuntu /tmp/tmp.A1fOAIxVSW
2024-07-26 16:14:13.958 Created temporary file '/tmp/tmp.A1fOAIxVSW' inside instance.
2024-07-26 16:14:14.022 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown root:root /tmp/tmp.A1fOAIxVSW
2024-07-26 16:14:14.082 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chmod 0644 /tmp/tmp.A1fOAIxVSW
2024-07-26 16:14:14.133 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mv /tmp/tmp.A1fOAIxVSW /etc/hostname
2024-07-26 16:14:14.196 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- hostname -F /etc/hostname
2024-07-26 16:14:14.255 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
2024-07-26 16:14:14.322 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- systemctl enable systemd-resolved
2024-07-26 16:14:14.536 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- systemctl restart systemd-resolved
2024-07-26 16:14:14.656 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mktemp
2024-07-26 16:14:14.732 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown ubuntu:ubuntu /tmp/tmp.qEjbCHw3eF
2024-07-26 16:14:14.800 Created temporary file '/tmp/tmp.qEjbCHw3eF' inside instance.
2024-07-26 16:14:14.866 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown root:root /tmp/tmp.qEjbCHw3eF
2024-07-26 16:14:14.928 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chmod 0644 /tmp/tmp.qEjbCHw3eF
2024-07-26 16:14:14.986 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mv /tmp/tmp.qEjbCHw3eF /etc/systemd/network/10-eth0.network
2024-07-26 16:14:15.036 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- systemctl enable systemd-networkd
2024-07-26 16:14:15.239 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- systemctl restart systemd-networkd
2024-07-26 16:14:15.415 Waiting for networking to be ready...
2024-07-26 16:14:15.415 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- getent hosts snapcraft.io
2024-07-26 16:14:15.738 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mktemp
2024-07-26 16:14:15.818 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown ubuntu:ubuntu /tmp/tmp.MlMY3cJFTD
2024-07-26 16:14:15.895 Created temporary file '/tmp/tmp.MlMY3cJFTD' inside instance.
2024-07-26 16:14:15.955 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown root:root /tmp/tmp.MlMY3cJFTD
2024-07-26 16:14:16.017 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chmod 0644 /tmp/tmp.MlMY3cJFTD
2024-07-26 16:14:16.070 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mv /tmp/tmp.MlMY3cJFTD /etc/apt/apt.conf.d/00no-recommends
2024-07-26 16:14:16.126 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mktemp
2024-07-26 16:14:16.176 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown ubuntu:ubuntu /tmp/tmp.qOdl4QUBvV
2024-07-26 16:14:16.232 Created temporary file '/tmp/tmp.qOdl4QUBvV' inside instance.
2024-07-26 16:14:16.283 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chown root:root /tmp/tmp.qOdl4QUBvV
2024-07-26 16:14:16.343 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- chmod 0644 /tmp/tmp.qOdl4QUBvV
2024-07-26 16:14:16.404 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- mv /tmp/tmp.qOdl4QUBvV /etc/apt/apt.conf.d/00update-errors
2024-07-26 16:14:16.459 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- apt-get update
2024-07-26 16:14:19.623 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- apt-get install -y apt-utils build-essential curl fuse udev python3 python3-dev python3-pip python3-wheel python3-setuptools gpg dirmngr
2024-07-26 16:15:23.229 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- systemctl is-active systemd-udevd
2024-07-26 16:15:23.320 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- apt-get install -y snapd
2024-07-26 16:15:23.572 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- ln -sf /var/lib/snapd/snap /snap
2024-07-26 16:15:23.651 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- systemctl enable --now snapd.socket
2024-07-26 16:15:23.822 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- systemctl restart snapd.service
2024-07-26 16:15:24.108 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- snap wait system seed.loaded
2024-07-26 16:15:24.197 Holding refreshes for snaps.
2024-07-26 16:15:24.197 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- snap refresh --hold
2024-07-26 16:15:24.430 Waiting for pending snap refreshes to complete.
2024-07-26 16:15:24.431 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- snap watch '--last=auto-refresh?'
2024-07-26 16:15:24.570 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- snap unset system proxy.http
2024-07-26 16:15:24.780 Executing on host: multipass exec charmcraft-anbox-stream-gateway-on-arm64-for-arm64-58268000 -- sudo -H -- snap unset system proxy.https
2024-07-26 16:15:25.060 Installing snap 'charmcraft' with channel=None and classic=True
2024-07-26 16:15:25.061 cannot inject snap 'charmcraft' from host on a non-linux system
install the snap from the store by setting the 'channel' parameter
2024-07-26 16:15:25.072 Traceback (most recent call last):
2024-07-26 16:15:25.072   File "/opt/homebrew/Cellar/charmcraft/3.1.1/libexec/lib/python3.12/site-packages/craft_providers/multipass/multipass_provider.py", line 236, in launched_environment
2024-07-26 16:15:25.072     instance = launch(
2024-07-26 16:15:25.072                ^^^^^^^
2024-07-26 16:15:25.072   File "/opt/homebrew/Cellar/charmcraft/3.1.1/libexec/lib/python3.12/site-packages/craft_providers/multipass/_launch.py", line 82, in launch
2024-07-26 16:15:25.072     base_configuration.setup(executor=instance)
2024-07-26 16:15:25.072   File "/opt/homebrew/Cellar/charmcraft/3.1.1/libexec/lib/python3.12/site-packages/craft_providers/base.py", line 1044, in setup
2024-07-26 16:15:25.072     self._setup_snaps(executor=executor)
2024-07-26 16:15:25.072   File "/opt/homebrew/Cellar/charmcraft/3.1.1/libexec/lib/python3.12/site-packages/craft_providers/base.py", line 917, in _setup_snaps
2024-07-26 16:15:25.072     self._install_snaps(executor=executor)
2024-07-26 16:15:25.072   File "/opt/homebrew/Cellar/charmcraft/3.1.1/libexec/lib/python3.12/site-packages/craft_providers/base.py", line 647, in _install_snaps
2024-07-26 16:15:25.072     raise BaseConfigurationError(
2024-07-26 16:15:25.072 craft_providers.errors.BaseConfigurationError: cannot inject snap 'charmcraft' from host on a non-linux system
2024-07-26 16:15:25.072 install the snap from the store by setting the 'channel' parameter
2024-07-26 16:15:25.072 
2024-07-26 16:15:25.072 The above exception was the direct cause of the following exception:
2024-07-26 16:15:25.072 Traceback (most recent call last):
2024-07-26 16:15:25.072   File "/opt/homebrew/Cellar/charmcraft/3.1.1/libexec/lib/python3.12/site-packages/craft_application/application.py", line 541, in run
2024-07-26 16:15:25.072     self.run_managed(platform, build_for)
2024-07-26 16:15:25.072   File "/opt/homebrew/Cellar/charmcraft/3.1.1/libexec/lib/python3.12/site-packages/charmcraft/application/main.py", line 137, in run_managed
2024-07-26 16:15:25.072     super().run_managed(platform, build_for)
2024-07-26 16:15:25.072   File "/opt/homebrew/Cellar/charmcraft/3.1.1/libexec/lib/python3.12/site-packages/craft_application/application.py", line 366, in run_managed
2024-07-26 16:15:25.072     with self.services.provider.instance(
2024-07-26 16:15:25.072   File "/opt/homebrew/Cellar/python@3.12/3.12.4/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py", line 137, in __enter__
2024-07-26 16:15:25.072     return next(self.gen)
2024-07-26 16:15:25.072            ^^^^^^^^^^^^^^
2024-07-26 16:15:25.072   File "/opt/homebrew/Cellar/charmcraft/3.1.1/libexec/lib/python3.12/site-packages/craft_application/services/provider.py", line 122, in instance
2024-07-26 16:15:25.072     with provider.launched_environment(
2024-07-26 16:15:25.072   File "/opt/homebrew/Cellar/python@3.12/3.12.4/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py", line 137, in __enter__
2024-07-26 16:15:25.072     return next(self.gen)
2024-07-26 16:15:25.072            ^^^^^^^^^^^^^^
2024-07-26 16:15:25.072   File "/opt/homebrew/Cellar/charmcraft/3.1.1/libexec/lib/python3.12/site-packages/craft_providers/multipass/multipass_provider.py", line 246, in launched_environment
2024-07-26 16:15:25.072     raise MultipassError(str(error)) from error
2024-07-26 16:15:25.072 craft_providers.multipass.errors.MultipassError: cannot inject snap 'charmcraft' from host on a non-linux system
2024-07-26 16:15:25.072 install the snap from the store by setting the 'channel' parameter
2024-07-26 16:15:25.072 Full execution log: '/Users/X/Library/Logs/charmcraft/charmcraft-20240726-161034.135204.log'
```
```


Discussion:

Patch:
diff --git a/charmcraft/application/commands/analyse.py b/charmcraft/application/commands/analyse.py
index 6b92762e..8b160efe 100644
--- a/charmcraft/application/commands/analyse.py
+++ b/charmcraft/application/commands/analyse.py
@@ -85,8 +85,7 @@ class Analyse(base.CharmcraftCommand):
                 filepath, ignore=ignore, include_ignored=False
             ):
                 emit.progress(str(result), permanent=True)
-                if result.level > max_level:
-                    max_level = result.level
+                max_level = max(result.level, max_level)
                 progress.advance(1)
 
         return max_level.return_code
diff --git a/charmcraft/application/main.py b/charmcraft/application/main.py
index d643eda2..21cd48f9 100644
--- a/charmcraft/application/main.py
+++ b/charmcraft/application/main.py
@@ -20,8 +20,9 @@ import pathlib
 import shutil
 from typing import Any
 
+import craft_application
 import craft_cli
-from craft_application import Application, AppMetadata
+from craft_application import util
 from craft_parts.plugins import plugins
 from overrides import override
 
@@ -39,7 +40,7 @@ operator development and collaboration.
 See https://charmhub.io/publishing for more information.
 """
 
-APP_METADATA = AppMetadata(
+APP_METADATA = craft_application.AppMetadata(
     name="charmcraft",
     summary=GENERAL_SUMMARY,
     ProjectClass=models.CharmcraftProject,
@@ -56,12 +57,12 @@ PRIME_BEHAVIOUR_CHANGE_MESSAGE = (
 )
 
 
-class Charmcraft(Application):
+class Charmcraft(craft_application.Application):
     """Charmcraft application definition."""
 
     def __init__(
         self,
-        app: AppMetadata,
+        app: craft_application.AppMetadata,
         services: CharmcraftServiceFactory,
     ) -> None:
         super().__init__(app=app, services=services, extra_loggers={"charmcraft"})
@@ -163,6 +164,21 @@ class Charmcraft(Application):
                     for filename in package_files:
                         shutil.move(str(self._work_dir / filename), output_path / filename)
 
+    def _expand_environment(self, yaml_data: dict[str, Any], build_for: str) -> None:
+        """Perform expansion of project environment variables.
+
+        :param yaml_data: The project's yaml data.
+        :param build_for: The architecture to build for.
+        """
+        if "-" in build_for:
+            build_for = util.get_host_architecture()
+            craft_cli.emit.debug(
+                "Expanding environment variables with the host architecture "
+                f"{build_for!r} as the build-for architecture because multiple "
+                "run-on architectures were specified."
+            )
+        super()._expand_environment(yaml_data, build_for)
+
 
 def main() -> int:
     """Run craft-application based charmcraft."""
diff --git a/charmcraft/services/lifecycle.py b/charmcraft/services/lifecycle.py
index cf0bc840..4989793c 100644
--- a/charmcraft/services/lifecycle.py
+++ b/charmcraft/services/lifecycle.py
@@ -16,7 +16,9 @@
 """Service class for running craft lifecycle commands."""
 from __future__ import annotations
 
-from craft_application import services
+from craft_application import services, util
+from craft_cli import emit
+from overrides import override
 
 
 class LifecycleService(services.LifecycleService):
@@ -26,3 +28,30 @@ class LifecycleService(services.LifecycleService):
         """Do Charmcraft-specific setup work."""
         self._manager_kwargs.setdefault("project_name", self._project.name)
         super().setup()
+
+    @override
+    def _get_build_for(self) -> str:
+        build_for = super()._get_build_for()
+        if "-" not in build_for:
+            if self._build_plan and self._build_plan[0].build_for == "all":
+                emit.progress(
+                    "WARNING: Charmcraft does not validate that charms with "
+                    "architecture 'all' are fully architecture agnostic.",
+                    permanent=True,
+                )
+            return build_for
+
+        # Multi-arch builds: Tell craft-parts that we're building for any foreign
+        # architecture (to trick it into trying to cross-compile anything, leading
+        # to more likely failures.)
+        emit.progress(
+            "WARNING: Charmcraft does not validate that charms with multiple "
+            "given architectures are architecture agnostic.",
+            permanent=True,
+        )
+        host_arch = util.get_host_architecture()
+        for arch in build_for.split("-"):
+            if arch != host_arch:
+                return arch
+
+        return host_arch


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index caaca688..33f2ae18 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -284,3 +284,51 @@ jobs:
           path: |
             dist\charmcraft-installer-self-signed.exe
             dist\charmcraft-installer.msix
+
+  macos-smoke-test:
+    strategy:
+      matrix:
+        os: [macos-12, macos-13]
+    runs-on: ${{ matrix.os }}
+    steps:
+      # Installing and caching homebrew using the action should speed up subsequent CI:
+      # https://github.com/Homebrew/actions/tree/master/setup-homebrew
+      - name: Set up Homebrew
+        id: set-up-homebrew
+        uses: Homebrew/actions/setup-homebrew@master
+      - name: Cache Homebrew Bundler RubyGems
+        id: cache
+        uses: actions/cache@v3
+        with:
+          path: ${{ steps.set-up-homebrew.outputs.gems-path }}
+          key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}
+          restore-keys: ${{ runner.os }}-rubygems-
+      - name: Install Homebrew Bundler RubyGems
+        if: steps.cache.outputs.cache-hit != 'true'
+        run: brew install-bundler-gems
+      - name: Install Multipass
+        run: |
+          brew install multipass
+      - name: Checkout code
+        uses: actions/checkout@v4
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: '3.12'
+          cache: 'pip'
+      - name: Build and install Charmcraft
+        run: |
+          pipx install .
+      - name: Check for fully-configured multipass
+        run: |
+          while ! multipass version; do
+            sleep 1
+          done
+      - name: Init and pack
+        run: |
+          mkdir test-charm
+          cd test-charm
+          export CRAFT_VERBOSITY_LEVEL=trace
+          charmcraft init --profile=machine
+          charmcraft pack
+          test -f *.charm
diff --git a/tests/commands/test_store_api.py b/tests/commands/test_store_api.py
index 686144db..11a22d9a 100644
--- a/tests/commands/test_store_api.py
+++ b/tests/commands/test_store_api.py
@@ -47,7 +47,7 @@ from charmcraft.utils import ResourceOption
 from tests.commands.test_store_client import FakeResponse
 
 
-@pytest.fixture()
+@pytest.fixture
 def client_mock(monkeypatch):
     """Fixture to provide a mocked client."""
     monkeypatch.setattr(platform, "node", lambda: "fake-host")
@@ -56,7 +56,7 @@ def client_mock(monkeypatch):
         yield client_mock
 
 
-@pytest.fixture()
+@pytest.fixture
 def anonymous_client_mock(monkeypatch):
     """Fixture to provide a mocked anonymous client."""
     anonymous_client_mock = MagicMock(spec=AnonymousClient)
diff --git a/tests/commands/test_store_client.py b/tests/commands/test_store_client.py
index 4dfa7860..392e23c4 100644
--- a/tests/commands/test_store_client.py
+++ b/tests/commands/test_store_client.py
@@ -100,7 +100,7 @@ class FakeResponse(requests.Response):
         return self.content
 
 
-@pytest.fixture()
+@pytest.fixture
 def client_class():
     """Return a client instance with craft-store's StoreClient methods mocked."""
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 3cd1591a..1eeecf33 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -38,7 +38,7 @@ from charmcraft.application.main import APP_METADATA
 from charmcraft.models import project
 
 
-@pytest.fixture()
+@pytest.fixture
 def simple_charm():
     return project.BasesCharm(
         type="charm",
@@ -60,7 +60,7 @@ def simple_charm():
     )
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_store_client():
     client = mock.Mock(spec_set=store.Client)
 
@@ -71,12 +71,12 @@ def mock_store_client():
     return client
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_store_anonymous_client() -> mock.Mock:
     return mock.Mock(spec_set=store.AnonymousClient)
 
 
-@pytest.fixture()
+@pytest.fixture
 def service_factory(
     fs,
     fake_project_dir,
@@ -108,7 +108,7 @@ def service_factory(
     return factory
 
 
-@pytest.fixture()
+@pytest.fixture
 def default_build_plan():
     arch = util.get_host_architecture()
     return [
@@ -121,33 +121,33 @@ def default_build_plan():
     ]
 
 
-@pytest.fixture()
+@pytest.fixture
 def fake_project_dir(fs) -> pathlib.Path:
     project_dir = pathlib.Path("/root/project")
     fs.create_dir(project_dir)
     return project_dir
 
 
-@pytest.fixture()
+@pytest.fixture
 def fake_prime_dir(fs) -> pathlib.Path:
     prime_dir = pathlib.Path("/root/prime")
     fs.create_dir(prime_dir)
     return prime_dir
 
 
-@pytest.fixture()
+@pytest.fixture
 def fake_path(fs) -> Iterator[pathlib.Path]:
     """Like tmp_path, but with a fake filesystem."""
     with tempfile.TemporaryDirectory() as tmp_dir:
         yield pathlib.Path(tmp_dir)
 
 
-@pytest.fixture()
+@pytest.fixture
 def global_debug():
     os.environ["CRAFT_DEBUG"] = "1"
 
 
-@pytest.fixture()
+@pytest.fixture
 def new_path(monkeypatch, tmp_path):
     monkeypatch.chdir(tmp_path)
     return tmp_path
@@ -163,7 +163,7 @@ def setup_parts():
     parts.setup_parts()
 
 
-@pytest.fixture()
+@pytest.fixture
 def charmhub_config() -> env.CharmhubConfig:
     """Provide a charmhub config for use in tests"""
     return env.CharmhubConfig(
@@ -194,14 +194,14 @@ def intertests_cleanups():
     callbacks.unregister_all()
 
 
-@pytest.fixture()
+@pytest.fixture
 def responses():
     """Simple helper to use responses module as a fixture, for easier integration in tests."""
     with responses_module.RequestsMock() as rsps:
         yield rsps
 
 
-@pytest.fixture()
+@pytest.fixture
 def prepare_charmcraft_yaml(tmp_path: pathlib.Path):
     """Helper to create a charmcraft.yaml file in disk.
 
@@ -238,7 +238,7 @@ def prepare_file(tmp_path: pathlib.Path, filename: str):
     return prepare
 
 
-@pytest.fixture()
+@pytest.fixture
 def prepare_metadata_yaml(tmp_path: pathlib.Path):
     """Helper to create a metadata.yaml file in disk.
 
@@ -247,7 +247,7 @@ def prepare_metadata_yaml(tmp_path: pathlib.Path):
     return prepare_file(tmp_path, const.METADATA_FILENAME)
 
 
-@pytest.fixture()
+@pytest.fixture
 def prepare_actions_yaml(tmp_path: pathlib.Path):
     """Helper to create a actions.yaml file in disk.
 
@@ -256,7 +256,7 @@ def prepare_actions_yaml(tmp_path: pathlib.Path):
     return prepare_file(tmp_path, const.JUJU_ACTIONS_FILENAME)
 
 
-@pytest.fixture()
+@pytest.fixture
 def prepare_config_yaml(tmp_path: pathlib.Path):
     """Helper to create a config.yaml file in disk.
 
@@ -265,7 +265,7 @@ def prepare_config_yaml(tmp_path: pathlib.Path):
     return prepare_file(tmp_path, const.JUJU_CONFIG_FILENAME)
 
 
-@pytest.fixture()
+@pytest.fixture
 def emitter(emitter):
     """Monkeypatch craft-cli's emitter fixture to easily test the JSON encoded output."""
 
@@ -284,7 +284,7 @@ def emitter(emitter):
     return emitter
 
 
-@pytest.fixture()
+@pytest.fixture
 def assert_output(capsys):
     """Assert that a given string was sent to stdout.
 
@@ -305,7 +305,7 @@ def assert_output(capsys):
     return helper
 
 
-@pytest.fixture()
+@pytest.fixture
 def build_charm_directory():
     def helper(tmp_path, fake_charms, file_type="charm"):
         expected = {}
@@ -324,7 +324,7 @@ def build_charm_directory():
     return helper
 
 
-@pytest.fixture()
+@pytest.fixture
 def stub_extensions(monkeypatch):
     from charmcraft.extensions import registry
 
@@ -334,7 +334,7 @@ def stub_extensions(monkeypatch):
     return extensions_dict
 
 
-@pytest.fixture()
+@pytest.fixture
 def charm_plugin(tmp_path):
     requirement_files = ["reqs1.txt", "reqs2.txt"]
     for req in requirement_files:
@@ -363,7 +363,7 @@ def charm_plugin(tmp_path):
     return plugins.get_plugin(part=part, part_info=part_info, properties=plugin_properties)
 
 
-@pytest.fixture()
+@pytest.fixture
 def bundle_plugin(tmp_path):
     project_dirs = craft_parts.ProjectDirs(work_dir=tmp_path)
     spec = {
diff --git a/tests/extensions/test_extensions.py b/tests/extensions/test_extensions.py
index b93504b1..794b09d9 100644
--- a/tests/extensions/test_extensions.py
+++ b/tests/extensions/test_extensions.py
@@ -98,7 +98,7 @@ class FullExtension(FakeExtension):
         return {"full-extension/new-part": {"plugin": "nil", "source": None}}
 
 
-@pytest.fixture()
+@pytest.fixture
 def fake_extensions(stub_extensions):
     extensions.register(FakeExtension.name, FakeExtension)
     extensions.register(ExperimentalExtension.name, ExperimentalExtension)
diff --git a/tests/extensions/test_registry.py b/tests/extensions/test_registry.py
index 50a78b27..b4cf9f30 100644
--- a/tests/extensions/test_registry.py
+++ b/tests/extensions/test_registry.py
@@ -55,7 +55,7 @@ class FakeExtension3(Extension):
     name = "fake-extension-3"
 
 
-@pytest.fixture()
+@pytest.fixture
 def fake_extensions(stub_extensions):
     fakes = [FakeExtension1, FakeExtension2]
     for ext_class in fakes:
diff --git a/tests/integration/commands/conftest.py b/tests/integration/commands/conftest.py
index 17576e7b..8248b5ce 100644
--- a/tests/integration/commands/conftest.py
+++ b/tests/integration/commands/conftest.py
@@ -20,6 +20,6 @@ import pytest
 from charmcraft import application
 
 
-@pytest.fixture()
+@pytest.fixture
 def config(service_factory):
     return {"app": application.APP_METADATA, "services": service_factory}
diff --git a/tests/integration/commands/test_expand_extensions.py b/tests/integration/commands/test_expand_extensions.py
index 4fbf54d0..bd8c3e61 100644
--- a/tests/integration/commands/test_expand_extensions.py
+++ b/tests/integration/commands/test_expand_extensions.py
@@ -37,7 +37,7 @@ class TestExtension(FakeExtension):
         return {"terms": ["https://example.com/test"]}
 
 
-@pytest.fixture()
+@pytest.fixture
 def fake_extensions(stub_extensions):
     extensions.register(TestExtension.name, TestExtension)
 
diff --git a/tests/integration/commands/test_init.py b/tests/integration/commands/test_init.py
index 813b0be8..a4f63280 100644
--- a/tests/integration/commands/test_init.py
+++ b/tests/integration/commands/test_init.py
@@ -95,7 +95,7 @@ VALID_AUTHORS = [
 ]
 
 
-@pytest.fixture()
+@pytest.fixture
 def init_command():
     return commands.InitCommand({"app": charmcraft.application.APP_METADATA, "services": None})
 
@@ -257,7 +257,7 @@ def test_executable_set(new_path, init_command):
         assert path.stat().st_mode & S_IXALL == S_IXALL
 
 
-@pytest.mark.slow()
+@pytest.mark.slow
 @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
 @pytest.mark.skipif(
     bool(os.getenv("RUNNING_TOX")) and sys.version_info < (3, 11),
diff --git a/tests/integration/commands/test_resource_revisions.py b/tests/integration/commands/test_resource_revisions.py
index 04d15346..3e7f8c63 100644
--- a/tests/integration/commands/test_resource_revisions.py
+++ b/tests/integration/commands/test_resource_revisions.py
@@ -31,7 +31,7 @@ from charmcraft.cmdbase import JSON_FORMAT
 from charmcraft.env import CharmhubConfig
 
 
-@pytest.fixture()
+@pytest.fixture
 def store_mock():
     """The fixture to fake the store layer in all the tests."""
     store_mock = mock.Mock(spec_set=store.Store)
diff --git a/tests/integration/commands/test_set_resource_architectures.py b/tests/integration/commands/test_set_resource_architectures.py
index ffdaefb1..ae40e050 100644
--- a/tests/integration/commands/test_set_resource_architectures.py
+++ b/tests/integration/commands/test_set_resource_architectures.py
@@ -25,7 +25,7 @@ from charmcraft.application.commands import SetResourceArchitecturesCommand
 from tests import get_fake_revision
 
 
-@pytest.fixture()
+@pytest.fixture
 def cmd(service_factory):
     return SetResourceArchitecturesCommand(
         config={"app": application.APP_METADATA, "services": service_factory}
diff --git a/tests/integration/commands/test_store_commands.py b/tests/integration/commands/test_store_commands.py
index 01742b61..873c40cb 100644
--- a/tests/integration/commands/test_store_commands.py
+++ b/tests/integration/commands/test_store_commands.py
@@ -27,7 +27,7 @@ from charmcraft.store.models import Library
 from tests import factory
 
 
-@pytest.fixture()
+@pytest.fixture
 def store_mock():
     """The fixture to fake the store layer in all the tests."""
     store_mock = mock.MagicMock()
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index e47c83af..b65258a6 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -23,14 +23,14 @@ from charmcraft import application, services
 from charmcraft.application import commands
 
 
-@pytest.fixture()
+@pytest.fixture
 def service_factory():
     factory = services.CharmcraftServiceFactory(app=application.APP_METADATA)
     factory.store.client = mock.Mock(spec_set=craft_store.StoreClient)
     return factory
 
 
-@pytest.fixture()
+@pytest.fixture
 def app(monkeypatch, new_path, service_factory):
     monkeypatch.setenv("CRAFT_DEBUG", "1")
     app = application.Charmcraft(app=application.APP_METADATA, services=service_factory)
diff --git a/tests/integration/services/conftest.py b/tests/integration/services/conftest.py
index 693d797f..04704da9 100644
--- a/tests/integration/services/conftest.py
+++ b/tests/integration/services/conftest.py
@@ -24,7 +24,7 @@ from charmcraft import services
 from charmcraft.application.main import APP_METADATA, Charmcraft
 
 
-@pytest.fixture()
+@pytest.fixture
 def service_factory(
     fs: pyfakefs.fake_filesystem.FakeFilesystem, fake_path, simple_charm
 ) -> services.CharmcraftServiceFactory:
diff --git a/tests/integration/services/test_lifecycle.py b/tests/integration/services/test_lifecycle.py
index 1187584a..ff473a44 100644
--- a/tests/integration/services/test_lifecycle.py
+++ b/tests/integration/services/test_lifecycle.py
@@ -16,7 +16,71 @@
 """Integration tests for the lifecycle service."""
 
 
-def test_init_lifecycle(service_factory):
+import distro
+import pytest
+from craft_application import errors, models, util
+from craft_providers import bases
+
+from charmcraft.services import CharmcraftServiceFactory
+
+
+def test_init_lifecycle(service_factory: CharmcraftServiceFactory):
     """Test the setup of a parts lifecycle, implicitly testing setup."""
 
     service_factory.lifecycle._init_lifecycle_manager()
+
+
+def test_lifecycle_build_for_invalid(service_factory: CharmcraftServiceFactory):
+    lifecycle = service_factory.lifecycle
+
+    lifecycle._build_plan = [
+        models.BuildInfo(
+            platform="something",
+            build_on=util.get_host_architecture(),
+            build_for="invalid",
+            base=bases.BaseName(distro.id(), distro.version()),
+        )
+    ]
+
+    with pytest.raises(
+        errors.PartsLifecycleError, match="[Aa]rchitecture '[a-z]+' is not supported"
+    ):
+        lifecycle._init_lifecycle_manager()
+
+
+def test_lifecycle_build_for_all(service_factory: CharmcraftServiceFactory):
+    lifecycle = service_factory.lifecycle
+
+    lifecycle._build_plan = [
+        models.BuildInfo(
+            platform="something",
+            build_on=util.get_host_architecture(),
+            build_for="all",
+            base=bases.BaseName(distro.id(), distro.version()),
+        )
+    ]
+
+    lcm = lifecycle._init_lifecycle_manager()
+
+    assert lcm._target_arch == util.get_host_architecture()
+
+
+def test_lifecycle_build_for_multi(service_factory: CharmcraftServiceFactory):
+    lifecycle = service_factory.lifecycle
+
+    host_arch = util.get_host_architecture()
+    arches = {"arm64", "riscv64", "amd64"} - {host_arch}
+    foreign_arch = next(iter(arches))
+
+    lifecycle._build_plan = [
+        models.BuildInfo(
+            platform="something",
+            build_on=util.get_host_architecture(),
+            build_for=f"{foreign_arch}-{host_arch}",
+            base=bases.BaseName(distro.id(), distro.version()),
+        )
+    ]
+
+    lcm = lifecycle._init_lifecycle_manager()
+
+    assert lcm._target_arch == foreign_arch
diff --git a/tests/integration/services/test_package.py b/tests/integration/services/test_package.py
index 6035e315..cfe9fc0b 100644
--- a/tests/integration/services/test_package.py
+++ b/tests/integration/services/test_package.py
@@ -27,7 +27,7 @@ from charmcraft import const, models, services
 from charmcraft.application.main import APP_METADATA
 
 
-@pytest.fixture()
+@pytest.fixture
 def package_service(fake_path, service_factory, default_build_plan):
     fake_project_dir = fake_path
     svc = services.PackageService(
diff --git a/tests/integration/services/test_store.py b/tests/integration/services/test_store.py
index 05ed870c..f54fe91a 100644
--- a/tests/integration/services/test_store.py
+++ b/tests/integration/services/test_store.py
@@ -21,7 +21,7 @@ import pytest
 from charmcraft import models, services
 
 
-@pytest.fixture()
+@pytest.fixture
 def store_service(service_factory):
     return service_factory.store
 
diff --git a/tests/spread/smoketests/basic/charmcraft-bases-22.04.yaml b/tests/spread/smoketests/basic/charmcraft-bases-22.04.yaml
index 9e89bba8..7ef9504a 100644
--- a/tests/spread/smoketests/basic/charmcraft-bases-22.04.yaml
+++ b/tests/spread/smoketests/basic/charmcraft-bases-22.04.yaml
@@ -10,3 +10,4 @@ bases:
     run-on:
     - name: ubuntu
       channel: "22.04"
+      architectures: [amd64, arm64, riscv64]
diff --git a/tests/spread/smoketests/basic/task.yaml b/tests/spread/smoketests/basic/task.yaml
index 8bf7a3b5..c4ea172c 100644
--- a/tests/spread/smoketests/basic/task.yaml
+++ b/tests/spread/smoketests/basic/task.yaml
@@ -28,7 +28,7 @@ restore: |
 
 execute: |
   cd charm
-  charmcraft pack --verbose
+  charmcraft pack
   test -f *.charm
   unzip -l *.charm | MATCH "src/charm.py"
   unzip -l *.charm | MATCH "venv/ops/charm.py"
diff --git a/tests/test_bases.py b/tests/test_bases.py
index d48d311c..be574315 100644
--- a/tests/test_bases.py
+++ b/tests/test_bases.py
@@ -24,14 +24,14 @@ from charmcraft.models.charmcraft import Base
 from charmcraft.utils import OSPlatform
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_get_os_platform():
     os_platform = OSPlatform(system="host-OS", release="host-CHANNEL", machine="host-ARCH")
     with patch("charmcraft.bases.get_os_platform", return_value=os_platform) as mock_platform:
         yield mock_platform
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_get_host_architecture():
     with patch(
         "craft_application.util.get_host_architecture", return_value="host-ARCH"
diff --git a/tests/test_instrum.py b/tests/test_instrum.py
index 5f775fea..b1602857 100644
--- a/tests/test_instrum.py
+++ b/tests/test_instrum.py
@@ -25,7 +25,7 @@ from charmcraft import instrum
 from charmcraft.instrum import Timer, _Measurements
 
 
-@pytest.fixture()
+@pytest.fixture
 def fake_times():
     """Provide times from 5 by 10."""
     with patch("charmcraft.instrum.time", side_effect=list(range(5, 100, 10))):
diff --git a/tests/test_snap.py b/tests/test_snap.py
index 6103720c..8ed617a1 100644
--- a/tests/test_snap.py
+++ b/tests/test_snap.py
@@ -27,7 +27,7 @@ from charmcraft.snap import (
 )
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_snap_config():
     with mock.patch("charmcraft.snap.snaphelpers.SnapConfig", autospec=True) as mock_snap_config:
         yield mock_snap_config
diff --git a/tests/unit/commands/test_lifecycle.py b/tests/unit/commands/test_lifecycle.py
index cc773e3d..04a210fa 100644
--- a/tests/unit/commands/test_lifecycle.py
+++ b/tests/unit/commands/test_lifecycle.py
@@ -58,7 +58,7 @@ def get_namespace(
     )
 
 
-@pytest.fixture()
+@pytest.fixture
 def pack(service_factory: services.ServiceFactory) -> lifecycle.PackCommand:
     return lifecycle.PackCommand({"app": application.APP_METADATA, "services": service_factory})
 
diff --git a/tests/unit/commands/test_list_extensions.py b/tests/unit/commands/test_list_extensions.py
index 769c81d6..ef8dc495 100644
--- a/tests/unit/commands/test_list_extensions.py
+++ b/tests/unit/commands/test_list_extensions.py
@@ -49,7 +49,7 @@ class YourFakeExtension(FakeExtension):
         return {"terms": ["https://example.com/test2"]}
 
 
-@pytest.fixture()
+@pytest.fixture
 def fake_extensions(stub_extensions):
     extensions.register(MyFakeExtension.name, MyFakeExtension)
     extensions.register(YourFakeExtension.name, YourFakeExtension)
diff --git a/tests/unit/parts/test_reactive.py b/tests/unit/parts/test_reactive.py
index 080715cf..f9eda5ca 100644
--- a/tests/unit/parts/test_reactive.py
+++ b/tests/unit/parts/test_reactive.py
@@ -31,7 +31,7 @@ from charmcraft.parts import reactive
 pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="Windows not [yet] supported")
 
 
-@pytest.fixture()
+@pytest.fixture
 def charm_exe(tmp_path):
     """Provide a fake charm executable."""
     charm_bin = pathlib.Path(tmp_path, "mock_bin", "charm")
@@ -44,7 +44,7 @@ def charm_exe(tmp_path):
     return charm_bin
 
 
-@pytest.fixture()
+@pytest.fixture
 def broken_charm_exe(tmp_path):
     """Provide a fake charm executable that fails to run."""
     charm_bin = pathlib.Path(tmp_path, "mock_bin", "charm")
@@ -54,7 +54,7 @@ def broken_charm_exe(tmp_path):
     return charm_bin
 
 
-@pytest.fixture()
+@pytest.fixture
 def spec(tmp_path):
     """Provide a common spec to build the different artifacts."""
     return {
@@ -67,12 +67,12 @@ def spec(tmp_path):
     }
 
 
-@pytest.fixture()
+@pytest.fixture
 def plugin_properties(spec):
     return reactive.ReactivePluginProperties.unmarshal(spec)
 
 
-@pytest.fixture()
+@pytest.fixture
 def plugin(tmp_path, plugin_properties, spec):
     project_dirs = craft_parts.ProjectDirs(work_dir=tmp_path)
     part_spec = plugins.extract_part_properties(spec, plugin_name="reactive")
@@ -153,7 +153,7 @@ def test_validate_broken_charm(plugin, plugin_properties, broken_charm_exe):
     assert raised.value.reason == "charm tools failed with error code 2"
 
 
-@pytest.fixture()
+@pytest.fixture
 def build_dir(tmp_path):
     build_dir = tmp_path / const.BUILD_DIRNAME
     build_dir.mkdir()
@@ -161,7 +161,7 @@ def build_dir(tmp_path):
     return build_dir
 
 
-@pytest.fixture()
+@pytest.fixture
 def install_dir(tmp_path):
     install_dir = tmp_path / "install"
     install_dir.mkdir()
@@ -169,7 +169,7 @@ def install_dir(tmp_path):
     return install_dir
 
 
-@pytest.fixture()
+@pytest.fixture
 def fake_run():
     patcher = patch("subprocess.run")
     yield patcher.start()
diff --git a/tests/unit/services/test_analysis.py b/tests/unit/services/test_analysis.py
index ce24461f..d3c3d9eb 100644
--- a/tests/unit/services/test_analysis.py
+++ b/tests/unit/services/test_analysis.py
@@ -83,21 +83,21 @@ LINTER_NAMES = frozenset(linter.name for linter in STUB_LINTERS)
 ALL_CHECKER_NAMES = ATTRIBUTE_CHECKER_NAMES | LINTER_NAMES
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_temp_dir(monkeypatch):
     mock_obj = mock.MagicMock(spec=tempfile.TemporaryDirectory)
     monkeypatch.setattr(tempfile, "TemporaryDirectory", mock.Mock(return_value=mock_obj))
     return mock_obj
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_zip_file(monkeypatch):
     mock_obj = mock.MagicMock(spec=zipfile.ZipFile)
     monkeypatch.setattr(zipfile, "ZipFile", mock.Mock(return_value=mock_obj))
     return mock_obj
 
 
-@pytest.fixture()
+@pytest.fixture
 def analysis_service():
     return analysis.AnalysisService(app=application.APP_METADATA, services=None)
 
diff --git a/tests/unit/services/test_image.py b/tests/unit/services/test_image.py
index 61022d1e..f6fdd014 100644
--- a/tests/unit/services/test_image.py
+++ b/tests/unit/services/test_image.py
@@ -28,18 +28,18 @@ import pytest
 from charmcraft import application, const, services, utils
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_docker() -> mock.Mock:
     return mock.Mock(spec_set=docker.DockerClient)
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_skopeo(fake_process) -> mock.Mock:
     fake_process.register(["/skopeo", "--version"])
     return mock.Mock(wraps=utils.Skopeo(skopeo_path="/skopeo"))
 
 
-@pytest.fixture()
+@pytest.fixture
 def image_service(service_factory, mock_skopeo, mock_docker) -> services.ImageService:
     service = services.ImageService(app=application.APP_METADATA, services=service_factory)
     service._skopeo = mock_skopeo
diff --git a/tests/unit/services/test_lifecycle.py b/tests/unit/services/test_lifecycle.py
new file mode 100644
index 00000000..cb0e0964
--- /dev/null
+++ b/tests/unit/services/test_lifecycle.py
@@ -0,0 +1,79 @@
+# Copyright 2023-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 lifecycle service."""
+
+import pytest
+from craft_application import models, util
+from craft_cli.pytest_plugin import RecordingEmitter
+from craft_providers import bases
+
+from charmcraft.services.lifecycle import LifecycleService
+
+HOST_ARCH = util.get_host_architecture()
+
+
+@pytest.fixture
+def service(service_factory) -> LifecycleService:
+    return service_factory.lifecycle
+
+
+@pytest.mark.parametrize(
+    ("plan_build_for", "expected"),
+    [
+        ("amd64", "amd64"),
+        ("arm64", "arm64"),
+        ("riscv64", "riscv64"),
+        ("all", HOST_ARCH),
+        # Test multi-arch charms
+        (f"{HOST_ARCH}-foreign", "foreign"),
+        (f"foreign-{HOST_ARCH}", "foreign"),
+    ],
+)
+def test_get_build_for_values(service: LifecycleService, plan_build_for: str, expected: str):
+    service._build_plan = [
+        models.BuildInfo(
+            base=bases.BaseName("ubuntu", "22.04"),
+            platform="something",
+            build_on=HOST_ARCH,
+            build_for=plan_build_for,
+        )
+    ]
+
+    assert service._get_build_for() == expected
+
+
+def test_build_for_warns_on_all(service: LifecycleService, emitter: RecordingEmitter):
+    service._build_plan[0].build_for = "all"
+
+    assert service._get_build_for() == HOST_ARCH
+
+    emitter.assert_progress(
+        "WARNING: Charmcraft does not validate that charms with architecture 'all' "
+        "are fully architecture agnostic.",
+        permanent=True,
+    )
+
+
+def test_build_for_warns_on_multi(service: LifecycleService, emitter: RecordingEmitter):
+    service._build_plan[0].build_for = f"{HOST_ARCH}-foreign"
+
+    assert service._get_build_for() == "foreign"
+
+    emitter.assert_progress(
+        "WARNING: Charmcraft does not validate that charms with multiple "
+        "given architectures are architecture agnostic.",
+        permanent=True,
+    )
diff --git a/tests/unit/services/test_package.py b/tests/unit/services/test_package.py
index 85968423..affaa7bf 100644
--- a/tests/unit/services/test_package.py
+++ b/tests/unit/services/test_package.py
@@ -43,7 +43,7 @@ MANIFEST_WITH_ATTRIBUTE = models.Manifest(
 )
 
 
-@pytest.fixture()
+@pytest.fixture
 def package_service(fake_path, simple_charm, service_factory, default_build_plan):
     fake_project_dir = fake_path / "project"
     fake_project_dir.mkdir(parents=True)
diff --git a/tests/unit/services/test_provider.py b/tests/unit/services/test_provider.py
index ff358323..b55f6c48 100644
--- a/tests/unit/services/test_provider.py
+++ b/tests/unit/services/test_provider.py
@@ -23,7 +23,7 @@ from craft_providers import bases
 from charmcraft import models, services
 
 
-@pytest.fixture()
+@pytest.fixture
 def provider_service(
     fake_path: pathlib.Path,
     service_factory: services.CharmcraftServiceFactory,
diff --git a/tests/unit/services/test_store.py b/tests/unit/services/test_store.py
index 53d2daf7..8407d6ce 100644
--- a/tests/unit/services/test_store.py
+++ b/tests/unit/services/test_store.py
@@ -32,7 +32,7 @@ from charmcraft.store import client
 from tests import get_fake_revision
 
 
-@pytest.fixture()
+@pytest.fixture
 def store(service_factory) -> services.StoreService:
     store = services.StoreService(app=application.APP_METADATA, services=service_factory)
     store.client = mock.Mock(spec_set=client.Client)
diff --git a/tests/unit/store/test_client.py b/tests/unit/store/test_client.py
index ccc9df1a..e739272e 100644
--- a/tests/unit/store/test_client.py
+++ b/tests/unit/store/test_client.py
@@ -22,12 +22,12 @@ import pytest
 from charmcraft import store
 
 
-@pytest.fixture()
+@pytest.fixture
 def client() -> store.Client:
     return store.Client(api_base_url="http://charmhub.local")
 
 
-@pytest.fixture()
+@pytest.fixture
 def anonymous_client() -> store.AnonymousClient:
     return store.AnonymousClient("http://charmhub.local", "http://storage.charmhub.local")
 
diff --git a/tests/unit/test_application.py b/tests/unit/test_application.py
index ae3b8f8b..c8cd2865 100644
--- a/tests/unit/test_application.py
+++ b/tests/unit/test_application.py
@@ -15,9 +15,13 @@
 # For further info, check https://github.com/canonical/charmcraft
 """Unit tests for application class."""
 import textwrap
+from unittest import mock
 
+import craft_application
+import craft_cli.pytest_plugin
 import pyfakefs.fake_filesystem
 import pytest
+from craft_application import util
 
 from charmcraft import application, errors, services
 from charmcraft.application.main import PRIME_BEHAVIOUR_CHANGE_MESSAGE
@@ -264,3 +268,31 @@ def test_deprecated_prime_warning_not_raised_in_managed_mode(
     app = application.Charmcraft(app=application.APP_METADATA, services=service_factory)
 
     app._extra_yaml_transform(charm_yaml, build_for=None, build_on="riscv64")
+
+
+@pytest.mark.parametrize(
+    "build_for",
+    [
+        "amd64-arm64",
+        "s390x-ppc64el-riscv64",
+    ],
+)
+def test_expand_environment_multi_arch(
+    monkeypatch: pytest.MonkeyPatch,
+    emitter: craft_cli.pytest_plugin.RecordingEmitter,
+    service_factory: services.CharmcraftServiceFactory,
+    build_for,
+) -> None:
+    mock_parent_expand_environment = mock.Mock()
+    monkeypatch.setattr(
+        craft_application.Application, "_expand_environment", mock_parent_expand_environment
+    )
+    app = application.Charmcraft(app=application.APP_METADATA, services=service_factory)
+
+    app._expand_environment({}, build_for)
+
+    emitter.assert_debug(
+        "Expanding environment variables with the host architecture "
+        f"{util.get_host_architecture()!r} as the build-for architecture "
+        "because multiple run-on architectures were specified."
+    )
diff --git a/tests/unit/test_charm_builder.py b/tests/unit/test_charm_builder.py
index 9ab99664..24fe5d30 100644
--- a/tests/unit/test_charm_builder.py
+++ b/tests/unit/test_charm_builder.py
@@ -30,7 +30,7 @@ pytestmark = [
 REQUIREMENTS_FILES = [pytest.param("", id="empty"), "ops~=2.5", "requests\nops"]
 
 
-@pytest.fixture()
+@pytest.fixture
 def builder(fs: FakeFilesystem) -> charm_builder.CharmBuilder:
     fs.cwd = "/root"
     fs.makedirs(const.BUILD_DIRNAME)
diff --git a/tests/unit/utils/test_cli.py b/tests/unit/utils/test_cli.py
index 83aba4b0..971f3679 100644
--- a/tests/unit/utils/test_cli.py
+++ b/tests/unit/utils/test_cli.py
@@ -47,19 +47,19 @@ COMPOUND_DICTS_STRATEGY = strategies.dictionaries(
 )
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_isatty():
     with patch("sys.stdin.isatty", return_value=True) as mock_isatty:
         yield mock_isatty
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_input():
     with patch("charmcraft.utils.cli.input", return_value="") as mock_input:
         yield mock_input
 
 
-@pytest.fixture()
+@pytest.fixture
 def mock_is_charmcraft_running_in_managed_mode():
     with patch(
         "charmcraft.utils.cli.is_charmcraft_running_in_managed_mode", return_value=False
diff --git a/tests/unit/utils/test_skopeo.py b/tests/unit/utils/test_skopeo.py
index af2c6668..45ce80c9 100644
--- a/tests/unit/utils/test_skopeo.py
+++ b/tests/unit/utils/test_skopeo.py
@@ -87,7 +87,7 @@ def test_get_global_command(fake_process, kwargs, expected):
     assert skopeo.get_global_command() == ["/skopeo", *expected]
 
 
-@pytest.fixture()
+@pytest.fixture
 def fake_skopeo(fake_process):
     fake_process.register(["/skopeo", "--version"])
     return Skopeo(skopeo_path="/skopeo")