Two follow-ups to #90 in one PR:
1. Two new match forms in data/interfaces.toml grammar:
suffix = "digits_optional" prefix + 0+ ASCII digits
suffix = "any" prefix + 1+ any chars
Needed by the upcoming whitelist (PR-B) for patterns like
`seth_lte\d*` and `v4-.+`. Not used by any current [[vpn]] rule, but
the helper functions are exercised by direct unit tests in the
generated test modules so a bug would surface before whitelist lands.
2. [[test]] vectors in data/interfaces.toml that the codegen renders
into per-language unit tests:
- zygisk + lsposed/native: #[cfg(test)] mod tests inside the
generated iface_lists.rs (run via `cargo test`)
- lsposed/app: a separate IfaceListsGeneratedTest under
src/test/kotlin (run via `:app:testDebugUnitTest`)
- kmod: a userspace test driver test_iface_lists.c — the
generated header now has __KERNEL__-guarded includes so the
same matcher compiles against libc, and a new lint step builds
and runs it via gcc.
36 fixed vectors today; trivial to grow as new rules / corner cases
come up. CI catches drift on the next push: any single matcher that
disagrees with the toml fails its job.
No production behavior change — generated matches_vpn / vpnhide_iface_is_vpn
/ IfaceLists.isVpnIface bodies are byte-identical to before; only the
helper functions and test modules grew.
The kernel module, zygisk, lsposed-native, and the LSPosed Kotlin module
each had their own hand-written list of VPN interface name prefixes,
and the four had drifted: kmod/zygisk/HookEntry knew utun/l2tp/gre
while lsposed-native and DiagnosticsScreen only knew tun/wg/ppp/tap/
ipsec/xfrm. So the self-test could PASS while the hooks were actually
hiding more interfaces.
Move the rules to data/interfaces.toml and render four matchers from it
via scripts/codegen-interfaces.py — one per language target. A new lint
job re-runs the codegen and fails if anything drifts.
The match grammar is intentionally tiny so each codegen target
implements it without depending on regex (kernel C can't):
exact / prefix / prefix+digits / contains.
Side effect: native diagnostics now agree with the hooks, so the
self-test in DiagnosticsScreen will recognize utun*, l2tp*, gre* and
*vpn* substrings as VPN tunnels (previously it would silently PASS on
those). The /proc/net/route check also moved from raw substring to
whitespace-tokenized matching, which avoids matching VPN-prefix
substrings that show up by chance inside hex-encoded IP addresses.
Existing zygisk filter unit tests still pass unchanged — public API of
is_vpn_iface_bytes / is_vpn_iface_cstr is preserved, only the body now
delegates to the generated matches_vpn().
Cargo.lock files updated incidentally (synced with Cargo.toml versions
that were already 0.7.1 in the manifests).
Follow-up to #83. Five small fixes I caught while reviewing:
- build_lib: spell out the stdlib-only invariant in the module docstring.
build-version.py is called from app/build.gradle.kts on every Gradle
build, so adding pip/uv deps here would break the APK build.
- build_lib: drop unused get_python_exe() (no callers anywhere).
- build_lib: add version_sort_key() and use it in zygisk and kmod for
NDK / clang auto-detection. The previous lexicographic sorted() picked
the wrong directory when major versions span different digit widths
(e.g. 100.0.0 < 25.0.1, clang-r9 sorting after clang-r498344b).
zygisk/build-zip.sh used `sort -V` before the python port, so this is
a regression fix; kmod is a new safety net (DDK containers ship one
clang today, but auto-detect should still be correct).
- kmod/build-zip.py: drop the manual mtime check before `make strip`.
The check only watched vpnhide_kmod.c, so edits to the Makefile,
kernel headers, or .config wouldn't trigger a rebuild. Let make's own
dependency tracking decide.
- build_lib: minor cleanup — hoist `import subprocess` to the top of
the module instead of importing it inside get_build_version().
Quick uv-shebang script that hits the GitHub Releases API and prints a
per-asset download table for every release, plus a grand total. No new
deps in the repo — script declares its own (httpx + rich) inline.
Two changes that together eliminate changelog merge conflicts from
concurrent PRs:
1. **CHANGELOG.md is regenerated only by release.py.** The previous cut
still had every changelog.py invocation rewrite CHANGELOG.md with a
different [Unreleased] block, so two PRs producing different
unreleased content collided on the MD file. Checked-in CHANGELOG.md
now contains released versions only. Unreleased is rendered on
demand from changelog.d/ via scripts/preview-changelog.py — prints
to stdout, writes nothing.
2. **Fragment format: Markdown instead of TOML.** Filenames now look
like `<type>-<slug>-<hex4>.md` (e.g.
`fixed-dev-version-mismatch-a1b2.md`). Type is readable at-a-glance
in the directory listing; 4-char random hex prevents collision when
two PRs pick the same slug. Body is plain Markdown with `## English`
/ `## Русский` sections — renders directly on GitHub, no YAML/TOML
parser dependency.
- scripts/changelog_lib.py: MD parser replaces tomllib. render_full_md
drops the [Unreleased] block; write_md(data) signature simplified;
render_unreleased_md(fragments) for on-demand preview.
- scripts/changelog.py: writes <type>-<slug>-<hex4>.md, no MD regen.
- scripts/release.py: updated to the new write_md signature.
- scripts/preview-changelog.py: new.
- changelog.d/*.md: 10 existing TOML fragments migrated to MD. One
fragment (changelog-entries-now-live-as-per) updated to say Markdown
instead of TOML since that's the final state by the time this ships.
- CHANGELOG.md: regenerated — Unreleased block gone.
- .gitattributes: merge=union moved from *.toml to *.md.
- docs/changelog.md, docs/releasing.md, CONTRIBUTING.md,
changelog.d/README.md, CLAUDE.md: describe the new format + flow.
The JSON `unreleased` section was a conflict magnet — every PR touching
it collided with every other PR touching it. Fragments sidestep that:
each entry is its own TOML file, two PRs get two files, merges don't
touch the same bytes.
- changelog_lib.py: add FRAGMENTS_DIR, load_fragments(),
fragments_as_sections(), rotate_fragments_into_history();
render_full_md() now takes fragments explicitly.
- changelog.py: write a TOML fragment to changelog.d/<ts>-<slug>.toml
instead of appending to JSON. Auto-derive slug from EN text; accept
--slug override.
- release.py: load fragments, rotate into history[0], delete them.
- lsposed/app/src/main/assets/changelog.json: drop `unreleased` key;
only `history` remains (the 8 unreleased entries migrated to
changelog.d/*.toml). CHANGELOG.md regenerates byte-identical.
- UpdateChecker.kt: fix long-standing loadChangelog bug — was calling
parseChangelogEntry on the root object, which has no `version` field,
so the parse always threw and the function silently returned null.
Simplify ChangelogData to {history} and parse history[]; drop the
unused `current` field that DashboardScreen just concatenated back in.
- .gitattributes: mark changelog.d/*.toml as merge=union, belt-and-
suspenders fallback for same-filename collisions (rare — filenames
carry a timestamp).
- docs/changelog.md, docs/releasing.md, CONTRIBUTING.md,
changelog.d/README.md: describe the new flow.
Two tweaks driven by the same goal — make the artifact list on the CI
run page less ambiguous and give the release step a review gate.
- The APK artifact was named `vpnhide`, which blends in with the other
module-zip artifacts (`vpnhide-kmod-*`, `vpnhide-zygisk`,
`vpnhide-ports`). Rename to `vpnhide-apk` so every entry in the
Artifacts list names the thing you actually get when you download it.
- Release-on-tag job now creates a DRAFT GitHub release instead of
publishing directly. Gives a chance to eyeball the release notes and
attached binaries before they go public, and avoids racing
update-json.sh against the assets becoming reachable.
docs/releasing.md and the release.py post-run hints updated to reflect
the manual Publish step and the fact that update-json still has to
wait for the release to be *published*, not just drafted (draft
release assets sit behind auth).
Add scripts/build-version.sh — a single source of truth for the
effective version string:
* HEAD on tag vX.Y.Z -> "X.Y.Z"
* N commits past tag -> "X.Y.Z-N-gSHA"
* working tree dirty -> additional "-dirty" suffix
* no git / no matching tag -> VERSION file fallback
Wired into every packaging path:
* zygisk/build-zip.sh and portshide/build-zip.sh now stage a copy of
module/ and sed-patch `version=` in the staging copy, so committed
module.prop files stay at the last-released version.
* kmod/build-zip.sh now builds into a staging copy too.
* The kmod CI step runs build-version.sh and sed-patches module.prop
before zipping (git installed in the DDK container).
* lsposed/app/build.gradle.kts exec's build-version.sh at configure
time and assigns the result to `versionName` (versionCode stays
static, still bumped by release.py).
All actions/checkout@v6 gained `fetch-depth: 0` so git describe sees
the full tag history inside CI containers.
Result: a locally built or CI-from-main APK shows up in Android
Settings as e.g. `0.6.1-16-gf86e5e5`, and the zip inside carries the
same string in module.prop; the Magisk/KSU manager displays it in the
update list. Release tag builds are indistinguishable from before —
clean `X.Y.Z`. Diagnostic bug reports now carry the exact commit in
the App version line of device_info.txt.
Rework the changelog and release flow to remove the aspirational
top-level version that made it unclear whether new entries were
landing in an already-released section.
Schema change: `changelog.json` now has an explicit `unreleased`
object instead of hoisting the upcoming version to the top level. The
old `{version, sections, history}` layout becomes
`{unreleased, history}`, with the previously-released version moved
into `history[0]`.
New entries always go into `unreleased` via `changelog.py`. Releasing
is a single atomic operation (`release.py X.Y.Z`) that promotes
`unreleased` into `history[0]` with the target version number,
propagates the version to every source file, and regenerates the
markdown artifacts.
Script renames:
- `_changelog.py` → `changelog_lib.py` (no more underscore-prefixed
module that's imported by two siblings)
- `changelog-add.py` → `changelog.py`
- `update-version.py` → `release.py` (does more than just version
propagation — the name now reflects the full release action)
CHANGELOG.md rendering follows Keep a Changelog: a `## [Unreleased]`
block appears on top only when there are unreleased entries; the
update-json/changelog.md shown in Magisk/KSU popups still skips
Unreleased (only released versions make sense there).
Docs (docs/changelog.md, docs/releasing.md, CONTRIBUTING.md, CLAUDE.md)
updated with the new commands and the clarified model.
CLAUDE.md additionally gains a "read these before doing any work"
section that lists the contributor docs — so future sessions load the
workflow rules into context instead of skipping them as optional.
Split the generated markdown into two files:
- CHANGELOG.md at repo root — full history with the Keep a Changelog
header. Human-facing, discoverable from the GitHub repo page.
- update-json/changelog.md — still truncated to the last 5 versions,
for the Magisk/KSU update popup.
Both are regenerated from changelog.json on every changelog-add.py
and update-version.py run.
Also switch the CI release-notes extraction to read CHANGELOG.md so
the body is future-proof once a tag ages out of the short popup file.
Replace the bash update-version.sh with a python equivalent that also
rotates the changelog (previous top-level → history[0], new empty
top-level for the new VERSION) and regenerates update-json/changelog.md
from the JSON source of truth.
Add changelog-add.py for appending a bilingual entry (EN + RU) to the
upcoming version's section.
Both scripts are uv inline scripts; shared helpers live in _changelog.py.
Mirror the kmod/zygisk plumbing so KernelSU-Next / Magisk pick up
portshide updates automatically:
- scripts/update-version.sh bumps portshide/module/module.prop along
with the other modules when VERSION changes
- scripts/update-json.sh writes update-json/update-ports.json pointing
at the current release zip
- CI appends updateJson=.../update-ports.json to the portshide
module.prop before zipping, matching kmod/zygisk
- Dashboard reports portshide version mismatches as issues, with the
same up/down/different wording the other modules use
- scripts/update-version.sh — updates source files (module.prop, Cargo.toml, etc.)
- scripts/update-json.sh — generates Magisk/KSU update-json files
update-json must be committed AFTER the release is published to avoid
a race where Magisk sees a new version but the zip doesn't exist yet.
ensureSelfInTargets() and loadDashboardState() ran as independent
coroutines, so the dashboard could read selfNeedsRestart=false before
the init completed. On slower devices (Pixel 4a) this caused "Installed,
inactive" instead of the restart prompt.
- Make selfNeedsRestart nullable; show spinner until resolved
- Sync zygisk targets to module dir so next app fork picks them up
- Show "restart app to activate" instead of misleading "inactive"
- Add scripts/clean-device.sh for testing fresh installs via adb