chore: clean up code comments
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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'"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user