chore: clean up code comments

This commit is contained in:
edde746
2026-08-10 20:28:41 +02:00
parent 5611c6785a
commit 69fadc220d
170 changed files with 324 additions and 1765 deletions
+5 -10
View File
@@ -15,9 +15,8 @@ FLUTTER_COMMIT = "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
if len(sys.argv) > 2:
raise SystemExit(f"Usage: {Path(sys.argv[0]).name} [workflow-path]")
WORKFLOW = Path(sys.argv[1]).resolve() if len(sys.argv) == 2 else DEFAULT_WORKFLOW
# The shared bootstrap both windows-arm jobs call, and the pins it must keep.
# Resolved beside the workflow rather than from ROOT so that checking a fixture
# tree exercises this rule instead of silently re-reading the real action.
# Resolve the shared bootstrap beside the workflow so fixture checks use their
# local action rather than the checkout's real action.
SETUP_FLUTTER_GIT = WORKFLOW.parents[1] / "actions/setup-flutter-git/action.yml"
text = WORKFLOW.read_text(encoding="utf-8")
errors: list[str] = []
@@ -168,9 +167,7 @@ require(bool(setup_flutter_git), "missing .github/actions/setup-flutter-git/acti
for expected in (
f'$version = "{FLUTTER_VERSION}"',
f'$expectedCommit = "{FLUTTER_COMMIT}"',
# Fetch the release tag rather than the bare commit: the commit is only
# reachable through the tag, and the tag is what makes the SDK report its
# own version. Both halves are then verified, so a moved tag fails the job.
# Fetch and verify the release tag so moved tags cannot change the SDK.
'git -C $root fetch --depth 1 origin "refs/tags/${version}:refs/tags/${version}"',
'git -C $root checkout --detach "refs/tags/$version"',
"$actualCommit = git -C $root rev-parse HEAD",
@@ -336,10 +333,8 @@ require(
"every Flutter SDK cache must define its trusted cache key",
)
# check_workflow_action_pins.py owns the SHA-pin rule for every workflow, this
# one included; build.yml only adds the credential invariant on top, because it
# is workflow_dispatch-only and so escapes the pull-request rule in
# check_workflow_security.py.
# Action-pin checks run elsewhere; this guard adds the checkout credential
# invariant for the workflow-dispatch-only build.
remote_actions = [
reference.rpartition("@")[0]
for _, reference in iter_uses_references(text)
+5 -17
View File
@@ -40,11 +40,7 @@ _UI_ARGUMENT_RE = re.compile(r"(?<![A-Za-z0-9_])(?:" + "|".join(_UI_ARGUMENTS) +
_TEXT_RE = re.compile(r"(?:\bText|\bSelectableText)\s*\(\s*$")
_EXCLUDED_ARGUMENT_RE = re.compile(r"(?:debugLabel|fontFamily)\s*:\s*$")
_KEY_RE = re.compile(r"(?:\bKey|\bValueKey)\s*\(\s*$")
# `createdLog:`/`log:` name a diagnostic sink, never rendered copy. Matched
# unanchored because the literal is usually behind a lambda
# (`createdLog: (playlist) => 'Created ...'`), so the argument name is not
# the text immediately preceding the literal. The `[a-z]Log` arm keeps
# innocent names such as `catalog:` out of the exclusion.
# `log:`/`createdLog:` are diagnostic sinks, including inside lambdas.
_DIAGNOSTIC_RE = re.compile(
r"(?:\bappLogger\.|\bSentry\.|\bassert\s*\(|\bthrow\b|(?:\blog|[a-z]Log)\s*:)"
)
@@ -52,17 +48,9 @@ _TRANSLATION_INTERPOLATION_RE = re.compile(
r"\$\{\s*(?:t\.|context\.t\b|Translations\.of\s*\()"
)
_T_PARAMETER_RE = re.compile(r"(?:\(\s*t\s*\)|\bt)\s*=>[^;]*$")
# Rule 4 support: a literal bound to a name or returned, rather than handed
# straight to a widget. This is the shape issue #1856 actually had --
# `baseButtonText = 'Skip Intro';` a few lines above the `Text(buttonText)`
# that renders it -- which rules 1-3 structurally cannot see.
#
# Restricted to phrase-shaped literals: at least two letter-words separated by
# whitespace. Without that restriction the rule cannot tell display copy from
# the identifier strings this codebase binds constantly ('cast_row', 'auto',
# 'liveTv', 'HDR_UNSUPPORTED'), and it drowns in false positives. The cost is
# that a bound single-word label ('Software', 'Stereo') slips through; those
# are indistinguishable from an identifier without dataflow analysis.
# Rule 4 catches phrases assigned or returned before a widget renders them.
# Restrict it to multi-word phrases to avoid confusing identifiers with UI copy;
# single-word labels remain indistinguishable without dataflow analysis.
_PHRASE_RE = re.compile(r"[A-Za-z]\s+[A-Za-z]")
_BOUND_LITERAL_RE = re.compile(r"(?:\breturn|=>|(?<![=!<>+\-*/%&|^~])=)\s*$")
_RENDERS_UI_RE = re.compile(
@@ -326,7 +314,7 @@ def scan(
seen_literals: dict[str, set[str]] = {}
for path in sorted(lib_dir.rglob("*.dart")):
relative = path.relative_to(root).as_posix()
# lib/dev is a separate measurement entrypoint, explicitly "NOT part of the app".
# lib/dev is a separate, non-app measurement entrypoint.
if (
relative.startswith(("lib/i18n/", "lib/dev/"))
or path.name.endswith((".g.dart", ".freezed.dart"))
+18 -64
View File
@@ -1,32 +1,9 @@
#!/usr/bin/env python3
"""Guard the Linux package dependency lists against what the runner links.
"""Guard distro dependencies for host libraries linked by the Linux runner.
linux/packaging/bundle-libs.sh deliberately refuses to bundle the display- and
driver-coupled libraries (libEGL, libwayland-*, libGL, libdrm ...): they must
come from the host or the app will not talk to the compositor it is running
under. That makes them the package manager's problem, and the depends lists in
linux/packaging/build-packages.py are maintained by hand.
Nothing connected the two. Adding a pkg-config link to the runner produced a
binary with an undeclared shared-library dependency, and the failure surfaces
only on a user's machine at exec time - a class of bug no compile or unit test
can reach. This walks the runner's own link line instead:
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::WAYLAND_EGL)
-> pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
-> RUNTIME_PACKAGES["wayland-egl"] -> libwayland-egl1 / libwayland-egl / wayland
and requires every distro to declare it. A new pkg-config module fails here
until its runtime package names are named for all three.
What it walks is exactly CMAKE_FILES, the three CMakeLists.txt this checkout
owns - and nothing else. The Flutter plugins link into the same binary from
linux/flutter/generated_plugins.cmake, whose add_subdirectory() targets live
under flutter/ephemeral/.plugin_symlinks/, a directory that only exists after
`flutter pub get`. A plugin's own pkg_check_modules is therefore unreadable at
pull-request time, and a plugin that starts linking a new host library passes
here. linux/packaging/check-bundle-host-deps.py is what covers that: it runs
ldd over the built bundle, where every link edge is finally real.
The runner's CMake link graph maps through pkg-config modules to the hand-maintained
package lists. This catches undeclared runtime libraries before they fail on users'
machines; the built-bundle check covers plugin links unavailable before `pub get`.
"""
from pathlib import Path
@@ -43,31 +20,24 @@ LINUX = Path(sys.argv[1]).resolve() if len(sys.argv) == 2 else ROOT / "linux"
RUNNER_CMAKE = LINUX / "runner/CMakeLists.txt"
PACKAGES_PY = LINUX / "packaging/build-packages.py"
BUNDLE_SH = LINUX / "packaging/bundle-libs.sh"
# pkg_check_modules for targets the runner links may live in any of these.
# Runner-linked pkg_check_modules may be declared in any of these files.
CMAKE_FILES = (RUNNER_CMAKE, LINUX / "CMakeLists.txt", LINUX / "flutter/CMakeLists.txt")
# pkg-config modules whose library ships *inside* the package instead of being
# depended on. libmpv is pinned and Wayland-enabled because the video plane needs
# it to be; a distro libmpv silently drops hwdec to vaapi-copy. Bundling it means
# there is deliberately no runtime dependency to find, so the walk must not
# demand one - but the libraries it links that bundle-libs.sh excludes still have
# to be declared, which the packaging job re-derives from the built bundle.
# Bundled modules have no host dependency; their excluded runtime links are
# checked separately against the built bundle.
BUNDLED_MODULES = {"mpv"}
# pkg-config module -> the package that ships its runtime library, per distro.
# Only modules the runner actually links are consulted, so an unused entry here
# is harmless; a missing one is an error.
# pkg-config module -> runtime package name for each distro.
RUNTIME_PACKAGES = {
"gtk+-3.0": {"deb": "libgtk-3-0", "rpm": "gtk3", "pacman": "gtk3"},
"epoxy": {"deb": "libepoxy0", "rpm": "libepoxy", "pacman": "libepoxy"},
# Reached through the `flutter` INTERFACE target rather than named by the
# runner, which is why the graph has to cross file boundaries to see them.
# Reached through the `flutter` INTERFACE target.
"glib-2.0": {"deb": "libglib2.0-0", "rpm": "glib2", "pacman": "glib2"},
"gio-2.0": {"deb": "libglib2.0-0", "rpm": "glib2", "pacman": "glib2"},
"wayland-client": {
"deb": "libwayland-client0",
"rpm": "libwayland-client",
# Arch ships every libwayland-* in the one `wayland` package.
# Arch ships all libwayland-* libraries in `wayland`.
"pacman": "wayland",
},
"wayland-egl": {
@@ -75,7 +45,7 @@ RUNTIME_PACKAGES = {
"rpm": "libwayland-egl",
"pacman": "wayland",
},
# libglvnd is the vendor-neutral dispatch that provides libEGL.so.1.
# libglvnd provides libEGL.so.1.
"egl": {"deb": "libegl1", "rpm": "libglvnd-egl", "pacman": "libglvnd"},
}
@@ -95,9 +65,8 @@ def read(path: Path) -> str:
return ""
# Keywords that carry no target name.
LINK_KEYWORDS = {"PRIVATE", "PUBLIC", "INTERFACE", "optimized", "debug", "general"}
# Options CMake accepts between IMPORTED_TARGET and the module names, in any order.
# Link keywords and pkg_check_modules options are not module names.
PKG_OPTIONS = ("REQUIRED", "QUIET", "GLOBAL", "NO_CMAKE_PATH", "NO_CMAKE_ENVIRONMENT_PATH")
@@ -167,19 +136,11 @@ def pkgconfig_modules() -> dict[str, list[str]]:
options = "|".join(PKG_OPTIONS)
for path in CMAKE_FILES:
for match in re.finditer(
# The options may precede the module names, so skip any run of them
# rather than taking the first token and reporting `REQUIRED` as a
# package nobody ships. The tail is then every remaining token up to
# the closing paren.
# Skip options before module names.
r"pkg_check_modules\(\s*(\w+)\b[^)]*?IMPORTED_TARGET\s+((?:(?:" + options + r")\s+)*[^)]*)\)",
strip_comments(read(path)),
):
# A moduleSpec is `<name>` or `<name><op><version>`, so the version
# constraint has to come off before the name is looked up - otherwise
# a perfectly legal `mpv>=0.40` is reported as a package nobody
# ships, and the message sends whoever hits it off to invent a
# RUNTIME_PACKAGES entry for it. Pinning that minimum is a plausible
# next edit here: target-colorspace-hint=auto needs mpv 0.40.
# Strip version constraints before package lookup.
names = [
re.split(r"[<>=!]", t, maxsplit=1)[0] for t in match.group(2).split() if t not in PKG_OPTIONS
]
@@ -203,12 +164,8 @@ def declared_depends() -> dict[str, list[str]]:
return {}
# Every file, not just the runner's: `flutter` is defined in flutter/CMakeLists.txt
# and propagates GTK, GLIB and GIO to whatever links it, so a graph built from one
# file treats it as a leaf and never sees them. A member that moved or was renamed
# is fatal rather than a smaller walk: its modules drop out of the graph, a
# declaration deleted alongside it goes unreported, and every check below then
# passes over a tree nobody actually looked at.
# Include every owned CMake input; missing files fail closed instead of shrinking
# the graph and silently passing.
absent = [path for path in CMAKE_FILES if not path.is_file()]
if absent:
for path in absent:
@@ -225,8 +182,7 @@ depends = declared_depends()
require(bool(depends), "no distro depends lists were found, so nothing was checked")
# The exclusion list is what makes declaring these mandatory rather than
# optional. If bundling ever starts covering them, this guard is the wrong shape.
# These libraries must remain host-provided; bundling them would invalidate this guard.
bundle = read(BUNDLE_SH)
for pattern in (r"libEGL\.so", r"libwayland.*\.so"):
require(
@@ -253,9 +209,7 @@ for target in sorted(linked):
for module in target_modules:
checked_modules += 1
if module in BUNDLED_MODULES:
# Shipped inside the package, so there is no dependency to find. Still
# counted, so the summary keeps naming everything the walk reached and
# a module going missing is a drop rather than a silent skip.
# Bundled modules have no host dependency but remain part of the summary.
continue
packages = RUNTIME_PACKAGES.get(module)
if packages is None:
+4 -8
View File
@@ -24,11 +24,9 @@ PROGUARD_RULES = Path("android/app/proguard-rules.pro")
APP_JAVA_ROOT = Path("android/app/src/main/java")
CPP_ROOT = Path("android/app/src/main/cpp")
NATIVE_SUFFIXES = {".c", ".cc", ".cpp", ".h", ".hpp"}
# Namespaces the app borrows from a dependency purely so that dependency can reflect on
# them. A class under one of these has no direct caller by construction.
# Dependency namespaces reached only through reflection have no direct callers.
REFLECTED_NAMESPACES = ("androidx/media3/",)
# Framework types live on the bootclasspath, never in the app's dex, so R8 cannot rename
# them and they need no keep.
# Bootclasspath framework types are not in the app dex and need no keep rule.
PLATFORM_PREFIXES = ("java.", "javax.", "android.")
_STRING_LITERAL = re.compile(r'"((?:[^"\\]|\\.)*)"')
@@ -42,9 +40,7 @@ _MEMBER_LOOKUP = re.compile(
+ r"\s*\)"
)
_DESCRIPTOR_CLASS = re.compile(r"L([\w/$]+);")
# Only -keep and -keepclasseswithmembers protect a class from both shrinking and
# renaming. -keepclassmembers/-keepclassmembernames cover members alone, and the
# -keepnames family allows shrinking, so none of them save a class nothing references.
# Only -keep variants without allowshrinking/allowobfuscation keep classes and names.
_KEEP = re.compile(
r"^-(?:keep|keepclasseswithmembers)((?:\s*,\s*\w+)*)\s+(?:class|interface|enum)\s+(\S+)"
r"(?:\s*\{(.*?)\})?",
@@ -62,7 +58,7 @@ class Keep:
self.members = members
self._regex = re.compile(
"".join(
# ** spans package separators, * does not, ? is a single character.
# ** crosses package separators; * and ? match within a segment.
{"**": r".*", "*": r"[^.]*", "?": r"."}.get(token, re.escape(token))
for token in re.findall(r"\*\*|[*?]|[^*?]+", pattern)
)
+4 -7
View File
@@ -40,8 +40,7 @@ require(
)
iss = template()
# The script used to carry two near-identical copies of the whole .iss, one per
# architecture shape. Anything that appears twice again has drifted apart.
# A single template prevents architecture copies from drifting.
for once in (
r"^\[Setup\]$",
r"^\[Code\]$",
@@ -76,9 +75,7 @@ require(
"the dual-architecture [Files] entries must keep their architecture checks",
)
# A fresh install stays per-user and prompts for nothing; only an existing
# machine-wide copy pulls in elevation, and only via /ALLUSERS, which Inno
# ignores unless the commandline override is allowed.
# Fresh installs stay per-user; only /ALLUSERS may trigger elevation.
require(
re.search(r"(?m)^PrivilegesRequired=lowest\s*$", iss) is not None,
"a fresh install must stay per-user; PrivilegesRequired=lowest",
@@ -93,7 +90,7 @@ require(
"allowing dialog makes a silent install with no previous copy prompt; winget installs that way",
)
# The elevation path itself.
# Verify the elevation path.
require(
"IsAdminInstallMode" in iss,
"the elevation path must be skipped once Setup already runs in administrative install mode",
@@ -125,7 +122,7 @@ require(
"a refused elevation must explain itself instead of failing silently",
)
# Behavior other tooling already depends on.
# Preserve behavior required by release tooling.
require(
"{param:WINGET|0}" in iss and "{app}\\.winget" in iss,
"the winget marker file gates UpdateService.useNativeUpdater",
+12 -31
View File
@@ -36,18 +36,14 @@ PUBSPEC = ROOT / "pubspec.yaml"
WORKFLOW = ROOT / ".github/workflows/build.yml"
MSIX_STEP = "Build Store package (MSIX)"
BUNDLE = "plezy-windows.msixbundle"
# The identity reserved in Partner Center. All three are pinned here because a
# character of drift in any of them fails Store validation, and the first two
# derive the package family name edde746.Plezy_13q3sv6jzathm that installed
# copies are keyed by.
# Partner Center reserves these identity values and derives the package family name.
IDENTITY_NAME = "edde746.Plezy"
PUBLISHER = "CN=AA9C53CB-AD3C-48DA-B3E3-D1E8986D4E25"
PUBLISHER_DISPLAY_NAME = "edde746"
PACKAGE_FAMILY_SUFFIX = "13q3sv6jzathm"
FOUNDATION = "http://schemas.microsoft.com/appx/manifest/foundation/windows10"
ASSET_REFERENCE = re.compile(r"assets\\([A-Za-z0-9._-]+\.png)")
# Package's child order is fixed by the foundation schema. A manifest may use a
# subset of these, but never a different order.
# Package child order is fixed by the foundation schema.
SCHEMA_ORDER = (
"Identity",
"PhoneIdentity",
@@ -68,12 +64,10 @@ REQUIRED_ELEMENTS = (
"Applications",
)
REQUIRED_CAPABILITIES = ("runFullTrust", "internetClient", "privateNetworkClientServer")
# Certification requires these three. The optional tile and splash assets are
# only checked once referenced, since dropping them is a legitimate choice.
# Certification requires these capabilities; optional assets are checked only when referenced.
REQUIRED_ASSETS = ("StoreLogo.png", "Square150x150Logo.png", "Square44x44Logo.png")
# Normalized so that every pattern below can anchor on \n and $ regardless of
# whether this checkout stores the PowerShell scripts with CRLF endings.
# Normalize line endings so subsequent patterns are portable.
text = SCRIPT.read_text(encoding="utf-8").replace("\r\n", "\n")
errors: list[str] = []
@@ -144,8 +138,7 @@ require(
prelude = declarations()
manifest_template = template()
# build-installer.ps1 once carried one whole .iss per architecture shape and the
# copies drifted apart. Anything that appears twice here has drifted too.
# A single manifest template prevents architecture copies from drifting.
for once in (
r'^ return @"$',
r"^<Package ",
@@ -158,9 +151,7 @@ for once in (
f"{once} must match exactly one line; a second copy of the template will drift",
)
# The two values the caller supplies, plus every identity string the function
# declares. Together they must cover the whole template, so an interpolation
# added there has to be declared here before it can pass.
# Caller parameters and declared identity values must cover every interpolation.
for parameter in ("MsixVersion", "Architecture"):
require(
f"[Parameter(Mandatory)][string]${parameter}" in prelude,
@@ -198,8 +189,7 @@ if package is not None:
if identity is None:
require(False, "the manifest must declare an Identity element")
else:
# Both patterns are schema constraints: makeappx refuses the package
# before reading a single payload file when either is violated.
# makeappx rejects either schema violation before reading payloads.
require(
re.fullmatch(r"[-.A-Za-z0-9]{3,50}", identity.get("Name") or "") is not None,
"Identity/@Name must match the schema's [-.A-Za-z0-9]+ pattern; an underscore "
@@ -263,8 +253,7 @@ if package is not None:
"a packaged Win32 app must enter through Windows.FullTrustApplication",
)
# Assets are named in attributes (the tile logos) and in element text (the
# Properties/Logo), so both are scanned.
# Asset paths appear in attributes and element text.
referenced = {
match.group(1)
for element in package.iter()
@@ -280,9 +269,7 @@ if package is not None:
"tree; packaging fails on a missing asset",
)
# Independent of the manifest: Partner Center reports the package family name,
# so recomputing it from the pinned publisher proves that string is byte-exact.
# A mistyped publisher otherwise only surfaces as a rejected upload.
# Recompute the Partner Center package-family suffix to catch publisher drift.
require(
package_family_suffix(PUBLISHER) == PACKAGE_FAMILY_SUFFIX,
f"the pinned publisher must hash to the package family name reported by Partner "
@@ -314,8 +301,7 @@ require(
"a version that is not major.minor.patch must be rejected instead of packaged",
)
# Native exit codes are invisible to $ErrorActionPreference, so the one
# invocation that checks $LASTEXITCODE has to be the only one.
# PowerShell does not surface native exit codes; centralize the $LASTEXITCODE check.
require(
len(re.findall(r"(?m)^ & \$Tool @Arguments$", text)) == 1
and "& $MakeAppx" not in text
@@ -324,11 +310,7 @@ require(
"every SDK tool call must go through the single invocation that checks $LASTEXITCODE",
)
# The taskbar, task view and Alt-Tab draw the small logo on a plate filled with
# BackgroundColor, and the manifest's transparent background leaves the shell
# painting the user's accent colour behind the icon. Only an altform-unplated
# variant suppresses that plate, and qualified variants resolve solely through
# resources.pri - as plain payload files they are inert.
# Unplated variants prevent the shell's accent-colored plate; resources.pri resolves them.
for form in ("altform-unplated", "altform-lightunplated"):
require(
any(ASSETS.glob(f"Square44x44Logo.targetsize-*_{form}.png")),
@@ -351,8 +333,7 @@ require(
"fails publisher-identity validation",
)
# The version the manifest carries is passed in by the workflow, so the link
# back to pubspec.yaml lives there rather than in the script.
# The workflow supplies the manifest version from pubspec.yaml.
workflow = WORKFLOW.read_text(encoding="utf-8")
package_windows = job_block(workflow, "package-windows")
require(bool(package_windows), "missing package-windows job")
+2 -16
View File
@@ -1,12 +1,8 @@
#!/usr/bin/env bash
set -uo pipefail
# Git sets GIT_DIR (and friends) for hook invocations. Inside `flutter pub
# run`, that leaks into Flutter's own SDK-version probe (`git describe` from
# Flutter's checkout) and makes Flutter misreport its version as
# `1.35.1-0.0.pre-1`, which then fails dependency resolution. Strip those
# vars so the script behaves the same when invoked from a hook as it does
# from a plain shell.
# Hook-invoked Flutter commands inherit GIT_* variables and can misreport the SDK
# version; clear them so hooks and direct invocations behave identically.
unset GIT_DIR GIT_INDEX_FILE GIT_WORK_TREE GIT_PREFIX
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -37,7 +33,6 @@ have_dart_code_linter() {
FAILED=0
# 1. dart format (mirrors ci.yml "Verify formatting")
section "dart format"
files=()
while IFS= read -r -d '' f; do files+=("$f"); done < <(
@@ -59,7 +54,6 @@ else
rm -f "$out"
fi
# 2. Codegen freshness
section "codegen freshness"
out="$(mktemp)"
if scripts/codegen.sh --check >"$out" 2>&1; then
@@ -71,7 +65,6 @@ else
fi
rm -f "$out"
# 3. Translation hygiene
section "translation hygiene"
if python3 scripts/clean_translations.py --check --strict; then
ok "locale files normalized and no unused keys found"
@@ -80,7 +73,6 @@ else
FAILED=1
fi
# 4. Hardcoded user-facing strings
section "hardcoded UI strings"
if python3 scripts/check_hardcoded_strings.py; then
ok "user-facing strings use the translation layer"
@@ -89,7 +81,6 @@ else
FAILED=1
fi
# 5. Workflow and script regression guards
section "workflow and script guards"
if bash scripts/ci_guard_checks.sh; then
ok "workflow and script guards passed"
@@ -98,7 +89,6 @@ else
FAILED=1
fi
# 6. Icon consistency
section "icon consistency"
if dart run scripts/check_icon_consistency.dart; then
ok "production icons use AppIcon and rounded Symbols"
@@ -107,7 +97,6 @@ else
FAILED=1
fi
# 7. Native formatting
section "native format"
out="$(mktemp)"
if scripts/format_native.sh --check >"$out" 2>&1; then
@@ -119,7 +108,6 @@ else
fi
rm -f "$out"
# 8. Dart analyzer (mirrors ci.yml "Analyze code")
section "Dart analyzer"
if dart run scripts/check_analyzer.dart; then
ok "no unapproved diagnostics"
@@ -128,7 +116,6 @@ else
FAILED=1
fi
# 9. Unused code (mirrors ci.yml "Check for unused code")
section "dart_code_linter: unused code"
if ! have_dart_code_linter; then
skip "dart_code_linter unresolved — run 'flutter pub get'"
@@ -145,7 +132,6 @@ else
rm -f "$out"
fi
# 10. Unused files (mirrors ci.yml "Check for unused files")
section "dart_code_linter: unused files"
if ! have_dart_code_linter; then
skip "dart_code_linter unresolved — run 'flutter pub get'"
+4 -11
View File
@@ -1,15 +1,8 @@
#!/usr/bin/env bash
# Workflow and script regression guards.
#
# Single source of truth for the guard roster, shared by the "Verify workflow
# and script guards" step in .github/workflows/ci.yml and section 4 of
# scripts/ci_checks.sh. The checkers are named explicitly because a few of them
# belong to other jobs (check_bun_audit.py needs Bun, check_codegen.py runs via
# codegen.sh, and linux/packaging/check-bundle-host-deps.py and
# check-package-deps.py need a built bundle and built packages, so both run in
# the linux-packages job and again in the release build), but their regression
# tests are discovered by glob so a newly added scripts/test_*.py is picked up
# automatically instead of having to be remembered in two places.
# Single source of truth for workflow/script guards, shared by CI and
# scripts/ci_checks.sh. Regression tests use a glob so new test_checkers are
# picked up automatically; checkers requiring Bun or built packages run in
# their owning jobs as well.
set -euo pipefail
shopt -s nullglob
+5 -21
View File
@@ -1,23 +1,18 @@
#!/usr/bin/env bash
# Android Icon Generator Script
# Generates notification icons and monochrome launcher icons from SVG source
# Usage: ./generate_android_icons.sh
# Generate Android notification and monochrome icons from assets/plezy.svg.
set -e
# Configuration
SVG_SOURCE="assets/plezy.svg"
ANDROID_RES="android/app/src/main/res"
TEMP_DIR="/tmp/android_icons_$$"
# Check if source SVG exists
if [ ! -f "$SVG_SOURCE" ]; then
echo "Error: $SVG_SOURCE not found"
exit 1
fi
# Check for required tools
if ! command -v rsvg-convert &> /dev/null; then
echo "Error: rsvg-convert not found. Install with: brew install librsvg"
exit 1
@@ -37,27 +32,24 @@ if ! command -v bc &> /dev/null; then
exit 1
fi
# Create temp directory
mkdir -p "$TEMP_DIR"
echo "🎨 Generating Android icons from $SVG_SOURCE..."
# Function to generate white silhouette icon
generate_white_icon() {
local size=$1
local output=$2
# Get SVG dimensions to detect if it's non-square
# Read dimensions so non-square artwork can be padded.
local svg_viewbox=$(grep -o 'viewBox="[^"]*"' "$SVG_SOURCE" | sed 's/viewBox="//;s/"//')
local svg_width=$(echo "$svg_viewbox" | awk '{print $3}')
local svg_height=$(echo "$svg_viewbox" | awk '{print $4}')
# Convert SVG to PNG, preserving aspect ratio
# Preserve the source aspect ratio.
if (( $(echo "$svg_width != $svg_height" | bc -l) )); then
# Non-square SVG: render at size and add padding to center it
# Center non-square artwork on a transparent square canvas.
rsvg-convert --keep-aspect-ratio --background-color=transparent "$SVG_SOURCE" -o "$TEMP_DIR/temp_unpadded.png"
# Center the image in a square canvas with transparent padding
"$IMAGEMAGICK" "$TEMP_DIR/temp_unpadded.png" \
-resize "${size}x${size}" \
-gravity center \
@@ -65,11 +57,10 @@ generate_white_icon() {
-extent "${size}x${size}" \
"$TEMP_DIR/temp.png"
else
# Square SVG: convert directly
rsvg-convert -w "$size" -h "$size" --background-color=transparent "$SVG_SOURCE" -o "$TEMP_DIR/temp.png"
fi
# Convert to white silhouette: extract alpha, fill with white, apply alpha
# Convert the source alpha channel to a white silhouette.
"$IMAGEMAGICK" "$TEMP_DIR/temp.png" \
-alpha extract \
"$TEMP_DIR/alpha_mask.png"
@@ -85,12 +76,9 @@ generate_white_icon() {
echo " ✓ Generated $(basename $output) (${size}x${size}px)"
}
# Generate Notification Icons (24dp base)
echo ""
echo "📱 Generating notification icons (ic_stat_notification.png)..."
# Notification icon densities: 24dp base
# mdpi=1x, hdpi=1.5x, xhdpi=2x, xxhdpi=3x, xxxhdpi=4x
declare -A NOTIF_SIZES=(
["mdpi"]=24
["hdpi"]=36
@@ -106,12 +94,9 @@ for density in "${!NOTIF_SIZES[@]}"; do
generate_white_icon "$size" "$output_dir/ic_stat_notification.png"
done
# Generate Monochrome Launcher Icons (108dp base)
echo ""
echo "🚀 Generating monochrome launcher icons (ic_launcher_monochrome.png)..."
# Monochrome launcher icon densities: 108dp base
# mdpi=1x, hdpi=1.5x, xhdpi=2x, xxhdpi=3x, xxxhdpi=4x
declare -A MONO_SIZES=(
["mdpi"]=108
["hdpi"]=162
@@ -127,7 +112,6 @@ for density in "${!MONO_SIZES[@]}"; do
generate_white_icon "$size" "$output_dir/ic_launcher_monochrome.png"
done
# Clean up
rm -rf "$TEMP_DIR"
echo ""
+6 -20
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env pwsh
# Windows MSIX Asset Generator
# Renders the tile, store and splash PNGs the Store manifest references from
# assets/plezy.png, for windows/msix/assets. The results are committed and
# windows/build-msix.ps1 only copies them, so packaging stays runnable without
# image tooling; regenerating is only needed when the app icon changes.
# Generate committed MSIX assets from assets/plezy.png.
$ErrorActionPreference = "Stop"
@@ -16,9 +12,7 @@ Add-Type -AssemblyName System.Drawing
$Source = Resolve-Path "assets\plezy.png"
$OutputDir = Join-Path $ProjectRoot "windows\msix\assets"
# Sizes and names come from the uap:VisualElements attributes in the manifest
# template; a rename here has to be made there too. The two non-square targets
# letterbox the square icon on transparent padding rather than stretching it.
# Names and dimensions mirror the manifest's VisualElements.
$Targets = @(
@{ Name = "Square44x44Logo.png"; Width = 44; Height = 44 }
@{ Name = "Square150x150Logo.png"; Width = 150; Height = 150 }
@@ -26,25 +20,19 @@ $Targets = @(
@{ Name = "Wide310x150Logo.png"; Width = 310; Height = 150 }
@{ Name = "Square310x310Logo.png"; Width = 310; Height = 310 }
@{ Name = "SplashScreen.png"; Width = 620; Height = 300 }
# The base logos are scale-100. Without a 200 the shell upscales them on the
# high-DPI displays most laptops ship with.
# Scale-200 variants avoid shell upscaling on high-DPI displays.
@{ Name = "Square44x44Logo.scale-200.png"; Width = 88; Height = 88 }
@{ Name = "Square150x150Logo.scale-200.png"; Width = 300; Height = 300 }
)
# The taskbar, task view and Alt-Tab draw the small logo on a plate filled with
# BackgroundColor, and a transparent background leaves the shell painting the
# user's accent colour behind the icon. An altform-unplated variant is the only
# way to suppress that plate; lightunplated is its light-theme counterpart.
# These resolve through resources.pri, which windows/build-msix.ps1 builds - as
# plain payload files they are inert.
# Resources.pri resolves these unplated variants; plain payload files are inert.
foreach ($Size in 16, 24, 32, 48, 256) {
foreach ($Form in "", "_altform-unplated", "_altform-lightunplated") {
$Targets += @{ Name = "Square44x44Logo.targetsize-${Size}${Form}.png"; Width = $Size; Height = $Size }
}
}
# Regenerate from scratch so a renamed target cannot leave an orphan behind.
# Remove stale outputs when targets are renamed.
if (Test-Path $OutputDir) { Remove-Item (Join-Path $OutputDir "*.png") -Force }
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
@@ -58,9 +46,7 @@ try {
try {
$Graphics = [System.Drawing.Graphics]::FromImage($Canvas)
try {
# SourceCopy keeps the icon's own alpha instead of blending it
# into the canvas; the manifest declares a transparent tile
# background and expects the padding to stay transparent.
# Preserve alpha for the manifest's transparent tile background.
$Graphics.CompositingMode = [System.Drawing.Drawing2D.CompositingMode]::SourceCopy
$Graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$Graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
+3 -7
View File
@@ -578,12 +578,8 @@ class MaestroRunner:
check=False,
quiet=True,
)
# Maestro waits for the view hierarchy to settle after every tap, input,
# and key press. With animations at their default 1.0 scale each of
# those waits pays for a real transition, which dominates a flow: taps
# measured 3-5s apiece on a physical Pixel 7. CI's emulator gets this
# from the runner's disable-animations flag; nothing was setting it for
# a real device. cleanup() restores the captured values.
# Disable animations to avoid Maestro's per-action transition waits;
# cleanup() restores the captured settings.
for key in ANIMATION_SCALES:
self._adb_run("shell", "settings", "put", "global", key, "0", check=False, quiet=True)
self._adb_run("shell", "input", "keyevent", "KEYCODE_BACK", check=False, quiet=True)
@@ -621,7 +617,7 @@ class MaestroRunner:
) as response:
output.write(response.read().decode(errors="replace"))
output.write("\n")
except Exception as error: # Diagnostics must not hide the original failure.
except Exception as error: # Keep diagnostics from masking the failure.
output.write(f"health_error={error}\n")
if self.container_name:
+6 -19
View File
@@ -15,9 +15,8 @@ ANDROID_15_INSTRUMENTATION_CLASSES = (
"com.edde746.plezy.exoplayer.PlezyAudioModePlaybackTest"
)
ANDROID_15_INSTRUMENTATION_TARGET = "android-15-instrumentation"
# Kept separate from the suites above: only one build type can host androidTest, and
# those suites drive media3 builder APIs the app itself never calls, which R8 shrinks
# legitimately. This class asserts only name-based reachability (#1703).
# Separate R8 reachability from instrumentation: these suites exercise APIs the app
# does not call directly, so shrinking them is expected. Covers name-based reachability (#1703).
ANDROID_R8_REACHABILITY_CLASSES = "androidx.media3.decoder.ffmpeg.FfmpegDecoderReachabilityTest"
ANDROID_R8_REACHABILITY_TARGET = "android-r8-reachability"
@@ -126,9 +125,7 @@ GROUPS: dict[str, tuple[tuple[str, ...], ...]] = {
"android-9": (
(
"basic",
# API 28's emulator routing to the 10.0.2.2 host alias is unreliable
# on this image, so reach Jellyfin over an adb reverse mapping the
# way the media suite already does.
# API 28 uses adb reverse because 10.0.2.2 routing is unreliable here.
"--adb-reverse",
"--flow",
".maestro/flows/05_playback.yaml",
@@ -138,13 +135,7 @@ GROUPS: dict[str, tuple[tuple[str, ...], ...]] = {
"build/maestro-legacy/diagnostics",
),
),
# Real Android TV hardware only, so no workflow dispatches it. The three
# `tv` regressions above run on a phone emulator that `onboard_jellyfin_tv`
# forces into TV mode; this drives the rail layout a device reports on its
# own. The D-pad-only path is also the only way to reach the TV number
# spinner, which InputModeTracker hides as soon as a tap arrives. Run as
# `python3 scripts/run_maestro_ci.py android-tv-device` with
# MAESTRO_DEVICE_ID set to the box.
# Manual Android TV hardware target; workflow runs phone-emulator TV regressions.
"android-tv-device": (
(
"basic",
@@ -201,12 +192,8 @@ def run_android_15_instrumentation() -> None:
def run_android_r8_reachability() -> None:
print("==> Android R8 reachability", flush=True)
# The `minified` build type runs R8 over the app under test, so a keep rule that stops
# covering a reflective lookup, a JNI callback or a native library load fails here
# instead of shipping. No other gate in this repository runs R8 at all.
#
# compileFlutterBuildMinified is deliberately not excluded: CI only prebuilds the
# debug APK, so this variant has no Flutter outputs to reuse.
# Run minified instrumentation to catch reflective, JNI, and native-load keep
# regressions. Do not exclude the Flutter build; CI only prebuilds debug.
run_maestro._run_checked(
(
"android/gradlew",
+8 -32
View File
@@ -1,41 +1,17 @@
#!/usr/bin/env bash
set -uo pipefail
# Run the Flutter test suite with a concurrency that matches the host.
#
# `flutter test` defaults to ceil(numCPUs / 2), which leaves half the machine
# idle. That default is a poor fit here because roughly three quarters of this
# suite's cost is per-file Dart kernel compilation rather than test execution
# (436 test files, each its own isolate), and compilation scales with cores.
#
# Measured on an 8-core host, full suite:
# -j 4 (the default) 190s
# -j 6 168s
# -j 8 136s
# -j 12 165s
#
# One job per core wins; oversubscribing regresses. So scale to the core count
# instead of hard-coding a number that would oversubscribe smaller CI runners.
#
# Any arguments are forwarded to `flutter test`, and an explicit -j/--concurrency
# still overrides the computed value.
# Match Flutter test concurrency to the host. Kernel compilation dominates this
# suite, and one job per available core outperformed the default on CI hosts.
# Explicit -j/--concurrency arguments are forwarded unchanged.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# Overridable so scripts/test_run_tests.py can point the detector at fixtures.
# Overridable for scripts/test_run_tests.py fixtures.
: "${PLEZY_CGROUP_ROOT:=/sys/fs/cgroup}"
# Cores this process may actually use.
#
# Three limits can each be the binding one, and a container can hit any subset
# of them: a generous CPU quota paired with a narrow cpuset is as common as the
# reverse. Taking whichever is discovered first would oversubscribe whenever a
# different one binds, so collect them all and use the smallest.
#
# cgroup v2 quota cpu.max ("<quota> <period>", or "max" when unlimited)
# cgroup v1 quota cpu.cfs_quota_us / cpu.cfs_period_us (-1 when unlimited)
# affinity/cpuset reported by nproc, which honours sched_getaffinity
# Use the smallest available limit: cgroup quota, legacy quota, or affinity.
online_cpus() {
if command -v nproc >/dev/null 2>&1; then
nproc 2>/dev/null && return
@@ -46,7 +22,7 @@ online_cpus() {
getconf _NPROCESSORS_ONLN 2>/dev/null
}
# ceil(quota / period), skipped unless both are positive integers.
# Return ceil(quota / period) for positive numeric values.
quota_cpus() {
local quota="$1" period="$2"
case "$quota$period" in
@@ -82,7 +58,7 @@ detect_cpus() {
fi
fi
# Nothing readable anywhere: prefer a conservative guess over the host count.
# Nothing readable: prefer a conservative default.
if [ "${#limits[@]}" -eq 0 ]; then
echo 4
return
@@ -96,7 +72,7 @@ detect_cpus() {
echo "$smallest"
}
# Sourced by the tests to exercise the detector; only a direct run continues.
# Tests source this file to exercise the detector.
if [ "${BASH_SOURCE[0]}" != "$0" ]; then
return 0
fi