From 74d94309296e31c71810dc01030de60ef998a838 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 28 Aug 2026 22:11:55 +0200 Subject: [PATCH 1/8] CI: cross-platform-actions 1.5.0, re-add haiku (r1beta6) 1.5.0 adds support for Haiku R1/beta6, so bring haiku back into the vm_tests matrix - it was removed in 30ed2bcbf because r1beta5 (2024) was too painful to work with, see #9463. Also: build blake3 outside /tmp, one rustc at a time rustc 1.94.1 ICEd while compiling the blake3 crate ("assertion failed: bytes[len] == STR_SENTINEL"), reading back metadata that does not match what it wrote. Try building outside /boot/system/cache/tmp with a single rustc at a time, in case that corruption comes from disk or memory pressure there. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d18ec53dc..4be281812d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -451,6 +451,12 @@ jobs: pyver: '3.13' do_binaries: false + - os: haiku + version: 'r1beta6' + display_name: Haiku + pyver: '3.14' + do_binaries: false + steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -474,7 +480,7 @@ jobs: # a normal boot takes < 1 minute; since 1.4.0 the action bounds # waiting for boot itself, this is just an additional safety net. timeout-minutes: 15 - uses: cross-platform-actions/action@24ef01df165c76df1ed2b9f9e9212e78dc2fc963 # v1.4.0 + uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0 with: operating_system: ${{ matrix.os }} version: ${{ matrix.version }} @@ -641,13 +647,11 @@ jobs: haiku) pkgman refresh - pkgman install -y git pkgconfig lz4 - pkgman install -y openssl3 - pkgman install -y rust_bin - pkgman install -y python3.11 + # already installed: + # pkgman install -y git pkgconfig lz4 openssl3 pkgman install -y lz4_devel openssl3_devel + pkgman install -y rust_bin - # 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 @@ -656,6 +660,17 @@ jobs: export PKG_CONFIG_PATH="/system/develop/lib/pkgconfig:/system/lib/pkgconfig:${PKG_CONFIG_PATH:-}" export BORG_LIBLZ4_PREFIX=/system/develop export BORG_OPENSSL_PREFIX=/system/develop + + # rustc 1.94.1 crashes while compiling the blake3 crate ("assertion failed: + # bytes[len] == STR_SENTINEL"), i.e. it read back metadata that does not + # match what it wrote. 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 + pip install -r requirements.d/development.lock.txt pip install -e . From d47f3ff4da98e3c93b454b4c15f84889c86f682b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 29 Aug 2026 00:10:49 +0200 Subject: [PATCH 2/8] haiku: depend on tzdata, there is no system tz database zoneinfo finds no tz database on haiku, so every "date:" pattern with a named timezone fails with ZoneInfoNotFoundError. Same situation as on windows, so pull in the tzdata package there, too. Co-Authored-By: Claude Opus 5 --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index a80a756a59..4ce900721d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,8 @@ dependencies = [ "blake3 >= 1.0; sys_platform != 'win32'", "blake3 >= 1.0,!=1.0.9; sys_platform == 'win32'", # 1.0.9 does not build on windows/msys2/mingw "tzdata; sys_platform == 'win32'", # zoneinfo has no system tz database on Windows (date: named zones) + # same on haiku (sys.platform is 'haiku1' on pythons older than 3.13) + "tzdata; sys_platform == 'haiku' or sys_platform == 'haiku1'", ] [project.optional-dependencies] From 14cb8d0735f10d906f5b98e2e008aa2ff091e871 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 29 Aug 2026 00:12:30 +0200 Subject: [PATCH 3/8] haiku: a writer-less fifo read fails with ENOMEM, not b"" SpecialFileReader relies on os.read() returning b"" while no writer has opened the fifo yet. On haiku that read fails with ENOMEM instead, which propagated as a backup error, so --read-special never reached its timeout and the fifo tests failed. Co-Authored-By: Claude Opus 5 --- src/borg/helpers/fs.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/borg/helpers/fs.py b/src/borg/helpers/fs.py index e7a3d44ebf..3225bf8277 100644 --- a/src/borg/helpers/fs.py +++ b/src/borg/helpers/fs.py @@ -16,7 +16,7 @@ from .errors import Error from .process import prepare_subprocess_env -from ..platformflags import is_win32 +from ..platformflags import is_win32, is_haiku from ..constants import * # NOQA @@ -614,6 +614,13 @@ def read(self, size): data = os.read(self.fd, remaining) except BlockingIOError: data = None # a writer is connected, but no data is available right now + except OSError as err: + if is_haiku and err.errno == errno.ENOMEM: + # haiku fails a read from a fifo nobody has opened for writing with ENOMEM + # instead of returning b"" - it means the same here: no writer (yet). + data = b"" + else: + raise if data: parts.append(data) remaining -= len(data) From 2fa73c95e5a63c5cce47d15ed84eb4d06083f99c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 30 Aug 2026 20:26:01 +0200 Subject: [PATCH 4/8] haiku: tolerate failures reading the lock directory kill_stale_lock() only expected ENOENT and EACCES from iterdir(). 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 - the same conclusion by_me() draws right above. Co-Authored-By: Claude Opus 5 --- src/borg/fslocking.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/borg/fslocking.py b/src/borg/fslocking.py index 5a7f241ea0..13bba12df2 100644 --- a/src/borg/fslocking.py +++ b/src/borg/fslocking.py @@ -220,6 +220,11 @@ def kill_stale_lock(self): return False except PermissionError: # win32 might throw this. return False + 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, see by_me(). + return False else: for name in names: try: From a9338b649d270708933af20d6321a56ebaf8eb74 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 29 Aug 2026 00:13:39 +0200 Subject: [PATCH 5/8] haiku: do not treat select()'s exceptional set as a fatal error haiku puts the pipes to the borg 1.x serve process into select()'s exceptional set although nothing is wrong, and LegacyRemoteRepository turned that into "FD exception occurred", so transferring from a borg 1.x ssh:// repository failed right at the version negotiation. Co-Authored-By: Claude Opus 5 --- src/borg/legacy/remote.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/borg/legacy/remote.py b/src/borg/legacy/remote.py index 93a5c9dbfb..cef1be02cc 100644 --- a/src/borg/legacy/remote.py +++ b/src/borg/legacy/remote.py @@ -29,7 +29,7 @@ from ..repository import Repository, StoreObjectNotFound from ..version import parse_version, format_version from ..helpers.datastruct import EfficientCollectionQueue -from ..platform import is_win32 +from ..platform import is_win32, is_haiku logger = create_logger(__name__) @@ -249,7 +249,10 @@ def __init__(self, location, create=False, exclusive=False, lock_wait=None, lock self.stdout_fd = self.p.stdout.fileno() self.stderr_fd = self.p.stderr.fileno() self.r_fds = [self.stdout_fd, self.stderr_fd] - self.x_fds = [self.stdin_fd, self.stdout_fd, self.stderr_fd] + # haiku's select() reports pipes in the exceptional set although nothing is wrong, + # so do not watch for exceptional conditions there - a broken pipe still shows up + # as a read/write error below. + self.x_fds = [] if is_haiku else [self.stdin_fd, self.stdout_fd, self.stderr_fd] else: raise Error(f"Unsupported protocol {location.proto}") From f461ba37a1232254781b98b34838af9c942ac2bf Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 29 Aug 2026 11:28:15 +0200 Subject: [PATCH 6/8] tests: retry webdav connects that fail with EWOULDBLOCK haiku fails connecting to the test webdav server with EWOULDBLOCK rather than waiting for it to accept, so the webdav tests failed there whenever the VM was slow. The bigger listen backlog alone did not stop it. Co-Authored-By: Claude Opus 5 --- .../testsuite/archiver/webdav_cmd_test.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/borg/testsuite/archiver/webdav_cmd_test.py b/src/borg/testsuite/archiver/webdav_cmd_test.py index 5457b41fd5..7b6f957265 100644 --- a/src/borg/testsuite/archiver/webdav_cmd_test.py +++ b/src/borg/testsuite/archiver/webdav_cmd_test.py @@ -84,15 +84,30 @@ def webdav_server(archiver): thread.join(timeout=10) +def urlopen(request, attempts=10): + """urllib.request.urlopen, but retry a connect that fails with EWOULDBLOCK. + + haiku fails connecting to a listening localhost socket with EWOULDBLOCK (rather than + waiting for the server to accept) when the server does not accept immediately. + """ + for attempt in range(attempts): + try: + return urllib.request.urlopen(request) + except urllib.error.URLError as err: + if not isinstance(err.reason, BlockingIOError) or attempt == attempts - 1: + raise + time.sleep(0.1) + + def get(url, headers=None): request = urllib.request.Request(url, headers=headers or {}) - with urllib.request.urlopen(request) as response: + with urlopen(request) as response: return response.status, dict(response.headers), response.read() def http_request(url, method, headers=None, body=None): req = urllib.request.Request(url, data=body, method=method, headers=headers or {}) - with urllib.request.urlopen(req) as response: + with urlopen(req) as response: return response.status, dict(response.headers), response.read() @@ -300,7 +315,7 @@ def test_webdav_errors(archivers, request): get(base_url + "/test/input/../input/file1") assert exc_info.value.code == 404 with pytest.raises(urllib.error.HTTPError) as exc_info: - urllib.request.urlopen(urllib.request.Request(base_url + "/", method="POST")) + urlopen(urllib.request.Request(base_url + "/", method="POST")) assert exc_info.value.code == 405 # writing/locking WebDAV methods are rejected, too for method in "PUT", "MKCOL", "LOCK", "PROPPATCH": From 97465caefacca3f56b5a0ba19f3d7ee7d987dd75 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 29 Aug 2026 14:01:23 +0200 Subject: [PATCH 7/8] tests: do not inherit the session's SSH_ORIGINAL_COMMAND borg serve parses SSH_ORIGINAL_COMMAND as the command line its client wants to run (ssh forced command support). When the test suite itself runs in an ssh session that has one - as on the haiku CI runner, where it is "cd && ..." - every borg serve a test starts tries to parse that, fails and exits before serving anything, so the borg 1.x ssh:// transfer test only saw its connection being closed. Co-Authored-By: Claude Opus 5 --- src/borg/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/borg/conftest.py b/src/borg/conftest.py index fd20dfe3f3..9925ee9d29 100644 --- a/src/borg/conftest.py +++ b/src/borg/conftest.py @@ -84,6 +84,10 @@ def clean_env(tmpdir_factory, monkeypatch): keys = [key for key in os.environ if key.startswith("BORG_") and key not in ("BORG_FUSE_IMPL",)] for key in keys: monkeypatch.delenv(key, raising=False) + # a "borg serve" started by a test must not mistake the ssh forced command of the session + # the tests happen to run in (e.g. a CI runner driven over ssh) for the command line of a + # borg client - it would fail to parse it and exit before serving anything. + monkeypatch.delenv("SSH_ORIGINAL_COMMAND", raising=False) # avoid that we access / modify the user's normal .config / .cache directory: base_dir = tmpdir_factory.mktemp("borg-base-dir") monkeypatch.setenv("BORG_BASE_DIR", str(base_dir)) From 9004e3c2294079eef689bf3f00224883b0690827 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 30 Aug 2026 18:48:20 +0200 Subject: [PATCH 8/8] CI: retry pip installs on haiku, the VM corrupts data This VM hands back data that does not match what was written to it: rustc crashed on crate metadata it had written itself ("assertion failed: bytes[len] == STR_SENTINEL"), and pip found sha256 mismatches reading wheels back from its own cache - twice on cython (1.3MB), once on virtualenv (5.5MB), with a different wrong hash every time and none of them matching any published artifact. Only the big files were hit. Retry the installs with a purged pip cache, so a damaged file does not fail the job - and does not get stored in the actions cache, from where it would fail every following run until someone deletes the cache. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4be281812d..71150412bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -661,18 +661,30 @@ jobs: export BORG_LIBLZ4_PREFIX=/system/develop export BORG_OPENSSL_PREFIX=/system/develop - # rustc 1.94.1 crashes while compiling the blake3 crate ("assertion failed: - # bytes[len] == STR_SENTINEL"), i.e. it read back metadata that does not - # match what it wrote. 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. + # 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 - pip install -r requirements.d/development.lock.txt - pip install -e . + # 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 - and does + # not end up in the actions cache, failing every following run, too. + 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.lock.txt + pip_install -e . # troubles with either tox or pytest xdist, so we run pytest manually: pytest -v -n auto -rs --cov=borg --cov-config=pyproject.toml --cov-report=xml --junitxml=test-results.xml --benchmark-skip -k "not remote and not socket"