Repository: canonical/charmcraft

Issue:
Strict dependencies prevents charm from building
### Bug Description

`ModuleNotFoundError: No module named 'markupsafe'` when building charm with `charm-strict-dependencies: true`

### To Reproduce

```
git clone https://github.com/canonical/mysql-router-operator
git checkout with-hashes
tox run -e pack-wrapper
mv requirements-last-build.txt requirements.txt
charmcraft pack
```

### Environment

Ubuntu 22.04

### charmcraft.yaml

```shell
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.

type: charm
bases:
  # Whenever "bases" is changed:
  # - Update tests/integration/conftest.py::pytest_configure()
  # - Update .github/workflow/ci.yaml integration-test matrix
  - build-on:
      - name: "ubuntu"
        channel: "20.04"
    run-on:
      - name: "ubuntu"
        channel: "20.04"
  - build-on:
      - name: "ubuntu"
        channel: "22.04"
    run-on:
      - name: "ubuntu"
        channel: "22.04"
parts:
  charm:
    override-pull: |
      craftctl default
      if [[ ! -f requirements.txt ]]
      then
          echo 'ERROR: Use "tox run -e build" instead of calling "charmcraft pack" directly' >&2
          exit 1
      fi
    charm-strict-dependencies: true
    charm-entrypoint: src/machine_charm.py
```


### Relevant log output
[charmcraft-20240103-154031.230848.log](https://github.com/canonical/charmcraft/files/13820882/charmcraft-20240103-154031.230848.log)



Discussion:
lengau: Closing and reopening because GitHub never actually started running the CI.

Patch:
diff --git a/charmcraft/charm_builder.py b/charmcraft/charm_builder.py
index 9bd53778..e7ac4434 100644
--- a/charmcraft/charm_builder.py
+++ b/charmcraft/charm_builder.py
@@ -247,7 +247,9 @@ class CharmBuilder:
             # common charm dependencies (e.g. ops). Resolve this by updating to a
             # known working version of pip.
             if get_pip_version(pip_cmd) < MINIMUM_PIP_VERSION:
-                _process_run([pip_cmd, "install", f"pip@{KNOWN_GOOD_PIP_URL}"])
+                _process_run(
+                    [pip_cmd, "install", "--force-reinstall", f"pip@{KNOWN_GOOD_PIP_URL}"]
+                )
 
         with instrum.Timer("Installing all dependencies"):
             if self.strict_dependencies:


Test patch:
diff --git a/tests/test_charm_builder.py b/tests/test_charm_builder.py
index 343cb8e9..e5c1986e 100644
--- a/tests/test_charm_builder.py
+++ b/tests/test_charm_builder.py
@@ -615,7 +615,7 @@ def test_build_dependencies_virtualenv_simple(tmp_path, assert_output):
 
     assert mock.mock_calls == [
         call(["python3", "-m", "venv", str(tmp_path / STAGING_VENV_DIRNAME)]),
-        call([pip_cmd, "install", f"pip@{KNOWN_GOOD_PIP_URL}"]),
+        call([pip_cmd, "install", "--force-reinstall", f"pip@{KNOWN_GOOD_PIP_URL}"]),
         call([pip_cmd, "install", "--no-binary=:all:", f"--requirement={reqs_file}"]),
     ]
 
@@ -652,7 +652,7 @@ def test_build_dependencies_virtualenv_multiple(tmp_path, assert_output):
     pip_cmd = str(charm_builder._find_venv_bin(tmp_path / STAGING_VENV_DIRNAME, "pip"))
     assert mock.mock_calls == [
         call(["python3", "-m", "venv", str(tmp_path / STAGING_VENV_DIRNAME)]),
-        call([pip_cmd, "install", f"pip@{KNOWN_GOOD_PIP_URL}"]),
+        call([pip_cmd, "install", "--force-reinstall", f"pip@{KNOWN_GOOD_PIP_URL}"]),
         call(
             [
                 pip_cmd,
@@ -714,7 +714,7 @@ def test_build_dependencies_virtualenv_packages(tmp_path, assert_output):
 
     assert mock.mock_calls == [
         call(["python3", "-m", "venv", str(tmp_path / STAGING_VENV_DIRNAME)]),
-        call([pip_cmd, "install", f"pip@{KNOWN_GOOD_PIP_URL}"]),
+        call([pip_cmd, "install", "--force-reinstall", f"pip@{KNOWN_GOOD_PIP_URL}"]),
         call([pip_cmd, "install", "--no-binary=:all:", "pkg1", "pkg2"]),
     ]
 
@@ -747,7 +747,7 @@ def test_build_dependencies_virtualenv_binary_packages(tmp_path, assert_output):
 
     assert mock.mock_calls == [
         call(["python3", "-m", "venv", str(tmp_path / STAGING_VENV_DIRNAME)]),
-        call([pip_cmd, "install", f"pip@{KNOWN_GOOD_PIP_URL}"]),
+        call([pip_cmd, "install", "--force-reinstall", f"pip@{KNOWN_GOOD_PIP_URL}"]),
         call([pip_cmd, "install", "pkg1", "pkg2"]),
     ]
 
@@ -786,7 +786,7 @@ def test_build_dependencies_virtualenv_all(tmp_path, assert_output):
 
     assert mock.mock_calls == [
         call(["python3", "-m", "venv", str(tmp_path / STAGING_VENV_DIRNAME)]),
-        call([pip_cmd, "install", f"pip@{KNOWN_GOOD_PIP_URL}"]),
+        call([pip_cmd, "install", "--force-reinstall", f"pip@{KNOWN_GOOD_PIP_URL}"]),
         call(
             [
                 pip_cmd,
diff --git a/tests/unit/utils/test_package.py b/tests/unit/utils/test_package.py
index 8bfe73af..693ce959 100644
--- a/tests/unit/utils/test_package.py
+++ b/tests/unit/utils/test_package.py
@@ -131,7 +131,14 @@ def test_get_pip_command(
 
 @pytest.mark.parametrize(
     ("pip_cmd", "stdout", "expected"),
-    [("pip", "pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)\n", (22, 0, 2))],
+    [
+        ("pip", "pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)\n", (22, 0, 2)),
+        (
+            "venv/bin/pip",
+            "pip 20.0.2 from /root/venv/lib/python3.8/site-packages/pip (python 3.8)",
+            (20, 0, 2),
+        ),
+    ],
 )
 def test_get_pip_version_success(
     fake_process,