Repository: canonical/charmcraft

Issue:
Unable to deploy charm using multi-arch OCI uploaded with charmcraft 3
### Bug Description

Uploading a multi-arch OCI with charmcraft 3 (from edge) `upload-resource` causes the charm to not be deployable on Juju on amd64 or arm64

### To Reproduce

1. On amd64 machine, upload resource (we had to create a new hash for `charmed-mysql` since uploading a hash already in Charmhub uses the existing upload)
```
$ charmcraft upload-resource --format json mysql-router-k8s mysql-router-image --image docker://ghcr.io/canonical/charmed-mysql@sha256:458dbc8d0759136a57882a28d83e90132c109f9ca3fb75761ce593d8217b6054
{tatus checked: {'revisions': [{'errors': None, 'revision': 59, 'status': 'approved', 'upload-id': 'cf949687-4ce3-4647-b693-1857792cfc3c'}]}
    "revision": 59
}
```
2. Release charm
```
$ charmcraft release mysql-router-k8s --revision 543 --channel 8.0/edge/test-cc-poetry-plugin --resource mysql-router-image:59
Revision 543 of 'mysql-router-k8s' released to 8.0/edge/test-cc-poetry-plugin (attaching resources: 'mysql-router-image' r59)
```
3. On arm64 machine, deploy charm
```
$ juju deploy mysql-router-k8s --channel 8.0/edge/test-cc-poetry-plugin -n 3 --base ubuntu@22.04
Deployed "mysql-router-k8s" from charm-hub charm "mysql-router-k8s", revision 543 in channel 8.0/edge/test-cc-poetry-plugin on ubuntu@22.04/stable
```
4. Juju status shows
```
Unit                 Workload  Agent  Address      Ports  Message
mysql-router-k8s/0   error     idle   10.1.215.20         unknown container reason "ImagePullBackOff": Back-off pulling image "registry.jujucharms.com/charm/g78qli3013qicvevb3...
mysql-router-k8s/1*  error     idle   10.1.215.22         unknown container reason "ImagePullBackOff": Back-off pulling image "registry.jujucharms.com/charm/g78qli3013qicvevb3...
mysql-router-k8s/2   error     idle   10.1.215.21         unknown container reason "ImagePullBackOff": Back-off pulling image "registry.jujucharms.com/charm/g78qli3013qicvevb3...
```

```
$ kubectl -n foo3 describe pod mysql-router-k8s-0
[...]
Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  86s                default-scheduler  Successfully assigned foo3/mysql-router-k8s-0 to test2
  Normal   Pulled     86s                kubelet            Container image "docker.io/jujusolutions/jujud-operator:3.6.0" already present on machine
  Normal   Created    86s                kubelet            Created container charm-init
  Normal   Started    86s                kubelet            Started container charm-init
  Normal   Pulled     85s                kubelet            Container image "docker.io/jujusolutions/charm-base:ubuntu-22.04" already present on machine
  Normal   Created    85s                kubelet            Created container charm
  Normal   Started    85s                kubelet            Started container charm
  Normal   BackOff    55s (x5 over 83s)  kubelet            Back-off pulling image "registry.jujucharms.com/charm/g78qli3013qicvevb3oj4z8u0zhjod1agws2d/mysql-router-image@sha256:458dbc8d0759136a57882a28d83e90132c109f9ca3fb75761ce593d8217b6054"
  Warning  Failed     55s (x5 over 83s)  kubelet            Error: ImagePullBackOff
  Normal   Pulling    43s (x3 over 85s)  kubelet            Pulling image "registry.jujucharms.com/charm/g78qli3013qicvevb3oj4z8u0zhjod1agws2d/mysql-router-image@sha256:458dbc8d0759136a57882a28d83e90132c109f9ca3fb75761ce593d8217b6054"
  Warning  Failed     42s (x3 over 83s)  kubelet            Failed to pull image "registry.jujucharms.com/charm/g78qli3013qicvevb3oj4z8u0zhjod1agws2d/mysql-router-image@sha256:458dbc8d0759136a57882a28d83e90132c109f9ca3fb75761ce593d8217b6054": rpc error: code = NotFound desc = failed to pull and unpack image "registry.jujucharms.com/charm/g78qli3013qicvevb3oj4z8u0zhjod1agws2d/mysql-router-image@sha256:458dbc8d0759136a57882a28d83e90132c109f9ca3fb75761ce593d8217b6054": failed to resolve reference "registry.jujucharms.com/charm/g78qli3013qicvevb3oj4z8u0zhjod1agws2d/mysql-router-image@sha256:458dbc8d0759136a57882a28d83e90132c109f9ca3fb75761ce593d8217b6054": registry.jujucharms.com/charm/g78qli3013qicvevb3oj4z8u0zhjod1agws2d/mysql-router-image@sha256:458dbc8d0759136a57882a28d83e90132c109f9ca3fb75761ce593d8217b6054: not found
  Warning  Failed     42s (x3 over 83s)  kubelet            Error: ErrImagePull
```

### Environment

Ubuntu 22.04

on amd64 machine
```
$ snap list
Name              Version        Rev    Tracking            Publisher          Notes
charmcraft        0.0.0          x1     -                   -                  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.0          29130  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.0          13008  latest/stable       canonical✓         classic
snapd             2.66.1         23258  latest/stable       canonical✓         snapd
```

installed charmcraft is https://github.com/canonical/charmcraft/tree/9ff19c328e23b50cc06f04e8a5ad4835740badf4 plus
```
$ git diff
diff --git a/charmcraft/dispatch.py b/charmcraft/dispatch.py
index cd02457b..77df77d7 100644
--- a/charmcraft/dispatch.py
+++ b/charmcraft/dispatch.py
@@ -27,7 +27,7 @@ dispatch_path="$(dirname $(realpath $0))"
 venv_bin_path="${{dispatch_path}}/venv/bin"
 python_path="${{venv_bin_path}}/python"
 if [ ! -e "${{python_path}}" ]; then
-    mkdir -p "{{venv_bin_path}}"
+    mkdir -p "${{venv_bin_path}}"
     ln -s $(which python3) "${{python_path}}"
 fi
 
diff --git a/requirements.txt b/requirements.txt
index dc5b6a33..15b11368 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,7 +10,7 @@ craft-application==4.4.0
 craft-archives==2.0.1
 craft-cli==2.10.1
 craft-grammar==2.0.1
-craft-parts==2.1.3
+craft-parts==2.1.4
 craft-platforms==0.4.0
 craft-providers==2.0.4
 craft-store==3.0.2
```

on arm64 machine where charm built (same git diff to charmcraft as above)
```
$ snap list
Name        Version                 Rev    Tracking            Publisher   Notes
charmcraft  3.2.2.post82+g9ff19c32  x1     -                   -           classic
core20      20240911                2437   latest/stable       canonical✓  base
core22      20241119                1720   latest/stable       canonical✓  base
core24      20240920                612    latest/stable       canonical✓  base
juju        3.6.0                   29131  3/stable            canonical✓  -
lxd         5.21.2-084c8c8          31216  5.21/stable/…       canonical✓  -
microk8s    v1.29.11                7437   1.29-strict/stable  canonical✓  -
snapcraft   8.5.0                   13016  latest/stable       canonical✓  classic
snapd       2.66.1                  23259  latest/stable       canonical✓  snapd
```

on arm64 machine where charm deployed
```
$ snap list
Name        Version        Rev    Tracking            Publisher   Notes
charmcraft  3.2.2          5306   latest/stable       canonical✓  classic
core20      20240911       2437   latest/stable       canonical✓  base
core22      20241119       1720   latest/stable       canonical✓  base
core24      20240920       612    latest/stable       canonical✓  base
juju        3.6.0          29131  3/stable            canonical✓  -
lxd         5.0.4-497fe1e  31335  5.0/stable/…        canonical✓  -
microk8s    v1.29.11       7437   1.29-strict/stable  canonical✓  -
snapd       2.66.1         23259  latest/stable       canonical✓  snapd
```

### charmcraft.yaml

```yaml
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.

type: charm
base: ubuntu@22.04
platforms:
  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
      curl -sSL https://install.python-poetry.org | python3 - --version 1.8.4
      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-with:
      - charm-libs
    build-snaps:
      - rustup
    build-packages:
      - libffi-dev
      - libssl-dev
      - pkg-config
    override-build: |
      rustup default stable
      craftctl default
  # "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
Normal   Pulling    43s (x3 over 85s)  kubelet            Pulling image "registry.jujucharms.com/charm/g78qli3013qicvevb3oj4z8u0zhjod1agws2d/mysql-router-image@sha256:458dbc8d0759136a57882a28d83e90132c109f9ca3fb75761ce593d8217b6054"
  Warning  Failed     42s (x3 over 83s)  kubelet            Failed to pull image "registry.jujucharms.com/charm/g78qli3013qicvevb3oj4z8u0zhjod1agws2d/mysql-router-image@sha256:458dbc8d0759136a57882a28d83e90132c109f9ca3fb75761ce593d8217b6054": rpc error: code = NotFound desc = failed to pull and unpack image "registry.jujucharms.com/charm/g78qli3013qicvevb3oj4z8u0zhjod1agws2d/mysql-router-image@sha256:458dbc8d0759136a57882a28d83e90132c109f9ca3fb75761ce593d8217b6054": failed to resolve reference "registry.jujucharms.com/charm/g78qli3013qicvevb3oj4z8u0zhjod1agws2d/mysql-router-image@sha256:458dbc8d0759136a57882a28d83e90132c109f9ca3fb75761ce593d8217b6054": registry.jujucharms.com/charm/g78qli3013qicvevb3oj4z8u0zhjod1agws2d/mysql-router-image@sha256:458dbc8d0759136a57882a28d83e90132c109f9ca3fb75761ce593d8217b6054: not found
  Warning  Failed     42s (x3 over 83s)  kubelet            Error: ErrImagePull
```


Discussion:
lengau: @carlcsaposs-canonical - if I'm very lucky, this might be all we need for #2020. I don't have a good way to test it right now, so if you have time I'd appreciate you installing Charmcraft from [the build artefact](https://github.com/canonical/charmcraft/actions/runs/12299291502/artifacts/2312387183) and trying it.

If it doesn't work, can we meet some time tomorrow or Monday to help me get the infrastructure I need to properly replicate this?

Patch:
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 00000000..efbe1996
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,12 @@
+# GitHub CODEOWNERS. See:
+# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
+# Remember, the last match takes precedence.
+
+# Global owner, used as a last resort.
+*       @lengau
+
+# Documentation owners
+/docs/  @medubelko @lengau
+
+# Finally, all CODEOWNERS changes need to be approved by The Man, Himself.
+/.github/CODEOWNERS     @sergiusens
diff --git a/.gitignore b/.gitignore
index 3ac2a33d..48591a5c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,6 +74,7 @@ docs/_build/
 # Auto-generated command reference
 /docs/reference/commands/
 /docs/reference/_autosummary/
+/docs/common/craft-parts
 
 # PyBuilder
 target/
diff --git a/charmcraft/parts/plugins/_charm.py b/charmcraft/parts/plugins/_charm.py
index a93995b5..2e8bdbd1 100644
--- a/charmcraft/parts/plugins/_charm.py
+++ b/charmcraft/parts/plugins/_charm.py
@@ -193,6 +193,7 @@ class CharmPlugin(plugins.Plugin):
                 "python3-setuptools",
                 "python3-venv",
                 "python3-wheel",
+                "libffi-dev",
                 "libyaml-dev",
             }
         elif platform.is_yum_based():
diff --git a/charmcraft/parts/plugins/_poetry.py b/charmcraft/parts/plugins/_poetry.py
index 5af1d1f2..b0c5b5cd 100644
--- a/charmcraft/parts/plugins/_poetry.py
+++ b/charmcraft/parts/plugins/_poetry.py
@@ -16,6 +16,7 @@
 """Charmcraft-specific poetry plugin."""
 
 import pathlib
+import shlex
 from pathlib import Path
 
 from craft_parts.plugins import poetry_plugin
@@ -55,10 +56,11 @@ class PoetryPlugin(poetry_plugin.PoetryPlugin):
         :returns: A list of strings forming the install script.
         """
         pip = self._get_pip()
+        pip_extra_args = shlex.join(self._options.poetry_pip_extra_args)
         return [
             # These steps need to be separate because poetry export defaults to including
             # hashes, which don't work with installing from a directory.
-            f"{pip} install --no-deps '--requirement={requirements_path}'",
+            f"{pip} install --no-deps --no-binary=:all: {pip_extra_args} '--requirement={requirements_path}'",
             # Check that the virtualenv is consistent.
             f"{pip} check",
         ]
diff --git a/charmcraft/parts/plugins/_python.py b/charmcraft/parts/plugins/_python.py
index 99925008..1123dd4e 100644
--- a/charmcraft/parts/plugins/_python.py
+++ b/charmcraft/parts/plugins/_python.py
@@ -38,7 +38,9 @@ class PythonPlugin(python_plugin.PythonPlugin):
 
     @override
     def get_build_environment(self) -> dict[str, str]:
-        return utils.extend_python_build_environment(super().get_build_environment())
+        return {
+            "PIP_NO_BINARY": ":all:",
+        } | utils.extend_python_build_environment(super().get_build_environment())
 
     @override
     def _get_venv_directory(self) -> Path:
diff --git a/charmcraft/services/image.py b/charmcraft/services/image.py
index cc2827b0..25626681 100644
--- a/charmcraft/services/image.py
+++ b/charmcraft/services/image.py
@@ -98,6 +98,8 @@ class ImageService(craft_application.AppService):
             dest_username=dest_username,
             dest_password=dest_password,
             preserve_digests=True,
+            # all_images allows uploading multi-arch OCI images.
+            all_images=True,
         )
 
     @staticmethod
diff --git a/charmcraft/services/provider.py b/charmcraft/services/provider.py
index 704a98f6..27ec9d09 100644
--- a/charmcraft/services/provider.py
+++ b/charmcraft/services/provider.py
@@ -124,6 +124,13 @@ class ProviderService(services.ProviderService):
             **kwargs,  # type: ignore[arg-type]
         ) as instance:
             try:
+                # Use /root/.cache even if we're in the snap.
+                instance.execute_run(
+                    ["rm", "-rf", "/root/snap/charmcraft/common/cache"]
+                )
+                instance.execute_run(
+                    ["ln", "-s", "/root/.cache", "/root/snap/charmcraft/common/cache"]
+                )
                 yield instance
             finally:
                 if fcntl is not None and self._lock:
diff --git a/charmcraft/utils/parts.py b/charmcraft/utils/parts.py
index 2c994621..9ed7d9ce 100644
--- a/charmcraft/utils/parts.py
+++ b/charmcraft/utils/parts.py
@@ -28,7 +28,6 @@ def extend_python_build_environment(environment: dict[str, str]) -> dict[str, st
     :returns: the environment dictionary with charmcraft-specific additions.
     """
     return environment | {
-        "PIP_NO_BINARY": ":all:",  # Build from source
         "PARTS_PYTHON_VENV_ARGS": "--without-pip",
     }
 
@@ -66,11 +65,25 @@ def get_venv_cleanup_commands(venv_path: pathlib.Path, *, keep_bins: bool) -> li
     """
     venv_bin = venv_path / "bin"
     venv_lib64 = venv_path / "lib64"
-    delete_bins = [] if keep_bins else [f"rm -rf {venv_bin}"]
+    if keep_bins:
+        delete_bins = []
+    else:
+        delete_bins = [
+            # Remove all files in venv_bin except `activate`
+            "shopt -s extglob",
+            f"rm -rf {venv_bin}/!(activate)",
+            "shopt -u extglob",
+        ]
+    update_activate = [
+        # Replace hard-coded path in `activate` with portable path
+        # "\&" is escape for sed
+        'sed -i \'s#^VIRTUAL_ENV=.*$#VIRTUAL_ENV="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." \\&> /dev/null \\&\\& pwd )"#\' '
+        + str(venv_bin / "activate"),
+    ]
     delete_lib64 = textwrap.dedent(f"""
         if [ -L '{venv_lib64}' ]; then
           rm -f '{venv_lib64}'
         fi
     """)
 
-    return [*delete_bins, delete_lib64]
+    return [*delete_bins, *update_activate, delete_lib64]
diff --git a/common.mk b/common.mk
index 29259d08..4f5c9ef4 100644
--- a/common.mk
+++ b/common.mk
@@ -154,7 +154,7 @@ lint-docs:  ##- Lint the documentation
 ifneq ($(CI),)
 	@echo ::group::$@
 endif
-	uv run --extra docs sphinx-lint --max-line-length 88 --enable all $(DOCS)
+	uv run --extra docs sphinx-lint --max-line-length 88 --ignore docs/reference/commands --enable all $(DOCS)
 ifneq ($(CI),)
 	@echo ::endgroup::
 endif
@@ -190,11 +190,11 @@ test-coverage:  ## Generate coverage report
 
 .PHONY: docs
 docs:  ## Build documentation
-	uv run --extra docs sphinx-build -b html -W $(DOCS) $(DOCS)/_build
+	uv run --extra docs sphinx-build -b html $(DOCS) $(DOCS)/_build
 
 .PHONY: docs-auto
 docs-auto:  ## Build and host docs with sphinx-autobuild
-	uv run --extra docs sphinx-autobuild -b html --open-browser --port=8080 --watch $(PROJECT) -W $(DOCS) $(DOCS)/_build
+	uv run --extra docs sphinx-autobuild -b html --open-browser --port=8080 --ignore *.kate-swp --ignore docs/reference/commands/** --watch $(PROJECT) $(DOCS) $(DOCS)/_build
 
 .PHONY: pack-pip
 pack-pip:  ##- Build packages for pip (sdist, wheel)
diff --git a/docs/reference/changelog.rst b/docs/reference/changelog.rst
index 12cac5a8..e4f08895 100644
--- a/docs/reference/changelog.rst
+++ b/docs/reference/changelog.rst
@@ -77,14 +77,52 @@ Changelog
 
   For a complete list of commits, see the `X.Y.Z`_ release on GitHub.
 
-X.Y.Z (2024-MM-DD)
+3.3.0 (2025-01-13)
 ------------------
 
+Core
+====
+
+Plugins
+#######
+
+- New :ref:`craft_parts_poetry_plugin`.
+- New :ref:`craft_parts_python_plugin`.
+
+Reactive
+""""""""
+
+Extensions
+##########
+
+- New `fastapi-framework extension
+  <https://juju.is/docs/sdk/charmcraft-extension-fastapi-framework>`_
+- The Django extension is no longer considered experimental.
+
+- The ``charm`` command is now run in verbose mode by default.
+
 Command line
 ============
 
-The pack command now updates charm the libs in the project directory if they don't meet
-the requirements in the ``charm-libs`` key of ``charmcraft.yaml``.
+- The ``pack`` command now updates the charm libaries in the project directory
+  if they don't meet the requirements in the ``charm-libs`` key of
+  ``charmcraft.yaml``.
+- New :ref:`ref_commands_create-track` command.
+
+Linter
+======
+
+- New linter to check that the entrypoint contains a call to ``ops.main()``
+
+Documentation
+=============
+
+How-to guides for migrating to the new plugins:
+
+- :ref:`howto-migrate-to-poetry`
+- :ref:`howto-migrate-to-python`
+
+For a complete list of commits, see the `3.3.0`_ release on GitHub.
 
 3.2.2 (2024-10-16)
 ------------------
@@ -223,7 +261,7 @@ New Features
   Charmcraft 3.1. Please have a go with it. Documentation is fairly minimal
   right now, as the implementation is still in flux.
 * The :ref:`ref_commands_upload-resource` command now uses
-  `skopeo <https://github.com/containers/skopeo>`_ to upload images. Most notably,
+  `skopeo`_ to upload images. Most notably,
   this means you can enter
   `any skopeo-supported containers-transports URL
   <https://manpages.ubuntu.com/manpages/noble/man5/containers-transports.5.html>`_
@@ -376,3 +414,4 @@ page.
 .. _3.2.0: https://github.com/canonical/charmcraft/releases/tag/3.2.0
 .. _3.2.1: https://github.com/canonical/charmcraft/releases/tag/3.2.1
 .. _3.2.2: https://github.com/canonical/charmcraft/releases/tag/3.2.2
+.. _3.3.0: https://github.com/canonical/charmcraft/releases/tag/3.3.0
diff --git a/docs/reference/plugins/python_plugin.rst b/docs/reference/plugins/python_plugin.rst
index 3b9c8a25..f85f4037 100644
--- a/docs/reference/plugins/python_plugin.rst
+++ b/docs/reference/plugins/python_plugin.rst
@@ -46,7 +46,7 @@ During the build step, the plugin performs the following actions:
 2. It uses :command:`pip` to install the required Python packages specified
    by the ``python-requirements``, ``python-constraints`` and ``python-packages``
    keys.
-4. It copies any existing ``src`` and ``lib`` directories from your charm project into
+3. It copies any existing ``src`` and ``lib`` directories from your charm project into
    the final charm.
 
 Example
@@ -57,5 +57,3 @@ to build a charm for Ubuntu 24.04:
 
 .. literalinclude:: python-charmcraft.yaml
    :language: yaml
-
-
diff --git a/pyproject.toml b/pyproject.toml
index 7b28b2f0..4ee79fc8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ dependencies = [
     "craft-application~=4.2",
     "craft-cli>=2.3.0",
     "craft-grammar>=2.0.0",
-    "craft-parts>=2.1.0",
+    "craft-parts>=2.2.0",
     "craft-providers>=2.0.0",
     "craft-platforms~=0.3",
     "craft-providers>=2.0.0",
diff --git a/requirements-dev.txt b/requirements-dev.txt
index abe8addb..bba148bb 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -12,7 +12,7 @@ craft-application==4.4.0
 craft-archives==2.0.1
 craft-cli==2.10.1
 craft-grammar==2.0.1
-craft-parts==2.1.4
+craft-parts==2.2.0
 craft-platforms==0.4.0
 craft-providers==2.0.4
 craft-store==3.1.0
diff --git a/uv.lock b/uv.lock
index d07411a2..a282f905 100644
--- a/uv.lock
+++ b/uv.lock
@@ -351,7 +351,7 @@ wheels = [
 
 [[package]]
 name = "charmcraft"
-version = "3.2.2.post118+gf98c8013"
+version = "3.2.2.post113+g28880d8c"
 source = { editable = "." }
 dependencies = [
     { name = "craft-application" },
@@ -454,10 +454,10 @@ requires-dist = [
     { name = "craft-application", specifier = "~=4.2" },
     { name = "craft-cli", specifier = ">=2.3.0" },
     { name = "craft-grammar", specifier = ">=2.0.0" },
-    { name = "craft-parts", specifier = ">=2.1.0" },
+    { name = "craft-parts", specifier = ">=2.2.0" },
     { name = "craft-platforms", specifier = "~=0.3" },
     { name = "craft-providers", specifier = ">=2.0.0" },
-    { name = "craft-store", git = "https://github.com/canonical/craft-store?rev=work%2Fcharmcraft-1901" },
+    { name = "craft-store", specifier = ">=3.1.0" },
     { name = "distro", specifier = ">=1.7.0" },
     { name = "docker", specifier = ">=7.0.0" },
     { name = "freezegun", marker = "extra == 'dev'" },
@@ -839,7 +839,7 @@ wheels = [
 
 [[package]]
 name = "craft-parts"
-version = "2.1.4"
+version = "2.2.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "overrides" },
@@ -849,9 +849,9 @@ dependencies = [
     { name = "requests" },
     { name = "requests-unixsocket2" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/d3/93/bf658a4d73c17bfe8b26055ebc3d9aa06c090d3a94b9ed58ff595d3530d2/craft_parts-2.1.4.tar.gz", hash = "sha256:953e5a9e89055e80826d0d81d169a725e5105ecb68dbf4a6a3816e35e8900336", size = 341297 }
+sdist = { url = "https://files.pythonhosted.org/packages/e6/d8/47a791e7a6403a1824f2031f24e61f4e29ea196a50cd6a276bd1f5f0f452/craft_parts-2.2.0.tar.gz", hash = "sha256:4cc8dc3a79350fa7e96c0e0135001a1648cf8e2a9c236725a346236e62e82195", size = 564972 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/45/e7/7f93ed5000a9cc8a72f03d92a2dcd137c336a77c0c5a17808a18af54767e/craft_parts-2.1.4-py3-none-any.whl", hash = "sha256:4a26bb0fc6abddbacced13827e02d037390450a6395366084d56938a67997806", size = 435192 },
+    { url = "https://files.pythonhosted.org/packages/74/da/66ab5c19c971d6062d200c8e384d25f5ae69c718459d144fea62a8b1ce44/craft_parts-2.2.0-py3-none-any.whl", hash = "sha256:bf3a779c9c2db0a4a72907a99bd603716b31b275f822c467ec05fe4aaff565ab", size = 449019 },
 ]
 
 [[package]]
@@ -886,8 +886,8 @@ wheels = [
 
 [[package]]
 name = "craft-store"
-version = "3.0.2.post25+g63657b2"
-source = { git = "https://github.com/canonical/craft-store?rev=work%2Fcharmcraft-1901#63657b29d89de3bf890a3cc2b03154e4b8b1ffc7" }
+version = "3.1.0"
+source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "annotated-types" },
     { name = "httpx" },
@@ -901,6 +901,10 @@ dependencies = [
     { name = "requests-toolbelt" },
     { name = "typing-extensions" },
 ]
+sdist = { url = "https://files.pythonhosted.org/packages/1c/61/fcf3ba6904609d859e1fd6645de03c046d59d004a127a543e3fcd9d710d6/craft_store-3.1.0.tar.gz", hash = "sha256:aaa5a88e4ae0da036ab0b630a90db25907f6cc22f2824f3a75c6a3498c00effb", size = 196772 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a8/dc/9045bc95138ef57524ee348aac58882f3f206f3eb8bbee383a2f50e978b6/craft_store-3.1.0-py3-none-any.whl", hash = "sha256:3c712a49565f8b6d67327b5443059ab696d89e2807500c04bc6cf5358b3b64b0", size = 49172 },
+]
 
 [[package]]
 name = "crashtest"


Test patch:
diff --git a/tests/unit/parts/plugins/test_charm.py b/tests/unit/parts/plugins/test_charm.py
index 77c47e8f..cc27975d 100644
--- a/tests/unit/parts/plugins/test_charm.py
+++ b/tests/unit/parts/plugins/test_charm.py
@@ -38,6 +38,7 @@ def test_charmplugin_get_build_package_deb_based(charm_plugin):
             "python3-wheel",
             "python3-venv",
             "python3-dev",
+            "libffi-dev",
             "libyaml-dev",
         }
 
diff --git a/tests/unit/parts/plugins/test_poetry.py b/tests/unit/parts/plugins/test_poetry.py
index d9ac2c82..8bdb16d7 100644
--- a/tests/unit/parts/plugins/test_poetry.py
+++ b/tests/unit/parts/plugins/test_poetry.py
@@ -33,7 +33,7 @@ def test_get_build_environment(
 ):
     env = poetry_plugin.get_build_environment()
 
-    assert env["PIP_NO_BINARY"] == ":all:"
+    assert env["PARTS_PYTHON_VENV_ARGS"] == "--without-pip"
 
 
 def test_get_venv_directory(
@@ -48,7 +48,7 @@ def test_get_pip_install_commands(poetry_plugin: plugins.PoetryPlugin):
     assert poetry_plugin._get_pip_install_commands(
         pathlib.Path("/my dir/reqs.txt")
     ) == [
-        "/python -m pip install --no-deps '--requirement=/my dir/reqs.txt'",
+        "/python -m pip install --no-deps --no-binary=:all:  '--requirement=/my dir/reqs.txt'",
         "/python -m pip check",
     ]
 
@@ -97,7 +97,10 @@ def test_get_package_install_commands(
 def test_get_rm_command(
     poetry_plugin: plugins.PoetryPlugin, install_path: pathlib.Path
 ):
-    assert f"rm -rf {install_path / 'venv/bin'}" in poetry_plugin.get_build_commands()
+    assert (
+        f"rm -rf {install_path / 'venv/bin'}/!(activate)"
+        in poetry_plugin.get_build_commands()
+    )
 
 
 def test_no_get_rm_command(
@@ -110,5 +113,6 @@ def test_no_get_rm_command(
     }
     poetry_plugin._options = plugins.PoetryPluginProperties.unmarshal(spec)
     assert (
-        f"rm -rf {install_path / 'venv/bin'}" not in poetry_plugin.get_build_commands()
+        f"rm -rf {install_path / 'venv/bin'}/!(activate)"
+        not in poetry_plugin.get_build_commands()
     )
diff --git a/tests/unit/parts/plugins/test_python.py b/tests/unit/parts/plugins/test_python.py
index 5010e4dd..60067597 100644
--- a/tests/unit/parts/plugins/test_python.py
+++ b/tests/unit/parts/plugins/test_python.py
@@ -106,7 +106,10 @@ def test_get_package_install_commands(
 def test_get_rm_command(
     python_plugin: plugins.PythonPlugin, install_path: pathlib.Path
 ):
-    assert f"rm -rf {install_path / 'venv/bin'}" in python_plugin.get_build_commands()
+    assert (
+        f"rm -rf {install_path / 'venv/bin'}/!(activate)"
+        in python_plugin.get_build_commands()
+    )
 
 
 def test_no_get_rm_command(
@@ -119,5 +122,6 @@ def test_no_get_rm_command(
     }
     python_plugin._options = plugins.PythonPluginProperties.unmarshal(spec)
     assert (
-        f"rm -rf {install_path / 'venv/bin'}" not in python_plugin.get_build_commands()
+        f"rm -rf {install_path / 'venv/bin'}/!(activate)"
+        not in python_plugin.get_build_commands()
     )