ci+chore: add ruff (format + lint) for python scripts

Repo had ~1800 lines of Python (kmod/build.py, scripts/*, zygisk/build.py,
portshide/build-zip.py) with no formatter or linter. Long-lived scripts
like scripts/release.py and scripts/codegen-interfaces.py benefit from
catching unused-import / undefined-name / outdated-syntax issues early.

  pyproject.toml — ruff config, target-py312, line-length 100,
                   rules E F W I B UP SIM. Excludes zygisk/third_party,
                   target/, .claude/.
  ci.yml — astral-sh/ruff-action@v4 for `format --check` and `check`,
           ahead of the slow Rust/Gradle steps so it fails fast.
  docs/development.md — add `uvx ruff …` to the local-lint snippet.

Cleanup applied (`ruff format` + `ruff check --fix`):
  - reformat: kmod/build.py, scripts/{changelog_lib,codegen-interfaces,
    release,stats}.py, zygisk/build.py
  - I001: split multi-name imports onto separate lines after the
    sys.path.insert prelude (kmod/build.py, zygisk/build.py)
  - E501 manual: wrap one console.print line in scripts/release.py

Stdlib-only invariant from scripts/build_lib.py is preserved — ruff is
a dev/CI tool, not imported at runtime.
This commit is contained in:
okhsunrog 2026-04-26 23:48:37 +03:00
parent 9986100a77
commit 91013acb54
9 changed files with 70 additions and 33 deletions

View file

@ -38,6 +38,17 @@ jobs:
- name: Mark workspace safe
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
# Python lint + format (ruff). Fast (~100 ms on 1800 LoC) so it
# runs first — fails before the slow Rust/Gradle steps.
- name: ruff format
uses: astral-sh/ruff-action@v4
with:
args: format --check
- name: ruff check
uses: astral-sh/ruff-action@v4
with:
args: check
# Cache cargo deps + target dirs for clippy/test. Same key shape as
# zygisk + lsposed jobs — when those run on the same Cargo.lock the
# restore-keys fallback shares warm artifacts across jobs.

View file

@ -103,6 +103,10 @@ CI runs the same checks. See [.github/workflows/ci.yml](../.github/workflows/ci.
python3 scripts/codegen-interfaces.py
git diff --quiet # must be clean
# Python (ruff, config in pyproject.toml). uvx runs without installing anything global.
uvx ruff format --check
uvx ruff check
# Rust
cd zygisk && cargo fmt --check && cargo ndk -t arm64-v8a clippy -- -D warnings
cd ../lsposed/native && cargo fmt --check && cargo ndk -t arm64-v8a clippy -- -D warnings
@ -116,8 +120,7 @@ gcc -O2 -Wall -Werror -o /tmp/test_iface_lists kmod/test_iface_lists.c && /tmp/t
# Kotlin
ktlint "lsposed/**/*.kt"
cd lsposed && ./gradlew --no-daemon :app:lint
cd lsposed && ./gradlew --no-daemon :app:testDebugUnitTest
cd lsposed && ./gradlew :app:lint :app:testDebugUnitTest
```
## Build versions

View file

@ -45,8 +45,11 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
from build_lib import get_build_version, make_zip, version_sort_key # type: ignore[import-not-found]
from build_lib import ( # type: ignore[import-not-found]
get_build_version,
make_zip,
version_sort_key,
)
# Module file name on disk after `make`.
KMOD_KO = "vpnhide_kmod.ko"
@ -128,13 +131,9 @@ def native_build_one(
module_prop = staging / "module.prop"
content = module_prop.read_text(encoding="utf-8")
content = re.sub(
r"^version=.*", f"version=v{build_version}", content, flags=re.MULTILINE
)
content = re.sub(r"^version=.*", f"version=v{build_version}", content, flags=re.MULTILINE)
if re.search(r"^gkiVariant=", content, flags=re.MULTILINE):
content = re.sub(
r"^gkiVariant=.*", f"gkiVariant={kmi}", content, flags=re.MULTILINE
)
content = re.sub(r"^gkiVariant=.*", f"gkiVariant={kmi}", content, flags=re.MULTILINE)
else:
content = content.rstrip() + f"\ngkiVariant={kmi}\n"
update_json_url = (
@ -219,9 +218,7 @@ def find_runtime() -> tuple[str, bool]:
sys.exit(1)
def container_build_one(
runtime: str, is_podman: bool, repo_root: Path, kmi: str
) -> None:
def container_build_one(runtime: str, is_podman: bool, repo_root: Path, kmi: str) -> None:
image = f"ghcr.io/ylarod/ddk-min:{kmi}-{DDK_IMAGE_TAG}"
mount_spec = f"{repo_root}:/work"
cmd = [runtime, "run", "--rm"]
@ -314,10 +311,7 @@ def main() -> int:
parser.add_argument(
"--kdir",
type=str,
help=(
"Kernel source directory (overrides KDIR/KERNEL_SRC). Implies "
"native mode."
),
help=("Kernel source directory (overrides KDIR/KERNEL_SRC). Implies native mode."),
)
parser.add_argument(
"--clang-dir",

21
pyproject.toml Normal file
View file

@ -0,0 +1,21 @@
# Ruff config — applies to every .py in the repo.
# All our scripts are stdlib-only by policy (see scripts/build_lib.py
# header), so ruff is a dev/CI tool here, never a runtime dependency.
[tool.ruff]
target-version = "py312" # CI image (Ubuntu 24.04) ships Python 3.12
line-length = 100
extend-exclude = [
# third-party / vendored — not our code, don't lint
"zygisk/third_party",
# cargo build outputs that may contain stray .py files
"**/target",
# local-only worktrees / agent state, untracked
".claude",
]
[tool.ruff.lint]
# Conservative rule set: pycodestyle (E/W) + pyflakes (F) + isort (I)
# + bugbear foot-guns (B) + pyupgrade (UP) + simplifications (SIM).
# No pylint-style noise, no mypy.
select = ["E", "F", "W", "I", "B", "UP", "SIM"]

View file

@ -117,8 +117,8 @@ def parse_fragment(path: Path) -> dict:
if date_match.start() > en_match.start():
raise ValueError(f"{path.name}: date line must appear before the language sections")
en_body = text[en_match.end():ru_match.start()].strip()
ru_body = text[ru_match.end():].strip()
en_body = text[en_match.end() : ru_match.start()].strip()
ru_body = text[ru_match.end() :].strip()
if not en_body:
raise ValueError(f"{path.name}: empty English section")
if not ru_body:

View file

@ -464,7 +464,7 @@ def emit_rust(rules: list[Rule], tests: list[TestVector]) -> str:
expected = "true" if t.is_vpn else "false"
lines.append(
f" assert_eq!(matches_vpn({rust_byte_lit(t.name)}), {expected}, "
f"\"matches_vpn({t.name!r})\");"
f'"matches_vpn({t.name!r})");'
)
lines.append(" }")
lines.append("}")

View file

@ -114,8 +114,7 @@ def main() -> int:
for past in data.get("history", []):
if past.get("version") == version:
console.print(
f"[red]error:[/red] v{version} already exists in history[]. "
"Pick a new version.",
f"[red]error:[/red] v{version} already exists in history[]. Pick a new version.",
)
return 1
@ -169,11 +168,14 @@ def main() -> int:
console.print()
console.print("[bold]Next steps:[/bold]")
console.print(f" git commit -am \"chore: release v{version}\"")
console.print(f' git commit -am "chore: release v{version}"')
console.print(f" git tag v{version} && git push && git push origin v{version}")
console.print(" # CI builds artifacts and creates a DRAFT release — review on the Releases page, click Publish")
console.print(
" # CI builds artifacts and creates a DRAFT release — "
"review on the Releases page, click Publish"
)
console.print(" ./scripts/update-json.sh")
console.print(f" git commit -am \"chore: update-json for v{version}\"")
console.print(f' git commit -am "chore: update-json for v{version}"')
console.print(" git push")
return 0

View file

@ -22,9 +22,7 @@ def github_token() -> str | None:
if token := os.environ.get("GITHUB_TOKEN"):
return token
try:
out = subprocess.run(
["gh", "auth", "token"], capture_output=True, text=True, check=True
)
out = subprocess.run(["gh", "auth", "token"], capture_output=True, text=True, check=True)
return out.stdout.strip() or None
except (FileNotFoundError, subprocess.CalledProcessError):
return None
@ -34,9 +32,7 @@ headers = {"Accept": "application/vnd.github+json"}
if token := github_token():
headers["Authorization"] = f"Bearer {token}"
resp = httpx.get(
"https://api.github.com/repos/okhsunrog/vpnhide/releases", headers=headers
)
resp = httpx.get("https://api.github.com/repos/okhsunrog/vpnhide/releases", headers=headers)
resp.raise_for_status()
releases = resp.json()
@ -53,7 +49,13 @@ for release in releases:
total = sum(a["download_count"] for a in assets)
grand_total += total
table = Table(title=f"{release['tag_name']} ({total} downloads)", title_style="bold", show_header=False, box=None, padding=(0, 1))
table = Table(
title=f"{release['tag_name']} ({total} downloads)",
title_style="bold",
show_header=False,
box=None,
padding=(0, 1),
)
table.add_column("Asset", style="cyan")
table.add_column("Count", justify="right", style="yellow")
table.add_column("Bar", style="green", no_wrap=True)

View file

@ -18,7 +18,11 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
from build_lib import get_build_version, make_zip, version_sort_key # type: ignore[import-not-found]
from build_lib import ( # type: ignore[import-not-found]
get_build_version,
make_zip,
version_sort_key,
)
def main() -> int: