Skip to content

✨ feat(platform): support Termux/Android#678

Merged
gaborbernat merged 7 commits into
tox-dev:mainfrom
gaborbernat:feat/termux-support
Jul 18, 2026
Merged

✨ feat(platform): support Termux/Android#678
gaborbernat merged 7 commits into
tox-dev:mainfrom
gaborbernat:feat/termux-support

Conversation

@gaborbernat

Copy link
Copy Markdown
Member

import filelock crashed on Termux/Android with AttributeError: module 'os' has no attribute 'link', reported in the Termux support discussion (#677). The strict backend read os.link at import time, and Termux's bionic CPython ships without it, so from filelock import FileLock died before filelock could pick a backend, even though flock and the soft backend both work on the app-private filesystem. 🤖

Every os.link reference now sits behind one capability probe, so the module imports and a StrictSoftFileLock reports the missing hard links through the existing unsupported-filesystem error only when a caller acquires it. Termux also reports sys.platform == "android" rather than "linux", which had left the /proc-based process-start token degrading to PID-only liveness. Android runs the Linux kernel with a working /proc, so it now takes the Linux path.

A StrictSoftFileLock cannot run without hard links, so the suite skips those cases where os.link is absent, and a new termux CI job runs the library inside a real termux-docker userland to keep the platform green. The branch also folds in the pre-commit autoupdate that bumps ruff to 0.15.22 and rewrites the repo's # noqa suppressions as # ruff:ignore, which is why the diff reaches files beyond the platform change.

Importing filelock crashed on Termux with `AttributeError: module 'os'
has no attribute 'link'`, because the strict backend read `os.link` at
import time and Termux's bionic CPython ships without it (discussion
tox-dev#677). That killed `from filelock import FileLock` before any lock was
even chosen, even though flock and the soft backend both work there.

Gate every `os.link` reference on a single capability probe so the
import succeeds and only a real StrictSoftFileLock acquire reports the
missing hard links, reusing the existing unsupported-filesystem error
path. Termux also reports `sys.platform == "android"` rather than
"linux", so the /proc-based process-start token silently degraded to
PID-only liveness; treat android as Linux, since it runs the same kernel
with a working /proc.

The strict lock genuinely cannot run without hard links, so its tests
carry a requires_hard_links marker that the conftest skips when os.link
is absent, and a new CI job runs the whole suite inside a real
termux-docker userland to keep the platform honest. Verified there:
1042 passed, 159 skipped, zero failures.

Bundled with the pre-commit autoupdate that bumps ruff to 0.15.22 and
migrates the repo's `# noqa` suppressions to the `# ruff:ignore` syntax.
The requires_hard_links pytestmark added to test_strict_soft_recovery.py
rebound the module's existing cpython-only pytestmark instead of joining
it, so the crash-injection tests ran on PyPy, where the os.link/os.remove
audit events never fire and the child finishes cleanly rather than
crashing. Combine both markers in a list.

The eight-process contention worker proved exclusivity by creating a
shared file with O_EXCL and unlinking it inside the lock, which tripped
over Windows delete-pending lag: the previous holder's unlink had not
finished before the next holder's create ran, raising a spurious
FileExistsError. Count increments under the lock instead; a lost update
still exposes a real overlap without depending on unlink timing.
Replace the hand-rolled pip install with `docker compose run --rm
termux`, which installs tox and the patched filelock, then runs `tox run
-e py` inside a real Termux userland. tox imports filelock for its own
provisioning lock, so the patched tree goes in first, otherwise tox
crashes on startup with the very AttributeError this branch fixes. The
same one command reproduces the job locally.

Raise the per-environment coverage floor from 76 to 79, the lowest
non-Termux figure the matrix reports (Windows); Termux clears it at 86.
Give the sole try-except-in-loop suppression its missing rationale.
Under heavy contention StrictSoftFileLock could let two processes hold the
lock at once. A winner unlinked its intent claim, leaving only the freshly
linked held claim as its marker. os.scandir gives no guarantee about entries
created during a scan (POSIX leaves it unspecified), so a contender's final
rescan could miss that held claim, see none of the holder's claims, and read
itself as the lowest-token owner. PyPy's timing exposed this reliably (13/25
runs overlapped at 16x300); CPython hides it (0/8).

Keep the intent claim for the whole hold. It has existed unchanged since the
owner published it, so any complete scan is guaranteed to return it; the
min-token decision is then always computed over the true claim set. Release
removes both claims.

Observable: a held lock now exposes both an intent and a held claim, so
force_break must remove both to break it. Recovery and failure tests updated
to the two-claim state; the stress test now detects overlap with an occupancy
marker and no longer asserts empty worker stderr, which flaked on interpreter
warnings.
Split the Termux run into a cached base image (apt + tox) and a per-call
compose command, so iterating on the source no longer re-runs the apt/tox
bootstrap. tasks/termux/run.sh folds into tasks/termux/Dockerfile plus the
compose command.

Keep copying the source out of the mount before tox runs: in Termux, tox
registers its package env twice and dies with "duplicate configuration
definition for .pkg" whenever its work_dir is on a different filesystem than
the project (tox-dev/tox#3987). Copying keeps the project and its .tox on one
layer and leaves the mounted tree untouched.
_check_give_up drops its lock_id/lock_filename parameters and reads them from
self as an instance method; _resolve_lifetime takes the lock class instead of
five unpacked attributes. The remaining ignores sit on public constructors and
their metaclass __call__ forwarders, where the parameter count is the
documented API, so each now carries a reason for why it stays.
Every inline ruff:ignore now either states why it stands or is gone. Two are
fixed: _resolve_preserve_lock_file takes its bool keyword-only, and the soft
marker's line count reads through a named constant. The rest sit on public
signatures, deliberate internal cross-instance access, interface-parity
arguments, and intentional test patterns, each now carrying its reason.
@gaborbernat
gaborbernat merged commit 166b2b5 into tox-dev:main Jul 18, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant