repo_contract: skip Flutter plugins

A plugin has android/ but no launcher activity. Also rename the sickdays desktop entry to the one-word form the label rule requires; that red gate blocked every pubspec commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWY1k4muZfdVJfb7YGJ
This commit is contained in:
Krzysztof Rudnicki 2026-11-02 10:32:47 +01:00
parent ae29e67e28
commit 5b36e84793
3 changed files with 44 additions and 11 deletions

View File

@ -1,7 +1,7 @@
"""Launcher rules for every Flutter app in a repo (kuhy, 2026-10-23). """Launcher rules for every Flutter app in a repo (kuhy, 2026-10-23).
Applies to each tracked `pubspec.yaml` whose `dependencies:` include the Applies to each tracked `pubspec.yaml` whose `dependencies:` include the
Flutter SDK and that has an `android/` directory beside it, at the repo root Flutter SDK (and that is not a `flutter: plugin:`) and that has an `android/` directory beside it, at the repo root
or nested (screen-locker's workout_app): or nested (screen-locker's workout_app):
1. the launcher name (`launcher_label.py`); 1. the launcher name (`launcher_label.py`);
@ -47,22 +47,36 @@ def _tracked(repo: Path, *patterns: str) -> list[PurePosixPath] | None:
return [PurePosixPath(p) for p in done.stdout.split("\0") if p] return [PurePosixPath(p) for p in done.stdout.split("\0") if p]
def _needs_flutter(pubspec: Path) -> bool: def _block(pubspec: Path, key: str) -> list[str]:
"""Whether the top-level `dependencies:` block pulls in the Flutter SDK.""" """Code lines (comments stripped) indented under top-level `key:`."""
try: try:
lines = pubspec.read_text(encoding="utf-8").splitlines() lines = pubspec.read_text(encoding="utf-8").splitlines()
except (OSError, UnicodeDecodeError): except (OSError, UnicodeDecodeError):
return False return []
inside = False inside, found = False, []
for line in lines: for line in lines:
code = line.split("#", 1)[0].rstrip() # comments never end a block code = line.split("#", 1)[0].rstrip() # comments never end a block
if not code.strip(): if not code.strip():
continue continue
if not code[0].isspace(): if not code[0].isspace():
inside = code == "dependencies:" inside = code == f"{key}:"
elif inside and _SDK.match(code): elif inside:
return True found.append(code)
return False return found
def _is_plugin(pubspec: Path) -> bool:
"""Whether the top-level `flutter:` block declares a `plugin:`.
A plugin keeps an `android/` directory but ships no launcher activity, so
the app rules (label, variants, icon) have nothing to check in it.
"""
return any(code.strip() == "plugin:" for code in _block(pubspec, "flutter"))
def _needs_flutter(pubspec: Path) -> bool:
"""Whether the top-level `dependencies:` block pulls in the Flutter SDK."""
return any(_SDK.match(code) for code in _block(pubspec, "dependencies"))
def flutter_apps(repo: Path) -> list[Path]: def flutter_apps(repo: Path) -> list[Path]:
@ -74,7 +88,12 @@ def flutter_apps(repo: Path) -> list[Path]:
if PRUNE.intersection(rel.parts): if PRUNE.intersection(rel.parts):
continue continue
app = repo / rel.parent app = repo / rel.parent
if (app / "android").is_dir() and _needs_flutter(app / "pubspec.yaml"): pubspec = app / "pubspec.yaml"
if (
(app / "android").is_dir()
and _needs_flutter(pubspec)
and not _is_plugin(pubspec)
):
apps.append(app) apps.append(app)
return apps return apps

View File

@ -13,6 +13,11 @@ from .flutter_app import flutter_app, track
from .pngkit import write_icon from .pngkit import write_icon
ICON = "assets/icon/icon_foreground.png" ICON = "assets/icon/icon_foreground.png"
PUBSPEC_PLUGIN = (
"dependencies:\n flutter:\n sdk: flutter\n"
"flutter:\n plugin:\n platforms:\n android:\n"
" pluginClass: P\n"
)
DESKTOP = "[Desktop Entry]\nType=Application\nName={name}\nName[pl]=Zadania pl\n" DESKTOP = "[Desktop Entry]\nType=Application\nName={name}\nName[pl]=Zadania pl\n"
@ -61,6 +66,15 @@ def test_not_flutter_or_no_android_skipped(repo: Path) -> None:
assert launcher.flutter_apps(repo) == [] assert launcher.flutter_apps(repo) == []
def test_flutter_plugin_is_not_an_app(repo: Path) -> None:
"""A plugin has an android/ dir but no launcher activity to check."""
body = PUBSPEC_PLUGIN
write(repo, "plug/pubspec.yaml", body)
write(repo, "plug/android/x", "")
track(repo)
assert launcher.flutter_apps(repo) == []
def test_pubspec_comments_do_not_hide_an_app(repo: Path) -> None: def test_pubspec_comments_do_not_hide_an_app(repo: Path) -> None:
body = "dependencies: # exact pins\n# column-0 note\n\n flutter:\n sdk: flutter # sdk\n" body = "dependencies: # exact pins\n# column-0 note\n\n flutter:\n sdk: flutter # sdk\n"
write(repo, "a/pubspec.yaml", body) write(repo, "a/pubspec.yaml", body)

View File

@ -1,6 +1,6 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=Sick days Name=Sickdays
Comment=Mark sick days and choose which lockers still apply Comment=Mark sick days and choose which lockers still apply
Exec=sickdays gui Exec=sickdays gui
Icon=face-sick Icon=face-sick