gowalk-cicd 1.0.14 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/scripts/gradle_wrapper.py +271 -0
- package/android-action/scripts/resolve_android.py +30 -0
- package/android-action/scripts/test_gradle_wrapper.py +205 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -205,6 +205,33 @@ committed back. If the module computes its `versionCode` instead of declaring a
|
|
|
205
205
|
literal, the action emits a `::warning::` and leaves the project's own value
|
|
206
206
|
alone.
|
|
207
207
|
|
|
208
|
+
### Build toolchain floors (Flutter apps)
|
|
209
|
+
|
|
210
|
+
The Flutter Gradle plugin refuses to apply to a project whose wrapper is older
|
|
211
|
+
than the SDK's floor:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
Your project's Gradle version (8.11.1) is lower than Flutter's minimum
|
|
215
|
+
supported version of 8.14.0.
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
CI installs the current `stable` Flutter, so that floor rises on Flutter's
|
|
219
|
+
release cadence rather than the app's — every app in a fleet breaks on the same
|
|
220
|
+
day, long after the last commit that could have anticipated it. Before building,
|
|
221
|
+
the action reads the floor out of the runner's Flutter SDK and, when the
|
|
222
|
+
project's `gradle-wrapper.properties` is below it, rewrites `distributionUrl`
|
|
223
|
+
**in the checkout** with a `::warning::`. The Android Gradle Plugin and Kotlin
|
|
224
|
+
Gradle Plugin versions declared in `android/settings.gradle` (or the older
|
|
225
|
+
`buildscript { classpath ... }` / `ext.kotlin_version` form) are raised the same
|
|
226
|
+
way against Flutter's `errorAGPVersion` / `errorKGPVersion`. Same policy as the
|
|
227
|
+
versionCode rewrite: the edits are never committed back, so the project keeps
|
|
228
|
+
whatever versions its authors chose. Commit the bumps yourself to silence the
|
|
229
|
+
warnings.
|
|
230
|
+
|
|
231
|
+
(The rewritten name is verified against `services.gradle.org` because Gradle's
|
|
232
|
+
own naming is inconsistent across majors — `gradle-8.14-all.zip` but
|
|
233
|
+
`gradle-9.0.0-all.zip`.)
|
|
234
|
+
|
|
208
235
|
### iOS delivery
|
|
209
236
|
|
|
210
237
|
The iOS composite action runs on `macos-15` and:
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.16
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.16
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Raise a Flutter app's build toolchain to the versions its Flutter SDK requires.
|
|
3
|
+
|
|
4
|
+
The Flutter Gradle plugin refuses to apply when the project's wrapper is older
|
|
5
|
+
than the SDK's floor:
|
|
6
|
+
|
|
7
|
+
Your project's Gradle version (8.11.1) is lower than Flutter's minimum
|
|
8
|
+
supported version of 8.14.0.
|
|
9
|
+
|
|
10
|
+
CI installs the current `stable` Flutter, so that floor rises on Flutter's
|
|
11
|
+
release cadence, not the app's. Every app in a fleet then breaks on the same
|
|
12
|
+
day — long after the last commit that could have anticipated it.
|
|
13
|
+
|
|
14
|
+
This raises the wrapper inside the CI checkout, the same way the action already
|
|
15
|
+
rewrites versionCode there. The edit is never committed back; the project keeps
|
|
16
|
+
whatever version its authors chose, and the build stops depending on the
|
|
17
|
+
project having been touched since the last Flutter release.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
# `stable` moves the floor over time, so read it from the SDK rather than
|
|
26
|
+
# hardcoding a number this file would have to chase.
|
|
27
|
+
_CHECKER = Path("packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt")
|
|
28
|
+
# The same file declares a floor for each of the three: Gradle, the Android
|
|
29
|
+
# Gradle Plugin and the Kotlin Gradle Plugin. All three are hard errors, and
|
|
30
|
+
# all three move with `stable`.
|
|
31
|
+
_FLOORS = {
|
|
32
|
+
"gradle": r"errorGradleVersion\s*:\s*Version\s*=\s*Version\((\d+),\s*(\d+),\s*(\d+)\)",
|
|
33
|
+
"agp": r"errorAGPVersion\s*:\s*AndroidPluginVersion\s*=\s*AndroidPluginVersion\((\d+),\s*(\d+),\s*(\d+)\)",
|
|
34
|
+
"kgp": r"errorKGPVersion\s*:\s*Version\s*=\s*Version\((\d+),\s*(\d+),\s*(\d+)\)",
|
|
35
|
+
}
|
|
36
|
+
_ERROR_VERSION = re.compile(_FLOORS["gradle"])
|
|
37
|
+
_DISTRIBUTION = re.compile(r"^(distributionUrl=.*gradle-)([0-9]+(?:\.[0-9]+)*)(-(?:all|bin)\.zip)$", re.M)
|
|
38
|
+
|
|
39
|
+
Version = tuple[int, ...]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_version(text: str) -> Version | None:
|
|
43
|
+
if not re.fullmatch(r"[0-9]+(\.[0-9]+)*", text or ""):
|
|
44
|
+
return None
|
|
45
|
+
return tuple(int(part) for part in text.split("."))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _render(version: Version) -> str:
|
|
49
|
+
return ".".join(str(part) for part in version)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _stripped(version: Version) -> str:
|
|
53
|
+
parts = list(version)
|
|
54
|
+
while len(parts) > 2 and parts[-1] == 0:
|
|
55
|
+
parts.pop()
|
|
56
|
+
return _render(tuple(parts))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def candidates(version: Version) -> list[str]:
|
|
60
|
+
"""Distribution names to try, best guess first.
|
|
61
|
+
|
|
62
|
+
Gradle's naming is not consistent across majors: 8.x drops a trailing zero
|
|
63
|
+
patch (`gradle-8.14-all.zip`; `gradle-8.14.0-all.zip` is a 404) while 9.x
|
|
64
|
+
keeps it (`gradle-9.0.0-all.zip`; `gradle-9.0-all.zip` is a 404). Getting
|
|
65
|
+
this wrong swaps the error being fixed for a download failure that does not
|
|
66
|
+
look like our doing, so order by the observed rule and keep the other form
|
|
67
|
+
as a fallback.
|
|
68
|
+
"""
|
|
69
|
+
stripped, full = _stripped(version), _render(version)
|
|
70
|
+
order = [full, stripped] if version and version[0] >= 9 else [stripped, full]
|
|
71
|
+
return list(dict.fromkeys(order))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _exists(name: str) -> bool | None:
|
|
75
|
+
"""True/False if the distribution could be checked, None if we could not."""
|
|
76
|
+
import urllib.error
|
|
77
|
+
import urllib.request
|
|
78
|
+
|
|
79
|
+
url = f"https://services.gradle.org/distributions/gradle-{name}-all.zip"
|
|
80
|
+
request = urllib.request.Request(url, method="HEAD")
|
|
81
|
+
try:
|
|
82
|
+
with urllib.request.urlopen(request, timeout=15) as response:
|
|
83
|
+
return response.status == 200
|
|
84
|
+
except urllib.error.HTTPError as exc:
|
|
85
|
+
return exc.code == 200
|
|
86
|
+
except OSError:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def normalize(version: Version, probe=_exists) -> str:
|
|
91
|
+
"""The distribution name to write into distributionUrl.
|
|
92
|
+
|
|
93
|
+
Probes services.gradle.org so a future naming change corrects itself; falls
|
|
94
|
+
back to the ordering in `candidates` when the network is unavailable, which
|
|
95
|
+
is no worse than not probing at all.
|
|
96
|
+
"""
|
|
97
|
+
options = candidates(version)
|
|
98
|
+
for name in options:
|
|
99
|
+
if probe(name) is True:
|
|
100
|
+
return name
|
|
101
|
+
return options[0]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def flutter_minimum(flutter_root: Path, tool: str) -> Version | None:
|
|
105
|
+
"""The oldest `tool` (gradle | agp | kgp) this Flutter SDK will build against."""
|
|
106
|
+
checker = flutter_root / _CHECKER
|
|
107
|
+
try:
|
|
108
|
+
text = checker.read_text(encoding="utf-8")
|
|
109
|
+
except OSError:
|
|
110
|
+
return None
|
|
111
|
+
match = re.search(_FLOORS[tool], text)
|
|
112
|
+
if not match:
|
|
113
|
+
return None
|
|
114
|
+
return tuple(int(group) for group in match.groups())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def flutter_minimum_gradle(flutter_root: Path) -> Version | None:
|
|
118
|
+
"""The oldest Gradle this Flutter SDK will apply its plugin against."""
|
|
119
|
+
return flutter_minimum(flutter_root, "gradle")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def wrapper_properties(workspace: Path, gradle_root: Path | None = None) -> Path:
|
|
123
|
+
root = gradle_root or (workspace / "android")
|
|
124
|
+
return root / "gradle" / "wrapper" / "gradle-wrapper.properties"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def read_wrapper_version(properties: Path) -> Version | None:
|
|
128
|
+
try:
|
|
129
|
+
match = _DISTRIBUTION.search(properties.read_text(encoding="utf-8"))
|
|
130
|
+
except OSError:
|
|
131
|
+
return None
|
|
132
|
+
return parse_version(match.group(2)) if match else None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def set_wrapper_version(properties: Path, version: Version) -> bool:
|
|
136
|
+
try:
|
|
137
|
+
text = properties.read_text(encoding="utf-8")
|
|
138
|
+
except OSError:
|
|
139
|
+
return False
|
|
140
|
+
replaced, count = _DISTRIBUTION.subn(
|
|
141
|
+
lambda m: f"{m.group(1)}{normalize(version)}{m.group(3)}", text
|
|
142
|
+
)
|
|
143
|
+
if not count:
|
|
144
|
+
return False
|
|
145
|
+
properties.write_text(replaced, encoding="utf-8")
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def ensure_minimum(workspace: Path, flutter_root: Path, gradle_root: Path | None = None) -> str:
|
|
150
|
+
"""Raise the wrapper to Flutter's floor. Returns a line for the build log."""
|
|
151
|
+
properties = wrapper_properties(workspace, gradle_root)
|
|
152
|
+
if not properties.is_file():
|
|
153
|
+
return ""
|
|
154
|
+
minimum = flutter_minimum_gradle(flutter_root)
|
|
155
|
+
if minimum is None:
|
|
156
|
+
return (
|
|
157
|
+
"::warning::Could not read Flutter's minimum Gradle version from "
|
|
158
|
+
f"{flutter_root}; leaving the wrapper alone"
|
|
159
|
+
)
|
|
160
|
+
current = read_wrapper_version(properties)
|
|
161
|
+
if current is None:
|
|
162
|
+
return f"::warning::No Gradle distributionUrl found in {properties}; leaving it alone"
|
|
163
|
+
if current >= minimum:
|
|
164
|
+
return f"Gradle wrapper {normalize(current)} meets Flutter's minimum {normalize(minimum)}"
|
|
165
|
+
if not set_wrapper_version(properties, minimum):
|
|
166
|
+
return f"::warning::Failed to rewrite the Gradle distributionUrl in {properties}"
|
|
167
|
+
return (
|
|
168
|
+
f"::warning::Gradle wrapper {normalize(current)} is below Flutter's minimum "
|
|
169
|
+
f"{normalize(minimum)}; raised it to {normalize(minimum)} for this build only. "
|
|
170
|
+
f"Commit the bump in {properties.name} to make it permanent."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# --- Android Gradle Plugin and Kotlin Gradle Plugin -------------------------
|
|
175
|
+
#
|
|
176
|
+
# Both are declared in `android/settings.gradle(.kts)` in the modern Flutter
|
|
177
|
+
# template and in the top-level `android/build.gradle` in the older one. Flutter
|
|
178
|
+
# floors both, and both floors move with `stable`, so an app that built fine
|
|
179
|
+
# last month stops building without a single line of it having changed.
|
|
180
|
+
|
|
181
|
+
_PLUGIN_BLOCK = {
|
|
182
|
+
"agp": re.compile(
|
|
183
|
+
r"""(id\s*\(?\s*["']com\.android\.application["']\s*\)?\s*version\s*["'])([0-9][0-9.]*)(["'])"""
|
|
184
|
+
),
|
|
185
|
+
"kgp": re.compile(
|
|
186
|
+
r"""(id\s*\(?\s*["']org\.jetbrains\.kotlin\.android["']\s*\)?\s*version\s*["'])([0-9][0-9.]*)(["'])"""
|
|
187
|
+
),
|
|
188
|
+
}
|
|
189
|
+
_LEGACY = {
|
|
190
|
+
"agp": re.compile(r"""(["']com\.android\.tools\.build:gradle:)([0-9][0-9.]*)(["'])"""),
|
|
191
|
+
"kgp": re.compile(r"""(ext\.kotlin_version\s*=\s*["'])([0-9][0-9.]*)(["'])"""),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_LABEL = {"agp": "Android Gradle Plugin", "kgp": "Kotlin Gradle Plugin"}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def toolchain_files(workspace: Path, gradle_root: Path | None = None) -> list[Path]:
|
|
198
|
+
root = gradle_root or (workspace / "android")
|
|
199
|
+
names = ("settings.gradle", "settings.gradle.kts", "build.gradle", "build.gradle.kts")
|
|
200
|
+
return [root / name for name in names if (root / name).is_file()]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def read_declared(files: list[Path], tool: str) -> tuple[Path, Version] | None:
|
|
204
|
+
"""First declaration of `tool` across the candidate build files."""
|
|
205
|
+
for path in files:
|
|
206
|
+
try:
|
|
207
|
+
text = path.read_text(encoding="utf-8")
|
|
208
|
+
except OSError:
|
|
209
|
+
continue
|
|
210
|
+
for pattern in (_PLUGIN_BLOCK[tool], _LEGACY[tool]):
|
|
211
|
+
match = pattern.search(text)
|
|
212
|
+
if match:
|
|
213
|
+
version = parse_version(match.group(2))
|
|
214
|
+
if version is not None:
|
|
215
|
+
return path, version
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def set_declared(path: Path, tool: str, version: Version) -> bool:
|
|
220
|
+
try:
|
|
221
|
+
text = path.read_text(encoding="utf-8")
|
|
222
|
+
except OSError:
|
|
223
|
+
return False
|
|
224
|
+
total = 0
|
|
225
|
+
for pattern in (_PLUGIN_BLOCK[tool], _LEGACY[tool]):
|
|
226
|
+
text, count = pattern.subn(lambda m: f"{m.group(1)}{_render(version)}{m.group(3)}", text)
|
|
227
|
+
total += count
|
|
228
|
+
if not total:
|
|
229
|
+
return False
|
|
230
|
+
path.write_text(text, encoding="utf-8")
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def ensure_toolchain(
|
|
235
|
+
workspace: Path, flutter_root: Path, gradle_root: Path | None = None
|
|
236
|
+
) -> list[str]:
|
|
237
|
+
"""Raise Gradle, AGP and KGP to this Flutter SDK's floors. Returns log lines."""
|
|
238
|
+
messages = []
|
|
239
|
+
gradle = ensure_minimum(workspace, flutter_root, gradle_root)
|
|
240
|
+
if gradle:
|
|
241
|
+
messages.append(gradle)
|
|
242
|
+
files = toolchain_files(workspace, gradle_root)
|
|
243
|
+
if not files:
|
|
244
|
+
return messages
|
|
245
|
+
for tool in ("agp", "kgp"):
|
|
246
|
+
minimum = flutter_minimum(flutter_root, tool)
|
|
247
|
+
if minimum is None:
|
|
248
|
+
messages.append(
|
|
249
|
+
f"::warning::Could not read Flutter's minimum {_LABEL[tool]} version; leaving it alone"
|
|
250
|
+
)
|
|
251
|
+
continue
|
|
252
|
+
found = read_declared(files, tool)
|
|
253
|
+
if found is None:
|
|
254
|
+
# Plenty of valid projects inherit these from elsewhere; that is not
|
|
255
|
+
# something to fail a deploy over.
|
|
256
|
+
continue
|
|
257
|
+
path, current = found
|
|
258
|
+
if current >= minimum:
|
|
259
|
+
messages.append(
|
|
260
|
+
f"{_LABEL[tool]} {_render(current)} meets Flutter's minimum {_render(minimum)}"
|
|
261
|
+
)
|
|
262
|
+
continue
|
|
263
|
+
if not set_declared(path, tool, minimum):
|
|
264
|
+
messages.append(f"::warning::Failed to raise the {_LABEL[tool]} version in {path}")
|
|
265
|
+
continue
|
|
266
|
+
messages.append(
|
|
267
|
+
f"::warning::{_LABEL[tool]} {_render(current)} is below Flutter's minimum "
|
|
268
|
+
f"{_render(minimum)}; raised it to {_render(minimum)} in {path.name} for this build "
|
|
269
|
+
f"only. Commit the bump to make it permanent."
|
|
270
|
+
)
|
|
271
|
+
return messages
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
|
+
import shutil
|
|
7
8
|
import sys
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
11
|
+
import gradle_wrapper
|
|
10
12
|
from android_config import (
|
|
11
13
|
ConfigError,
|
|
12
14
|
detect_package_name,
|
|
@@ -126,6 +128,22 @@ def apply_gradle_version(project, build_number: int, build_name: str) -> None:
|
|
|
126
128
|
)
|
|
127
129
|
|
|
128
130
|
|
|
131
|
+
def flutter_root() -> Path | None:
|
|
132
|
+
"""Where the runner's Flutter SDK lives.
|
|
133
|
+
|
|
134
|
+
flutter-action exports FLUTTER_ROOT; a self-hosted runner with Flutter
|
|
135
|
+
merely on PATH does not, so fall back to walking up from the binary
|
|
136
|
+
(<root>/bin/flutter).
|
|
137
|
+
"""
|
|
138
|
+
declared = os.environ.get("FLUTTER_ROOT")
|
|
139
|
+
if declared:
|
|
140
|
+
return Path(declared)
|
|
141
|
+
binary = shutil.which("flutter")
|
|
142
|
+
if not binary:
|
|
143
|
+
return None
|
|
144
|
+
return Path(binary).resolve().parent.parent
|
|
145
|
+
|
|
146
|
+
|
|
129
147
|
def main() -> None:
|
|
130
148
|
workspace = Path(os.environ.get("GITHUB_WORKSPACE", ".")).resolve()
|
|
131
149
|
env_path = Path(os.environ["GITHUB_ENV"])
|
|
@@ -151,6 +169,18 @@ def main() -> None:
|
|
|
151
169
|
apply_gradle_version(
|
|
152
170
|
project, build_number, os.environ.get("INPUT_BUILD_NAME") or ""
|
|
153
171
|
)
|
|
172
|
+
else:
|
|
173
|
+
# Flutter refuses to apply its Gradle plugin when the project's
|
|
174
|
+
# Gradle, AGP or Kotlin plugin is older than the SDK's floor, and CI
|
|
175
|
+
# tracks `stable`, so those floors rise without the app changing.
|
|
176
|
+
# Raise them in the checkout rather than letting every app in the
|
|
177
|
+
# fleet break on Flutter's release day.
|
|
178
|
+
root = flutter_root()
|
|
179
|
+
if root is None:
|
|
180
|
+
print("::warning::Flutter SDK not found; skipping the build toolchain check")
|
|
181
|
+
else:
|
|
182
|
+
for message in gradle_wrapper.ensure_toolchain(workspace, root):
|
|
183
|
+
print(message)
|
|
154
184
|
except (ConfigError, KeyError, OSError) as exc:
|
|
155
185
|
print(f"::error::{exc}")
|
|
156
186
|
raise SystemExit(1) from exc
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for raising a Flutter app's Gradle wrapper to the SDK's floor."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import gradle_wrapper as gw
|
|
11
|
+
|
|
12
|
+
CHECKER_SOURCE = """
|
|
13
|
+
package com.flutter.gradle
|
|
14
|
+
object DependencyVersionChecker {
|
|
15
|
+
internal val warnGradleVersion: Version = Version(8, 7, 0)
|
|
16
|
+
internal val errorGradleVersion: Version = Version(8, 14, 0)
|
|
17
|
+
}
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
WRAPPER = (
|
|
21
|
+
"distributionBase=GRADLE_USER_HOME\n"
|
|
22
|
+
"distributionPath=wrapper/dists\n"
|
|
23
|
+
"distributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-all.zip\n"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def never(_name: str) -> bool:
|
|
28
|
+
"""Probe stub for tests: the network is never consulted."""
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RenderTest(unittest.TestCase):
|
|
33
|
+
def test_eight_x_drops_a_trailing_zero_patch(self):
|
|
34
|
+
self.assertEqual(gw.candidates((8, 14, 0))[0], "8.14")
|
|
35
|
+
|
|
36
|
+
def test_nine_x_keeps_a_trailing_zero_patch(self):
|
|
37
|
+
# gradle-9.0-all.zip is a 404; gradle-9.0.0-all.zip is the real name.
|
|
38
|
+
self.assertEqual(gw.candidates((9, 0, 0))[0], "9.0.0")
|
|
39
|
+
|
|
40
|
+
def test_a_real_patch_is_never_stripped(self):
|
|
41
|
+
self.assertEqual(gw.candidates((8, 14, 3)), ["8.14.3"])
|
|
42
|
+
|
|
43
|
+
def test_the_other_spelling_stays_available_as_a_fallback(self):
|
|
44
|
+
self.assertIn("8.14.0", gw.candidates((8, 14, 0)))
|
|
45
|
+
|
|
46
|
+
def test_falls_back_to_the_first_candidate_without_network(self):
|
|
47
|
+
self.assertEqual(gw.normalize((8, 14, 0), probe=never), "8.14")
|
|
48
|
+
|
|
49
|
+
def test_a_successful_probe_wins_over_the_ordering(self):
|
|
50
|
+
self.assertEqual(
|
|
51
|
+
gw.normalize((8, 14, 0), probe=lambda name: name == "8.14.0"), "8.14.0"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class WrapperFileTest(unittest.TestCase):
|
|
56
|
+
def setUp(self):
|
|
57
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
58
|
+
self.workspace = Path(self.tmp.name)
|
|
59
|
+
self.properties = gw.wrapper_properties(self.workspace)
|
|
60
|
+
self.properties.parent.mkdir(parents=True)
|
|
61
|
+
self.properties.write_text(WRAPPER, encoding="utf-8")
|
|
62
|
+
self.flutter = self.workspace / "flutter"
|
|
63
|
+
checker = self.flutter / gw._CHECKER
|
|
64
|
+
checker.parent.mkdir(parents=True)
|
|
65
|
+
checker.write_text(CHECKER_SOURCE, encoding="utf-8")
|
|
66
|
+
self.addCleanup(self.tmp.cleanup)
|
|
67
|
+
|
|
68
|
+
def test_reads_the_declared_version(self):
|
|
69
|
+
self.assertEqual(gw.read_wrapper_version(self.properties), (8, 11, 1))
|
|
70
|
+
|
|
71
|
+
def test_reads_the_sdk_floor(self):
|
|
72
|
+
self.assertEqual(gw.flutter_minimum_gradle(self.flutter), (8, 14, 0))
|
|
73
|
+
|
|
74
|
+
def test_raises_a_wrapper_below_the_floor(self):
|
|
75
|
+
gw.set_wrapper_version(self.properties, (8, 14, 0))
|
|
76
|
+
self.assertEqual(gw.read_wrapper_version(self.properties), (8, 14,))
|
|
77
|
+
self.assertIn("gradle-8.14-all.zip", self.properties.read_text())
|
|
78
|
+
|
|
79
|
+
def test_preserves_a_bin_distribution(self):
|
|
80
|
+
self.properties.write_text(
|
|
81
|
+
WRAPPER.replace("-all.zip", "-bin.zip"), encoding="utf-8"
|
|
82
|
+
)
|
|
83
|
+
gw.set_wrapper_version(self.properties, (8, 14, 0))
|
|
84
|
+
self.assertIn("gradle-8.14-bin.zip", self.properties.read_text())
|
|
85
|
+
|
|
86
|
+
def test_leaves_a_wrapper_already_at_the_floor_alone(self):
|
|
87
|
+
self.properties.write_text(
|
|
88
|
+
WRAPPER.replace("8.11.1", "8.14.3"), encoding="utf-8"
|
|
89
|
+
)
|
|
90
|
+
message = gw.ensure_minimum(self.workspace, self.flutter)
|
|
91
|
+
self.assertIn("meets Flutter's minimum", message)
|
|
92
|
+
self.assertIn("gradle-8.14.3-all.zip", self.properties.read_text())
|
|
93
|
+
|
|
94
|
+
def test_warns_and_raises_when_below_the_floor(self):
|
|
95
|
+
message = gw.ensure_minimum(self.workspace, self.flutter)
|
|
96
|
+
self.assertTrue(message.startswith("::warning::"))
|
|
97
|
+
self.assertIn("8.11.1", message)
|
|
98
|
+
self.assertIn("gradle-8.14", self.properties.read_text())
|
|
99
|
+
|
|
100
|
+
def test_no_wrapper_is_not_an_error(self):
|
|
101
|
+
self.properties.unlink()
|
|
102
|
+
self.assertEqual(gw.ensure_minimum(self.workspace, self.flutter), "")
|
|
103
|
+
|
|
104
|
+
def test_unreadable_sdk_leaves_the_wrapper_untouched(self):
|
|
105
|
+
message = gw.ensure_minimum(self.workspace, self.workspace / "nope")
|
|
106
|
+
self.assertTrue(message.startswith("::warning::"))
|
|
107
|
+
self.assertIn("gradle-8.11.1-all.zip", self.properties.read_text())
|
|
108
|
+
|
|
109
|
+
def test_a_wrapper_without_a_distribution_url_is_left_alone(self):
|
|
110
|
+
self.properties.write_text("distributionBase=GRADLE_USER_HOME\n", encoding="utf-8")
|
|
111
|
+
message = gw.ensure_minimum(self.workspace, self.flutter)
|
|
112
|
+
self.assertIn("No Gradle distributionUrl", message)
|
|
113
|
+
|
|
114
|
+
def test_honours_an_explicit_gradle_root(self):
|
|
115
|
+
root = self.workspace / "nested"
|
|
116
|
+
properties = gw.wrapper_properties(self.workspace, root)
|
|
117
|
+
properties.parent.mkdir(parents=True)
|
|
118
|
+
properties.write_text(WRAPPER, encoding="utf-8")
|
|
119
|
+
gw.ensure_minimum(self.workspace, self.flutter, gradle_root=root)
|
|
120
|
+
self.assertIn("gradle-8.14", properties.read_text())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
unittest.main()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
SETTINGS = """
|
|
128
|
+
pluginManagement { includeBuild("../flutter/packages/flutter_tools/gradle") }
|
|
129
|
+
plugins {
|
|
130
|
+
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
|
131
|
+
id "com.android.application" version "8.9.1" apply false
|
|
132
|
+
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
|
133
|
+
}
|
|
134
|
+
include ":app"
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
LEGACY_BUILD = """
|
|
138
|
+
buildscript {
|
|
139
|
+
ext.kotlin_version = '1.7.10'
|
|
140
|
+
dependencies { classpath 'com.android.tools.build:gradle:7.4.2' }
|
|
141
|
+
}
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
CHECKER_WITH_ALL_FLOORS = """
|
|
145
|
+
internal val errorGradleVersion: Version = Version(8, 14, 0)
|
|
146
|
+
internal val errorAGPVersion: AndroidPluginVersion = AndroidPluginVersion(8, 11, 1)
|
|
147
|
+
internal val errorKGPVersion: Version = Version(2, 0, 0)
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ToolchainTest(unittest.TestCase):
|
|
152
|
+
def setUp(self):
|
|
153
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
154
|
+
self.workspace = Path(self.tmp.name)
|
|
155
|
+
android = self.workspace / "android"
|
|
156
|
+
android.mkdir()
|
|
157
|
+
(android / "settings.gradle").write_text(SETTINGS, encoding="utf-8")
|
|
158
|
+
properties = gw.wrapper_properties(self.workspace)
|
|
159
|
+
properties.parent.mkdir(parents=True)
|
|
160
|
+
properties.write_text(WRAPPER, encoding="utf-8")
|
|
161
|
+
self.settings = android / "settings.gradle"
|
|
162
|
+
self.flutter = self.workspace / "flutter"
|
|
163
|
+
checker = self.flutter / gw._CHECKER
|
|
164
|
+
checker.parent.mkdir(parents=True)
|
|
165
|
+
checker.write_text(CHECKER_WITH_ALL_FLOORS, encoding="utf-8")
|
|
166
|
+
self.addCleanup(self.tmp.cleanup)
|
|
167
|
+
|
|
168
|
+
def test_reads_each_floor_from_the_sdk(self):
|
|
169
|
+
self.assertEqual(gw.flutter_minimum(self.flutter, "gradle"), (8, 14, 0))
|
|
170
|
+
self.assertEqual(gw.flutter_minimum(self.flutter, "agp"), (8, 11, 1))
|
|
171
|
+
self.assertEqual(gw.flutter_minimum(self.flutter, "kgp"), (2, 0, 0))
|
|
172
|
+
|
|
173
|
+
def test_reads_the_plugins_block(self):
|
|
174
|
+
files = gw.toolchain_files(self.workspace)
|
|
175
|
+
self.assertEqual(gw.read_declared(files, "agp")[1], (8, 9, 1))
|
|
176
|
+
self.assertEqual(gw.read_declared(files, "kgp")[1], (2, 1, 0))
|
|
177
|
+
|
|
178
|
+
def test_raises_agp_but_leaves_a_compliant_kgp(self):
|
|
179
|
+
messages = gw.ensure_toolchain(self.workspace, self.flutter)
|
|
180
|
+
text = self.settings.read_text()
|
|
181
|
+
self.assertIn('"com.android.application" version "8.11.1"', text)
|
|
182
|
+
self.assertIn('"org.jetbrains.kotlin.android" version "2.1.0"', text)
|
|
183
|
+
self.assertTrue(any("Android Gradle Plugin 8.9.1 is below" in m for m in messages))
|
|
184
|
+
self.assertTrue(any("Kotlin Gradle Plugin 2.1.0 meets" in m for m in messages))
|
|
185
|
+
|
|
186
|
+
def test_raises_the_wrapper_in_the_same_pass(self):
|
|
187
|
+
gw.ensure_toolchain(self.workspace, self.flutter)
|
|
188
|
+
self.assertIn("gradle-8.14-all.zip", gw.wrapper_properties(self.workspace).read_text())
|
|
189
|
+
|
|
190
|
+
def test_handles_the_legacy_buildscript_form(self):
|
|
191
|
+
self.settings.unlink()
|
|
192
|
+
(self.workspace / "android" / "build.gradle").write_text(LEGACY_BUILD, encoding="utf-8")
|
|
193
|
+
gw.ensure_toolchain(self.workspace, self.flutter)
|
|
194
|
+
text = (self.workspace / "android" / "build.gradle").read_text()
|
|
195
|
+
self.assertIn("com.android.tools.build:gradle:8.11.1", text)
|
|
196
|
+
self.assertIn("ext.kotlin_version = '2.0.0'", text)
|
|
197
|
+
|
|
198
|
+
def test_a_project_declaring_neither_is_left_alone(self):
|
|
199
|
+
self.settings.write_text("include ':app'\n", encoding="utf-8")
|
|
200
|
+
messages = gw.ensure_toolchain(self.workspace, self.flutter)
|
|
201
|
+
self.assertFalse(any("Android Gradle Plugin" in m for m in messages))
|
|
202
|
+
|
|
203
|
+
def test_an_unreadable_sdk_warns_per_tool(self):
|
|
204
|
+
messages = gw.ensure_toolchain(self.workspace, self.workspace / "nope")
|
|
205
|
+
self.assertEqual(sum("::warning::" in m for m in messages), 3)
|