`stage-packages` is nondeterministic
Metadata
Current evaluation
Fixed by preserving user-specified stage-package order before APT marking; commit 26f4b35e uses list(dict.fromkeys(package_names)), removing hash-seed-dependent dependency selection.
Resolution: closed resolved
Reason: Commit 26f4b35e deduplicates package names with list(dict.fromkeys(package_names)) and passes that ordered list to AptCache.mark_packages. This ensures locales-all is marked before postgresql-18, so APT consistently selects the intended alternative dependency.
Evaluated by: mr-cal-bot
No scores available.
Issue body
### Bug Description
`stage-packages` (with identical contents) installs different packages on different invocations of *craft pack
#### Context
The `postgresql-18` deb package depends on `locales | locales-all`
#### Expected behavior
There is a way to stage the `locales-all` and `postgresql-18` packages without staging the `locales` package
Either with
```yaml
stage-packages:
- locales-all
- postgresql-18
```
or
```yaml
parts:
a:
plugin: nil
stage-packages:
- locales-all
b:
after:
- a
stage-packages:
- postgresql-18
```
#### Actual behavior
```yaml
stage-packages:
- locales-all
- postgresql-18
```
sometimes stages the `locales` package and sometimes does not
### To Reproduce
```shell
mkdir foo
cd foo
cat > rockcraft.yaml << 'EOF'
name: repro
version: '1.0'
title: repro
summary: foo
description: foo
base: bare
build-base: ubuntu@26.04
platforms:
amd64:
arm64:
parts:
foo:
plugin: nil
stage-packages:
- locales-all
- postgresql-18
EOF
for i in $(seq 1 6); do
rm -rf *.rock unpack contents.txt
rockcraft clean
rockcraft pack
mkdir unpack
rock=$(ls *.rock)
tar -xf "$rock" -C unpack
for blob in unpack/blobs/sha256/*; do tar -tf "$blob" 2>/dev/null; done > contents.txt
if grep -q 'usr/sbin/locale-gen' contents.txt; then
echo "run $i: locales is in the rock"
else
echo "run $i: locales is NOT in the rock"
fi
done
```
Example output
```
Packed repro_1.0_arm64.rock
run 1: locales is NOT in the rock
Packed repro_1.0_arm64.rock
run 2: locales is in the rock
Packed repro_1.0_arm64.rock
run 3: locales is in the rock
Packed repro_1.0_arm64.rock
run 4: locales is NOT in the rock
Packed repro_1.0_arm64.rock
run 5: locales is NOT in the rock
Packed repro_1.0_arm64.rock
run 6: locales is NOT in the rock
```
#### Simplified steps to reproduce, without rockcraft
```shell
lxc launch ubuntu:26.04 repro
lxc shell repro
apt update
apt install python3-pip python3-apt -y
pip install craft-parts --break-system-packages
cat > repro.py << 'EOF'
import os
import pathlib
import craft_parts.packages.deb
import shutil
craft_parts.packages.deb.Ubuntu.configure("foo")
names = {"locales-all", "postgresql"} # deb.py:907 does `set(package_names)`
out = pathlib.Path("/tmp/out")
if out.exists():
shutil.rmtree(out)
out.mkdir()
result = craft_parts.packages.deb.Ubuntu.fetch_stage_packages(cache_dir=pathlib.Path("/tmp/cache"), package_names=names, stage_packages_path=out, base="bare", arch="amd64")
print(f"PYTHONHASHSEED={os.environ.get('PYTHONHASHSEED')} -> locales fetched:", any(package.startswith("locales=") for package in result))
EOF
for seed in 0 1 2 3 4 5 6 7; do PYTHONHASHSEED=$seed python3 repro.py; done
```
Example output
```
PYTHONHASHSEED=0 -> locales fetched: False
PYTHONHASHSEED=1 -> locales fetched: True
PYTHONHASHSEED=2 -> locales fetched: False
PYTHONHASHSEED=3 -> locales fetched: False
PYTHONHASHSEED=4 -> locales fetched: True
PYTHONHASHSEED=5 -> locales fetched: True
PYTHONHASHSEED=6 -> locales fetched: False
PYTHONHASHSEED=7 -> locales fetched: False
```
### part yaml
```shell
name: repro
version: '1.0'
title: repro
summary: foo
description: foo
base: bare
build-base: ubuntu@26.04
platforms:
amd64:
arm64:
parts:
foo:
plugin: nil
stage-packages:
- locales-all
- postgresql-18
```
### Relevant log output
```shell
-
```
Evaluation history
| Date | Model | Scores | Action | Summary |
|---|---|---|---|---|
| stealth/space-bunny-alpha | — | closed resolved | Fixed by preserving user-specified stage-package order before APT marking; commit 26f4b35e uses list(dict.fromkeys(package_names)), removing hash-seed-dependent dependency selection. | |
| stealth/space-bunny-alpha | — | closed resolved | Fixed in canonical/craft-parts#1752, commit 26f4b35e, making stage-packages deterministic by eliminating hash-order-dependent package marking. | |
| qwen/qwen3.8-27b |
Impact:
60
Quick Win:
34
Complexity:
35
Confidence:
85
Actionability:
88
|
needs triage | stage-packages is nondeterministic: deb.py:907 turns names into a set and AptCache.mark_packages iterates it, so apt marks packages in hash-dependent order; alternative deps (locales|locales-all) resolve differently per run. Valid, reproducible, unfixed. | |
| qwen/qwen3.8-27b |
Impact:
60
Quick Win:
38
Complexity:
30
Confidence:
85
Actionability:
90
|
needs triage | stage-packages nondeterminism: apt OR-dependency resolution (locales | locales-all) picks different providers per run because mark_packages iterates a Python set whose order varies with PYTHONHASHSEED. Root cause pinpointed at deb.py:907; new, unlabelled. | |
| qwen/qwen3.8-27b |
Impact:
60
Quick Win:
43
Complexity:
20
Confidence:
85
Actionability:
90
|
needs triage | stage-packages nondeterminism: mark_packages iterates a Python set, so apt resolves alternative deps (locales vs locales-all) differently per run. Root cause confirmed at deb.py:907; unfixed, unlabeled, no maintainer response. | |
| qwen/qwen3.8-27b |
Impact:
60
Quick Win:
28
Complexity:
45
Confidence:
85
Actionability:
85
|
needs triage | stage-packages fetch is nondeterministic: deb.py turns the ordered package list into a set, so apt marks packages in hash-randomized order and picks different alternatives (locales vs locales-all) per run. Unlabelled, no maintainer response. |
Update history
| Date | Change |
|---|---|
| closed | |
| updated | |
| created |
Related issues
| Issue | Project | State | Summary | Similarity |
|---|---|---|---|---|
| #1752 fix: Make `stage-packages` deterministic | craft-parts | merged | Merged PR #1752 in commit 26f4b35e, preserving stage-packages input order during deduplication and APT marking to make resolution deterministic; fixes #1751. |