Repository: canonical/charmcraft

Issue:
Poetry plugin: use parameters rather than env vars
### What needs to get done

The Charmcraft version of the poetry plugin uses environment variables to set pip options rather than parameters. With https://github.com/canonical/craft-parts/issues/932 it makes better sense to put this in parameters. This needs to be done before the Charmcraft 3.3 release since it breaks those plugins

### Why it needs to get done

Requested by data platform team

Discussion:

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/_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/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_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()
     )