Install directory not cleaned after failed build, causing subsequent builds to fail with unclear errors. New issue with no labels or maintainer response.
Suggested action: needs triage
Reason: This is a new, unlabelled issue with no maintainer response or comments. The issue is well-scoped with a clear reproducer test pointing to the exact code location in part_handler.py where the build directory is cleaned but the install directory is not. The bug is confirmed by examining the code: _run_build removes part_build_dir but not part_install_dir on failure, unlike _update_build which does clean the install directory.
### Bug Description
This is a distillation of one part of https://github.com/canonical/charmcraft/issues/2969
When the previous build of a part failed, we [remove the build directory](https://github.com/canonical/craft-parts/blob/cd1265cdd3347ce1328ddf0971df88e1d1b89327/craft_parts/executor/part_handler.py#L470-L471), but we don't remove the install directory. If the build plugin isn't idempotent (or the user provides a non-idempotent `override-build` script), this can result in a single failed build making all future builds fail for reasons that are unclear to the user.
### To Reproduce
I got Gemini to create this xfailing test that we can put in `TestPartHandling`:
```python
@pytest.mark.xfail(
raises=AssertionError,
reason="https://github.com/canonical/charmcraft/issues/2969",
strict=True,
)
def test_run_build_cleans_install_dir_after_failure(
self, mocker: MockerFixture
) -> None:
mocker.patch(
"craft_parts.packages.Repository.get_installed_packages",
return_value=[],
)
mocker.patch(
"craft_parts.packages.snaps.get_installed_snaps",
return_value=[],
)
mocker.patch("subprocess.check_output", return_value=b"os-info")
leftover_file = self._part.part_install_dir / "leftover.txt"
def _failing_build(*args: Any, **kwargs: Any) -> None:
leftover_file.parent.mkdir(parents=True, exist_ok=True)
leftover_file.write_text("partial content")
raise errors.ScriptletRunError(
part_name="foo", scriptlet_name="override-build", exit_code=1
)
mocker.patch(
"craft_parts.executor.step_handler.StepHandler._builtin_build",
side_effect=_failing_build,
)
with pytest.raises(errors.ScriptletRunError):
self._handler.run_action(
Action(self._part.name, Step.BUILD, action_type=ActionType.RUN)
)
# TODO: Change this back to an assert.
# This raises a RuntimeError to ensure that the caught AssertionError is
# the correct one.
if not leftover_file.exists():
raise RuntimeError(
"Precondition failed: failed build did not leave file in install dir"
)
mocker.patch(
"craft_parts.executor.step_handler.StepHandler._builtin_build",
return_value=None,
)
self._handler.run_action(
Action(self._part.name, Step.BUILD, action_type=ActionType.RUN)
)
assert not leftover_file.exists()
### part yaml
```shell
```
### Relevant log output
```shell
Relevant section from charmcraft#2969:
:: Removing python symlinks in /root/parts/charm-poetry/install/venv/bin
:: + shopt -s extglob
:: + rm -rf /root/parts/charm-poetry/install/venv/bin/Activate.ps1 /root/parts/charm-poetry/install/venv/bin/activate.csh /root/parts/charm-poetry/install/venv/bin/activate.fish /root/parts/charm-poetry/install/venv/bin/๐thon
:: + shopt -u extglob
:: + sed -i 's#^VIRTUAL_ENV=.*$#VIRTUAL_ENV="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." \&> /dev/null \&\& pwd )"#' /root/parts/charm-poetry/install/venv/bin/activate
:: + '[' -L /root/parts/charm-poetry/install/venv/lib64 ']'
:: + rm -f /root/parts/charm-poetry/install/venv/lib64
Building files
:: + craftctl default
:: + cp --archive --link --no-dereference . /root/parts/files/install
:: + exit 1
'override-build' in part 'files' failed with code 1.
Detailed information:
:: + craftctl default
:: + exit 1
Recommended resolution: Review the scriptlet and make sure it's correct.
For more information, check out: https://documentation.ubuntu.com/charmcraft/4/reference/plugins/
Failed to run charmcraft in instance
Full execution log: '/home/ubuntu/.local/state/charmcraft/log/charmcraft-20261022-111102.907563.log'
ubuntu@dualla:~/bug-report-charmcraft-monorepo-repack/kubernetes$ CHARMCRAFT_EXPERIMENTAL_MONOREPO=1 charmcraft pack -v
Starting charmcraft, version 4.5.0
Logging execution to '/home/ubuntu/.local/state/charmcraft/log/charmcraft-20261022-111342.742597.log'
Launching managed ubuntu 26.04 instance...
Starting instance
Starting charmcraft, version 4.5.0
Logging execution to '/tmp/charmcraft.log'
Initializing lifecycle
Installing build-packages
Skipping pull for charm-poetry (already ran)
Skipping pull for files (already ran)
Skipping build for charm-poetry (already ran)
Building files
:: + craftctl default
:: + cp --archive --link --no-dereference . /root/parts/files/install
:: cp: cannot create hard link '/root/parts/files/install/./pyproject.toml' to './pyproject.toml': File exists
:: cp: cannot create hard link '/root/parts/files/install/./poetry.lock' to './poetry.lock': File exists
:: cp: cannot create hard link '/root/parts/files/install/./metadata.yaml' to './metadata.yaml': File exists
:: cp: cannot create hard link '/root/parts/files/install/./charmcraft.yaml' to './charmcraft.yaml': File exists
Failed to run the build script for part 'files'.
Detailed information:
:: + cp --archive --link --no-dereference . /root/parts/files/install
:: cp: cannot create hard link '/root/parts/files/install/./pyproject.toml' to './pyproject.toml': File exists
:: cp: cannot create hard link '/root/parts/files/install/./poetry.lock' to './poetry.lock': File exists
:: cp: cannot create hard link '/root/parts/files/install/./metadata.yaml' to './metadata.yaml': File exists
:: cp: cannot create hard link '/root/parts/files/install/./charmcraft.yaml' to './charmcraft.yaml': File exists
Recommended resolution: Check the build output and verify the project can work with the 'dump' plugin.
```
Evaluation history
Date
Model
Scores
Action
Summary
qwen/qwen3.6-35b-a3b
Impact:
60
Quick Win:
38
Complexity:
20
Confidence:
80
Actionability:
80
needs triage
Install directory not cleaned after failed build, causing subsequent builds to fail with unclear errors. New issue with no labels or maintainer response.
qwen/qwen3.6-35b-a3b
Impact:
35
Quick Win:
22
Complexity:
25
Confidence:
85
Actionability:
85
keep open
Build directory is cleaned on failure but install directory is not, causing subsequent builds to fail with leftover partial state from non-idempotent builds.