From 42072ba34169d480c721cd9e9aa6eb4df2351ffa Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 3 Sep 2026 14:13:25 +0200 Subject: [PATCH 1/4] CI: cross-platform-actions 1.5.0, haiku r1beta6 Backport of 74d943092 and 9004e3c22 from master. cross-platform-actions 1.5.0 adds support for Haiku R1/beta6, so use that instead of r1beta5 (2024), which was too painful to work with, see #9463. The r1beta6 image already has git, pkgconfig, lz4 and openssl3, and it comes with a usable python3, so it does not need a python or cffi package installed anymore (1.4 does not use argon2-cffi). Also, from master: The VM corrupts data every now and then: rustc crashed on crate metadata it had written itself ("assertion failed: bytes[len] == STR_SENTINEL") and pip found sha256 mismatches reading big wheels back from its own cache. So build outside /boot/system/cache/tmp (haiku's /tmp), run one rustc at a time and retry a pip install with a purged cache, so a damaged file does not fail the whole job. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 41 +++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index daf6809eba..fad9bcd899 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -314,7 +314,7 @@ jobs: # extra RAM for the mfs-backed TMPDIR, see the openbsd test step memory: 12G - os: haiku - version: 'r1beta5' + version: 'r1beta6' display_name: Haiku do_binaries: false @@ -327,7 +327,7 @@ jobs: - name: Test on ${{ matrix.display_name }} id: cross_os - uses: cross-platform-actions/action@v1.4.0 + uses: cross-platform-actions/action@v1.5.0 env: DO_BINARIES: ${{ matrix.do_binaries }} with: @@ -442,24 +442,47 @@ jobs: ;; haiku) pkgman refresh - pkgman install -y git pkgconfig zstd lz4 xxhash - pkgman install -y openssl3 + # already installed: + # pkgman install -y git pkgconfig lz4 openssl3 + pkgman install -y lz4_devel zstd_devel xxhash_devel openssl3_devel pkgman install -y rust_bin - pkgman install -y python3.10 - pkgman install -y cffi - pkgman install -y lz4_devel zstd_devel xxhash_devel openssl3_devel libffi_devel + # there is no pkgman package for tox, so we install it into a venv python3 -m ensurepip --upgrade python3 -m pip install --upgrade pip wheel python3 -m venv .venv . .venv/bin/activate + export PKG_CONFIG_PATH="/system/develop/lib/pkgconfig:/system/lib/pkgconfig:${PKG_CONFIG_PATH:-}" export BORG_LIBLZ4_PREFIX=/system/develop export BORG_LIBZSTD_PREFIX=/system/develop export BORG_LIBXXHASH_PREFIX=/system/develop export BORG_OPENSSL_PREFIX=/system/develop - pip install -r requirements.d/development.txt - pip install -e . + + # this VM corrupts data every now and then: rustc crashed on crate metadata it + # had written itself ("assertion failed: bytes[len] == STR_SENTINEL") and pip + # found sha256 mismatches reading big wheels back from its own cache. Build + # outside /boot/system/cache/tmp (haiku's /tmp) and run one rustc at a time, + # in case that corruption comes from disk or memory pressure. + export TMPDIR=/boot/home/borg-tmp + mkdir -p "$TMPDIR" + export CARGO_TARGET_DIR="$TMPDIR/cargo-target" + export CARGO_BUILD_JOBS=1 + + # retry a pip install that hit a corrupted file, dropping the cached (possibly + # corrupted) files first, so one of them does not fail the whole job. + pip_install() { + for attempt in 1 2 3; do + pip install "$@" && return 0 + echo "*** pip install failed (attempt $attempt), purging the pip cache and retrying ***" + pip cache purge || true + done + return 1 + } + + pip_install -r requirements.d/development.txt + pip_install -e . + # troubles with either tox or pytest xdist, so we run pytest manually: pytest -v -rs --benchmark-skip -k "not remote and not socket" ;; From 8879ec9c6836c3b8ee8003f9eb9d0ca23c6d7a99 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 3 Sep 2026 14:34:54 +0200 Subject: [PATCH 2/4] haiku: tolerate failures reading the lock directory Backport of 2fa73c95e from master. kill_stale_lock() only expected FileNotFoundError from os.listdir(). Haiku throws EBUSY there while the lock directory is concurrently replaced via rename(), and sometimes "bad data" (B_BAD_DATA) instead, so the exception escaped ExclusiveLock.acquire() and the lock race test saw it as unclean concurrency handling. Do not enumerate error codes: not being able to read the lock directory means we can not call the lock stale, whatever the reason. Co-Authored-By: Claude Opus 5 --- src/borg/locking.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/borg/locking.py b/src/borg/locking.py index 27966d4dde..0271d1d4e0 100644 --- a/src/borg/locking.py +++ b/src/borg/locking.py @@ -195,6 +195,11 @@ def kill_stale_lock(self): names = os.listdir(self.path) except FileNotFoundError: # another process did our job in the meantime. pass + except OSError: + # we can not read the lock directory - e.g. haiku fails this with EBUSY or + # "bad data" while the directory is concurrently replaced via rename(). + # Not being able to look at the lock means we can not call it stale. + return False else: for name in names: try: From c96813ec04311ec2ad0c7c7887fdf9578cccdd39 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 3 Sep 2026 15:45:19 +0200 Subject: [PATCH 3/4] tests: skip the crc32_clmul overread check on haiku The overread check does not work on haiku: it builds its trap via ctypes.util.find_library('c'), which haiku answers by walking os.environ['LIBRARY_PATH'] - unset in a non-interactive shell, so the check subprocess dies with a KeyError and the test reports an overread that never happened. Co-Authored-By: Claude Opus 5 --- src/borg/testsuite/checksums.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/borg/testsuite/checksums.py b/src/borg/testsuite/checksums.py index d18942c554..134dea3ccb 100644 --- a/src/borg/testsuite/checksums.py +++ b/src/borg/testsuite/checksums.py @@ -8,6 +8,7 @@ from ..algorithms import checksums from ..helpers import bin_to_hex, hex_to_bin +from ..platformflags import is_haiku crc32_implementations = [checksums.crc32_slice_by_8] if checksums.have_clmul: @@ -118,6 +119,7 @@ def test_crc32_short_slices(implementation): @pytest.mark.skipif(not checksums.have_clmul, reason='needs CLMUL support') @pytest.mark.skipif(sys.platform == 'win32', reason='needs POSIX mmap/mprotect') +@pytest.mark.skipif(is_haiku, reason='the overread check does not work on haiku') def test_crc32_clmul_no_overread(): # crc32_clmul used to load 16 bytes unconditionally for inputs of 4..15 bytes, # reading up to 12 bytes past the end of the buffer. That is a SIGSEGV whenever From d2fb419221e94c9e51a2cf434316e9e14a287513 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 3 Sep 2026 16:02:39 +0200 Subject: [PATCH 4/4] extract: never try to remove the extraction directory itself An archive made with "borg create ARCHIVE ." has an item whose path is ".", so extract_item computed /. and the "remove what is there before (re)creating the item" block called os.rmdir(/.) on it. posix requires rmdir() to fail with EINVAL for a path whose last component is ".", so everywhere else this was a silently ignored no-op. Haiku's dir_remove() strips a trailing "/." from the path instead and then removed the extraction directory, which is empty at that point - after which the extraction failed with B_ENTRY_NOT_FOUND for every item. There is nothing to remove for such a path anyway (the item is the directory we are extracting into), so skip the block. Co-Authored-By: Claude Opus 5 --- src/borg/archive.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index f1fadd5d2f..66204cc9ae 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -868,17 +868,22 @@ def extract_item(self, item, restore_attrs=True, dry_run=False, stdout=False, sp # overwrite files outside the extraction directory (e.g. /etc/passwd). self.safe_dirs.discard(path) # path is about to be (re)created; never trust a stale entry for it self._check_safe_parent(item.path) - # Attempt to remove existing files, ignore errors on failure - try: - st = os.stat(path, follow_symlinks=False) - if stat.S_ISDIR(st.st_mode): - os.rmdir(path) - else: - os.unlink(path) - except UnicodeEncodeError: - raise self.IncompatibleFilesystemEncodingError(path, sys.getfilesystemencoding()) from None - except OSError: - pass + # Attempt to remove existing files, ignore errors on failure. + # A trailing "." component means the extraction directory itself (the archive's + # "." item, as created by "borg create ARCHIVE ."). posix requires rmdir() to + # refuse such a path with EINVAL, but haiku strips the "." and would remove the + # extraction directory (if empty), so do not even try to remove it. + if os.path.basename(path) != '.': + try: + st = os.stat(path, follow_symlinks=False) + if stat.S_ISDIR(st.st_mode): + os.rmdir(path) + else: + os.unlink(path) + except UnicodeEncodeError: + raise self.IncompatibleFilesystemEncodingError(path, sys.getfilesystemencoding()) from None + except OSError: + pass def make_parent(path): parent_dir = os.path.dirname(path)