diff --git a/CHANGELOG.md b/CHANGELOG.md index cb22cc7..aa1754e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ backfill releases from before this changelog. - Preserve historical selections and store-fault messages through graph refresh and resize. - Show hourly drill-down results without overwriting them with a loading placeholder. - Keep known unallocated daily volume visible across coverage gaps, and distinguish missing hourly evidence from zero on small terminals. +- Stage package directories with consistent public permissions, avoiding openSUSE RPM conflicts and inaccessible runtime paths when built with a restrictive umask. ## [0.4.0] - 2026-09-18 diff --git a/packaging/stage.sh b/packaging/stage.sh index 8b5eb6e..bf28e10 100755 --- a/packaging/stage.sh +++ b/packaging/stage.sh @@ -14,6 +14,10 @@ # /usr/lib/tmpfiles.d/fenris.conf set -euo pipefail +# Package directories must be traversable by every runtime user and agree +# with distribution-owned directories, regardless of the builder's umask. +umask 022 + REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" STAGE_DIR="${REPO_ROOT}/build/stage" diff --git a/tests/test_package_stage.py b/tests/test_package_stage.py new file mode 100644 index 0000000..6b8cb85 --- /dev/null +++ b/tests/test_package_stage.py @@ -0,0 +1,43 @@ +"""Package directory permissions must not inherit a builder's private umask.""" +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + + +@pytest.mark.parametrize("mask", ["077", "022"]) +def test_stage_is_publicly_traversable_under_any_builder_umask(tmp_path, mask): + root = Path(__file__).resolve().parent.parent + for directory in ("packaging", "scripts", "src", "units", "polkit"): + shutil.copytree(root / directory, tmp_path / directory, + ignore=shutil.ignore_patterns("__pycache__", "*.egg-info")) + for filename in ("LICENSE", "requirements.txt"): + shutil.copy(root / filename, tmp_path / filename) + (tmp_path / "dist").mkdir() + (tmp_path / "dist/fenris-9.9.9-py3-none-any.whl").touch() + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + # Avoid downloading dependencies: emulate pip's target output while + # exercising the real staging script and its package directory layout. + pip_stub = bin_dir / "python3" + pip_stub.write_text( + f"#!{sys.executable}\n" + "import sys\nfrom pathlib import Path\n" + "target = Path(sys.argv[sys.argv.index('--target') + 1]) / 'fenris'\n" + "target.mkdir(parents=True)\n" + "(target / '__init__.py').write_text('')\n" + ) + pip_stub.chmod(0o755) + subprocess.run( + ["bash", "-c", f"umask {mask}; exec bash packaging/stage.sh 9.9.9"], + cwd=tmp_path, env={**os.environ, "PATH": str(bin_dir) + os.pathsep + os.environ["PATH"]}, + check=True, capture_output=True, text=True, + ) + stage = tmp_path / "build/stage" + for directory in (stage, *(p for p in stage.rglob("*") if p.is_dir())): + assert directory.stat().st_mode & 0o777 == 0o755, directory + assert (stage / "usr/bin/fenris").stat().st_mode & 0o777 == 0o755 + assert (stage / "opt/fenris/vendor/fenris/__init__.py").stat().st_mode & 0o444 == 0o444