flutter-setup 2.1.0__py3-none-any.whl
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.
- flutter_setup/__init__.py +0 -0
- flutter_setup/bootstrap.py +904 -0
- flutter_setup/cicd_generator.py +814 -0
- flutter_setup/cli.py +662 -0
- flutter_setup/config.py +135 -0
- flutter_setup/config_manager.py +159 -0
- flutter_setup/core.py +204 -0
- flutter_setup/exceptions.py +37 -0
- flutter_setup/flutter_manager.py +579 -0
- flutter_setup/platform.py +15 -0
- flutter_setup/prerequisites.py +39 -0
- flutter_setup/prerequisites_linux.py +140 -0
- flutter_setup/prerequisites_macos.py +226 -0
- flutter_setup/project_creator.py +82 -0
- flutter_setup-2.1.0.dist-info/METADATA +365 -0
- flutter_setup-2.1.0.dist-info/RECORD +20 -0
- flutter_setup-2.1.0.dist-info/WHEEL +5 -0
- flutter_setup-2.1.0.dist-info/entry_points.txt +2 -0
- flutter_setup-2.1.0.dist-info/licenses/LICENSE +21 -0
- flutter_setup-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
"""Flutter SDK management for Flutter setup."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from .config import Config
|
|
11
|
+
from .exceptions import FlutterInstallationError
|
|
12
|
+
from .platform import detect_runtime_platform
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FlutterManager:
|
|
18
|
+
"""Manages Flutter SDK installation and updates."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: Config):
|
|
21
|
+
"""Initialize FlutterManager."""
|
|
22
|
+
self.config = config
|
|
23
|
+
self.platform = detect_runtime_platform()
|
|
24
|
+
self.home = Path.home()
|
|
25
|
+
self.flutter_root = config.flutter_location
|
|
26
|
+
self.path_profiles = self._path_profiles_for_platform()
|
|
27
|
+
|
|
28
|
+
def _path_profiles_for_platform(self) -> list[Path]:
|
|
29
|
+
"""Return shell profile files that should contain Flutter PATH."""
|
|
30
|
+
if self.platform == "darwin":
|
|
31
|
+
return [self.home / ".zprofile", self.home / ".zshrc"]
|
|
32
|
+
if self.platform == "linux":
|
|
33
|
+
return [self.home / ".bashrc", self.home / ".zshrc"]
|
|
34
|
+
return [self.home / ".profile"]
|
|
35
|
+
|
|
36
|
+
def _normalize_flutter_bin_path(self, path: str) -> str:
|
|
37
|
+
"""Normalize common shell home forms before comparing Flutter bin paths."""
|
|
38
|
+
expanded = path.replace("$HOME", str(self.home))
|
|
39
|
+
if expanded == "~":
|
|
40
|
+
expanded = str(self.home)
|
|
41
|
+
elif expanded.startswith("~/"):
|
|
42
|
+
expanded = str(self.home / expanded[2:])
|
|
43
|
+
return str(Path(expanded))
|
|
44
|
+
|
|
45
|
+
def _expected_flutter_bin_path(self) -> str:
|
|
46
|
+
"""Return the normalized expected Flutter bin path."""
|
|
47
|
+
return self._normalize_flutter_bin_path(str(self.flutter_root / "bin"))
|
|
48
|
+
|
|
49
|
+
def ensure_flutter(self) -> None:
|
|
50
|
+
"""Ensure Flutter SDK is installed and up to date."""
|
|
51
|
+
if self.config.dry_run:
|
|
52
|
+
console.print("[yellow]DRY RUN: Would manage Flutter SDK[/yellow]")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# Handle reclone mode
|
|
56
|
+
if self.config.flutter_update_mode == "reclone":
|
|
57
|
+
self._reclone_flutter()
|
|
58
|
+
# Check if Flutter is already installed
|
|
59
|
+
elif (
|
|
60
|
+
not self.flutter_root.exists() or not (self.flutter_root / ".git").exists()
|
|
61
|
+
):
|
|
62
|
+
self._install_flutter()
|
|
63
|
+
elif self.config.flutter_update_mode == "skip":
|
|
64
|
+
console.print(" âī¸ Skipping Flutter SDK update (update mode: skip)")
|
|
65
|
+
else:
|
|
66
|
+
self._update_flutter()
|
|
67
|
+
|
|
68
|
+
# Verify version constraint before proceeding
|
|
69
|
+
if self.config.flutter_version:
|
|
70
|
+
self._check_flutter_version()
|
|
71
|
+
|
|
72
|
+
# Ensure Flutter is in PATH
|
|
73
|
+
self._ensure_flutter_path()
|
|
74
|
+
|
|
75
|
+
# Ensure Android SDK environment variables are set when targeting Android
|
|
76
|
+
if "android" in self.config.platforms:
|
|
77
|
+
self._ensure_android_env()
|
|
78
|
+
|
|
79
|
+
# Run flutter doctor
|
|
80
|
+
self._run_flutter_doctor()
|
|
81
|
+
|
|
82
|
+
def _get_current_flutter_version(self) -> str | None:
|
|
83
|
+
"""Return the installed Flutter version string, or None if not detectable."""
|
|
84
|
+
flutter_bin = self.flutter_root / "bin" / "flutter"
|
|
85
|
+
try:
|
|
86
|
+
result = subprocess.run(
|
|
87
|
+
[str(flutter_bin), "--version"],
|
|
88
|
+
capture_output=True,
|
|
89
|
+
text=True,
|
|
90
|
+
check=False,
|
|
91
|
+
)
|
|
92
|
+
output = result.stdout or result.stderr or ""
|
|
93
|
+
match = re.search(r"Flutter\s+([\d.]+)", output)
|
|
94
|
+
if match:
|
|
95
|
+
return match.group(1)
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def _check_flutter_version(self) -> None:
|
|
101
|
+
"""Raise FlutterInstallationError if the installed version is older than config.flutter_version."""
|
|
102
|
+
required = self.config.flutter_version
|
|
103
|
+
if not required:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
current = self._get_current_flutter_version()
|
|
107
|
+
if current is None:
|
|
108
|
+
raise FlutterInstallationError(
|
|
109
|
+
f"Could not determine Flutter version. "
|
|
110
|
+
f"Ensure Flutter is installed at {self.flutter_root}."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def parse(v: str) -> tuple[int, ...]:
|
|
114
|
+
return tuple(int(x) for x in v.split("."))
|
|
115
|
+
|
|
116
|
+
if parse(current) < parse(required):
|
|
117
|
+
raise FlutterInstallationError(
|
|
118
|
+
f"Flutter version too old: found {current}, requires >={required}.\n"
|
|
119
|
+
f" To upgrade: git -C {self.flutter_root} fetch && "
|
|
120
|
+
f"git -C {self.flutter_root} checkout {required}"
|
|
121
|
+
)
|
|
122
|
+
console.print(f" â
Flutter {current} satisfies >={required}")
|
|
123
|
+
|
|
124
|
+
def _reclone_flutter(self) -> None:
|
|
125
|
+
"""Reclone Flutter repository."""
|
|
126
|
+
console.print(f" đ Recloning Flutter ({self.config.channel})...")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
if self.flutter_root.exists():
|
|
130
|
+
import shutil
|
|
131
|
+
|
|
132
|
+
shutil.rmtree(self.flutter_root)
|
|
133
|
+
|
|
134
|
+
self._install_flutter()
|
|
135
|
+
except Exception as e:
|
|
136
|
+
raise FlutterInstallationError(f"Failed to reclone Flutter: {e}")
|
|
137
|
+
|
|
138
|
+
def _install_flutter(self) -> None:
|
|
139
|
+
"""Install Flutter SDK."""
|
|
140
|
+
console.print(f" đĨ Installing Flutter ({self.config.channel})...")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
# Create parent directory
|
|
144
|
+
self.flutter_root.parent.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
|
|
146
|
+
# Clone Flutter repository
|
|
147
|
+
subprocess.run(
|
|
148
|
+
[
|
|
149
|
+
"git",
|
|
150
|
+
"clone",
|
|
151
|
+
"--depth",
|
|
152
|
+
"1",
|
|
153
|
+
"-b",
|
|
154
|
+
self.config.channel,
|
|
155
|
+
"https://github.com/flutter/flutter.git",
|
|
156
|
+
str(self.flutter_root),
|
|
157
|
+
],
|
|
158
|
+
check=True,
|
|
159
|
+
capture_output=True,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
console.print(" â
Flutter installed")
|
|
163
|
+
|
|
164
|
+
except subprocess.CalledProcessError as e:
|
|
165
|
+
raise FlutterInstallationError(f"Failed to install Flutter: {e}")
|
|
166
|
+
|
|
167
|
+
def _update_flutter(self) -> None:
|
|
168
|
+
"""Update existing Flutter installation."""
|
|
169
|
+
console.print(f" đ Updating Flutter ({self.config.channel})...")
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
# Set remote URL
|
|
173
|
+
subprocess.run(
|
|
174
|
+
[
|
|
175
|
+
"git",
|
|
176
|
+
"remote",
|
|
177
|
+
"set-url",
|
|
178
|
+
"origin",
|
|
179
|
+
"https://github.com/flutter/flutter.git",
|
|
180
|
+
],
|
|
181
|
+
cwd=self.flutter_root,
|
|
182
|
+
check=False,
|
|
183
|
+
capture_output=True,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Fetch latest changes
|
|
187
|
+
subprocess.run(
|
|
188
|
+
["git", "fetch", "origin", "--prune"],
|
|
189
|
+
cwd=self.flutter_root,
|
|
190
|
+
check=True,
|
|
191
|
+
capture_output=True,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Try to checkout the target channel
|
|
195
|
+
try:
|
|
196
|
+
subprocess.run(
|
|
197
|
+
["git", "checkout", self.config.channel],
|
|
198
|
+
cwd=self.flutter_root,
|
|
199
|
+
check=True,
|
|
200
|
+
capture_output=True,
|
|
201
|
+
)
|
|
202
|
+
except subprocess.CalledProcessError:
|
|
203
|
+
# Create new branch if it doesn't exist
|
|
204
|
+
subprocess.run(
|
|
205
|
+
[
|
|
206
|
+
"git",
|
|
207
|
+
"checkout",
|
|
208
|
+
"-b",
|
|
209
|
+
self.config.channel,
|
|
210
|
+
f"origin/{self.config.channel}",
|
|
211
|
+
],
|
|
212
|
+
cwd=self.flutter_root,
|
|
213
|
+
check=True,
|
|
214
|
+
capture_output=True,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Try fast-forward merge
|
|
218
|
+
try:
|
|
219
|
+
subprocess.run(
|
|
220
|
+
["git", "merge", "--ff-only", f"origin/{self.config.channel}"],
|
|
221
|
+
cwd=self.flutter_root,
|
|
222
|
+
check=True,
|
|
223
|
+
capture_output=True,
|
|
224
|
+
)
|
|
225
|
+
console.print(" â
Flutter updated (fast-forward)")
|
|
226
|
+
return
|
|
227
|
+
except subprocess.CalledProcessError:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
# Handle diverged branches
|
|
231
|
+
self._handle_diverged_branches()
|
|
232
|
+
|
|
233
|
+
except subprocess.CalledProcessError as e:
|
|
234
|
+
raise FlutterInstallationError(f"Failed to update Flutter: {e}")
|
|
235
|
+
|
|
236
|
+
def _handle_diverged_branches(self) -> None:
|
|
237
|
+
"""Handle diverged Git branches."""
|
|
238
|
+
if self.config.flutter_update_mode == "skip":
|
|
239
|
+
console.print(
|
|
240
|
+
" â ī¸ Flutter repo has diverged; skipping update (per --flutter-update skip)"
|
|
241
|
+
)
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
console.print(" â ī¸ Flutter repo has diverged from origin")
|
|
245
|
+
|
|
246
|
+
# Get commit counts
|
|
247
|
+
try:
|
|
248
|
+
result = subprocess.run(
|
|
249
|
+
[
|
|
250
|
+
"git",
|
|
251
|
+
"rev-list",
|
|
252
|
+
"--left-right",
|
|
253
|
+
"--count",
|
|
254
|
+
f"origin/{self.config.channel}...{self.config.channel}",
|
|
255
|
+
],
|
|
256
|
+
cwd=self.flutter_root,
|
|
257
|
+
capture_output=True,
|
|
258
|
+
text=True,
|
|
259
|
+
check=True,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
counts = result.stdout.strip().split()
|
|
263
|
+
if len(counts) == 2:
|
|
264
|
+
left_ahead = counts[0]
|
|
265
|
+
right_ahead = counts[1]
|
|
266
|
+
console.print(
|
|
267
|
+
f" đ Local ahead by: {right_ahead}, origin ahead by: {left_ahead}"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
except subprocess.CalledProcessError:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
# For now, we'll hard reset in reset mode
|
|
274
|
+
if self.config.flutter_update_mode == "reset":
|
|
275
|
+
console.print(" đ Resetting Flutter to origin (discarding local changes)")
|
|
276
|
+
try:
|
|
277
|
+
subprocess.run(
|
|
278
|
+
["git", "reset", "--hard", f"origin/{self.config.channel}"],
|
|
279
|
+
cwd=self.flutter_root,
|
|
280
|
+
check=True,
|
|
281
|
+
capture_output=True,
|
|
282
|
+
)
|
|
283
|
+
console.print(" â
Flutter reset to origin")
|
|
284
|
+
except subprocess.CalledProcessError as e:
|
|
285
|
+
raise FlutterInstallationError(f"Failed to reset Flutter: {e}")
|
|
286
|
+
|
|
287
|
+
def _ensure_flutter_path(self) -> None:
|
|
288
|
+
"""Ensure Flutter is in PATH."""
|
|
289
|
+
console.print(" đ§ Configuring Flutter PATH...")
|
|
290
|
+
|
|
291
|
+
flutter_path = f'export PATH="{self.flutter_root}/bin:$PATH"'
|
|
292
|
+
flutter_path_pattern = (
|
|
293
|
+
r'export\s+PATH=["\']([^"\']*flutter[^"\']*/bin):\$PATH["\']'
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
for profile in self.path_profiles:
|
|
297
|
+
if profile.exists():
|
|
298
|
+
with open(profile, "r") as f:
|
|
299
|
+
content = f.read()
|
|
300
|
+
|
|
301
|
+
existing_match = re.search(flutter_path_pattern, content)
|
|
302
|
+
if existing_match:
|
|
303
|
+
existing_flutter_path = existing_match.group(1)
|
|
304
|
+
normalized_existing_path = self._normalize_flutter_bin_path(
|
|
305
|
+
existing_flutter_path
|
|
306
|
+
)
|
|
307
|
+
if normalized_existing_path == self._expected_flutter_bin_path():
|
|
308
|
+
console.print(f" â
Flutter PATH already in {profile.name}")
|
|
309
|
+
else:
|
|
310
|
+
console.print(
|
|
311
|
+
f" â ī¸ Different Flutter PATH in {profile.name}: {existing_flutter_path}"
|
|
312
|
+
)
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
with open(profile, "a") as f:
|
|
316
|
+
f.write(f"\n{flutter_path}\n")
|
|
317
|
+
console.print(f" â
Flutter PATH added to {profile.name}")
|
|
318
|
+
else:
|
|
319
|
+
with open(profile, "w") as f:
|
|
320
|
+
f.write(f"{flutter_path}\n")
|
|
321
|
+
console.print(f" â
Flutter PATH added to {profile.name}")
|
|
322
|
+
|
|
323
|
+
# Add to current environment
|
|
324
|
+
flutter_bin = self.flutter_root / "bin"
|
|
325
|
+
if str(flutter_bin) not in sys.path:
|
|
326
|
+
sys.path.insert(0, str(flutter_bin))
|
|
327
|
+
|
|
328
|
+
def _detect_android_sdk_root(self) -> Path | None:
|
|
329
|
+
"""Return the Android SDK root, checking env vars then common locations."""
|
|
330
|
+
import os
|
|
331
|
+
|
|
332
|
+
for var in ("ANDROID_HOME", "ANDROID_SDK_ROOT"):
|
|
333
|
+
val = os.getenv(var)
|
|
334
|
+
if val:
|
|
335
|
+
path = Path(val)
|
|
336
|
+
if path.exists():
|
|
337
|
+
return path
|
|
338
|
+
|
|
339
|
+
common = [
|
|
340
|
+
Path("/opt/android-sdk"),
|
|
341
|
+
self.home / "Android" / "Sdk",
|
|
342
|
+
self.home / "Library" / "Android" / "sdk",
|
|
343
|
+
]
|
|
344
|
+
for path in common:
|
|
345
|
+
if path.exists():
|
|
346
|
+
return path
|
|
347
|
+
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
def _ensure_android_env(self) -> None:
|
|
351
|
+
"""Write ANDROID_HOME and Android SDK PATH entries to shell profiles."""
|
|
352
|
+
console.print(" đ¤ Configuring Android SDK environment...")
|
|
353
|
+
|
|
354
|
+
sdk_root = self._detect_android_sdk_root()
|
|
355
|
+
if sdk_root is None:
|
|
356
|
+
console.print(
|
|
357
|
+
" â ī¸ Android SDK not found; skipping ANDROID_HOME configuration"
|
|
358
|
+
)
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
android_home_export = f'export ANDROID_HOME="{sdk_root}"'
|
|
362
|
+
android_path_export = (
|
|
363
|
+
'export PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin'
|
|
364
|
+
":$ANDROID_HOME/platform-tools"
|
|
365
|
+
':$ANDROID_HOME/emulator"'
|
|
366
|
+
)
|
|
367
|
+
marker = "# Android SDK"
|
|
368
|
+
|
|
369
|
+
for profile in self.path_profiles:
|
|
370
|
+
if profile.exists():
|
|
371
|
+
content = profile.read_text()
|
|
372
|
+
else:
|
|
373
|
+
content = ""
|
|
374
|
+
|
|
375
|
+
if "ANDROID_HOME" in content:
|
|
376
|
+
console.print(f" â
Android SDK env already in {profile.name}")
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
with open(profile, "a") as f:
|
|
380
|
+
f.write(f"\n{marker}\n{android_home_export}\n{android_path_export}\n")
|
|
381
|
+
console.print(f" â
Android SDK env added to {profile.name}")
|
|
382
|
+
|
|
383
|
+
def _run_flutter_doctor(self) -> None:
|
|
384
|
+
"""Run flutter doctor to check setup."""
|
|
385
|
+
console.print(" đĨ Running Flutter doctor...")
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
# Run flutter doctor
|
|
389
|
+
result = subprocess.run(
|
|
390
|
+
[str(self.flutter_root / "bin" / "flutter"), "doctor", "-v"],
|
|
391
|
+
capture_output=True,
|
|
392
|
+
text=True,
|
|
393
|
+
check=False,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Flutter doctor outputs to stdout, but may also use stderr
|
|
397
|
+
output = result.stdout or result.stderr or ""
|
|
398
|
+
|
|
399
|
+
if result.returncode == 0:
|
|
400
|
+
console.print(" â
Flutter doctor passed")
|
|
401
|
+
else:
|
|
402
|
+
console.print(" â ī¸ Flutter doctor found issues:")
|
|
403
|
+
# Print output (stdout takes precedence, fallback to stderr)
|
|
404
|
+
if result.stdout:
|
|
405
|
+
console.print(result.stdout)
|
|
406
|
+
if result.stderr:
|
|
407
|
+
console.print(result.stderr)
|
|
408
|
+
|
|
409
|
+
# Check for Android licenses in either stream
|
|
410
|
+
combined_output = output
|
|
411
|
+
if "Some Android licenses not accepted" in combined_output:
|
|
412
|
+
console.print(" đą Android licenses need acceptance")
|
|
413
|
+
self._handle_android_licenses()
|
|
414
|
+
if self.platform == "linux":
|
|
415
|
+
self._print_linux_doctor_guidance(combined_output)
|
|
416
|
+
|
|
417
|
+
except Exception as e:
|
|
418
|
+
console.print(f" â ī¸ Flutter doctor warning: {e}")
|
|
419
|
+
|
|
420
|
+
def _print_linux_doctor_guidance(self, output: str) -> None:
|
|
421
|
+
"""Print Linux-specific remediation hints for doctor issues."""
|
|
422
|
+
if "Linux toolchain" in output or "Unable to locate" in output:
|
|
423
|
+
console.print(" đ§ Linux toolchain issues detected")
|
|
424
|
+
console.print(
|
|
425
|
+
" âšī¸ Ensure required packages are installed via apt (git curl unzip xz-utils zip libglu1-mesa clang cmake ninja-build pkg-config)"
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def _handle_android_licenses(self) -> None:
|
|
429
|
+
"""Handle Android license acceptance."""
|
|
430
|
+
if "android" not in self.config.platforms:
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
console.print(" đą Android licenses not accepted")
|
|
434
|
+
console.print(
|
|
435
|
+
" âšī¸ You can run 'flutter doctor --android-licenses' later to accept licenses"
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
def check_only(self) -> bool:
|
|
439
|
+
"""Check Flutter SDK installation without installing/updating. Returns True if all checks pass."""
|
|
440
|
+
all_ok = True
|
|
441
|
+
|
|
442
|
+
# Check if Flutter is installed
|
|
443
|
+
console.print(" đ Checking Flutter SDK installation...")
|
|
444
|
+
if not self.flutter_root.exists() or not (self.flutter_root / ".git").exists():
|
|
445
|
+
console.print(f" â Flutter SDK not found at {self.flutter_root}")
|
|
446
|
+
all_ok = False
|
|
447
|
+
else:
|
|
448
|
+
console.print(f" â
Flutter SDK found at {self.flutter_root}")
|
|
449
|
+
|
|
450
|
+
# Check Flutter binary
|
|
451
|
+
flutter_bin = self.flutter_root / "bin" / "flutter"
|
|
452
|
+
if not flutter_bin.exists():
|
|
453
|
+
console.print(" â Flutter binary not found")
|
|
454
|
+
all_ok = False
|
|
455
|
+
else:
|
|
456
|
+
console.print(" â
Flutter binary found")
|
|
457
|
+
|
|
458
|
+
# Check Flutter PATH configuration
|
|
459
|
+
console.print(" đ§ Checking Flutter PATH configuration...")
|
|
460
|
+
flutter_path_pattern = (
|
|
461
|
+
r'export\s+PATH=["\']([^"\']*flutter[^"\']*/bin):\$PATH["\']'
|
|
462
|
+
)
|
|
463
|
+
expected_path = self._expected_flutter_bin_path()
|
|
464
|
+
found_expected = False
|
|
465
|
+
found_any = False
|
|
466
|
+
for profile in self.path_profiles:
|
|
467
|
+
if not profile.exists():
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
with open(profile, "r") as f:
|
|
471
|
+
content = f.read()
|
|
472
|
+
|
|
473
|
+
existing_match = re.search(flutter_path_pattern, content)
|
|
474
|
+
if existing_match:
|
|
475
|
+
found_any = True
|
|
476
|
+
existing_flutter_path = existing_match.group(1)
|
|
477
|
+
normalized_existing_path = self._normalize_flutter_bin_path(
|
|
478
|
+
existing_flutter_path
|
|
479
|
+
)
|
|
480
|
+
if normalized_existing_path == expected_path:
|
|
481
|
+
console.print(
|
|
482
|
+
f" â
Flutter PATH correctly configured in {profile.name}"
|
|
483
|
+
)
|
|
484
|
+
found_expected = True
|
|
485
|
+
else:
|
|
486
|
+
console.print(
|
|
487
|
+
f" â ī¸ Different Flutter PATH in {profile.name}: {existing_flutter_path}"
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
if not found_any:
|
|
491
|
+
console.print(" â ī¸ Flutter PATH not found in shell profiles")
|
|
492
|
+
all_ok = False
|
|
493
|
+
elif not found_expected:
|
|
494
|
+
all_ok = False
|
|
495
|
+
|
|
496
|
+
# Check if Flutter is up to date (if installed)
|
|
497
|
+
if self.flutter_root.exists() and (self.flutter_root / ".git").exists():
|
|
498
|
+
console.print(
|
|
499
|
+
f" đ Checking Flutter update status ({self.config.channel})..."
|
|
500
|
+
)
|
|
501
|
+
try:
|
|
502
|
+
# Fetch latest changes
|
|
503
|
+
subprocess.run(
|
|
504
|
+
["git", "fetch", "origin", "--prune"],
|
|
505
|
+
cwd=self.flutter_root,
|
|
506
|
+
check=True,
|
|
507
|
+
capture_output=True,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Check if local branch is up to date
|
|
511
|
+
result = subprocess.run(
|
|
512
|
+
[
|
|
513
|
+
"git",
|
|
514
|
+
"rev-list",
|
|
515
|
+
"--left-right",
|
|
516
|
+
"--count",
|
|
517
|
+
f"origin/{self.config.channel}...{self.config.channel}",
|
|
518
|
+
],
|
|
519
|
+
cwd=self.flutter_root,
|
|
520
|
+
capture_output=True,
|
|
521
|
+
text=True,
|
|
522
|
+
check=True,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
counts = result.stdout.strip().split()
|
|
526
|
+
if len(counts) == 2:
|
|
527
|
+
left_ahead = int(counts[0])
|
|
528
|
+
right_ahead = int(counts[1])
|
|
529
|
+
if left_ahead == 0 and right_ahead == 0:
|
|
530
|
+
console.print(" â
Flutter is up to date")
|
|
531
|
+
elif left_ahead > 0:
|
|
532
|
+
console.print(
|
|
533
|
+
f" â ī¸ Flutter is {left_ahead} commit(s) behind origin"
|
|
534
|
+
)
|
|
535
|
+
elif right_ahead > 0:
|
|
536
|
+
console.print(
|
|
537
|
+
f" âšī¸ Flutter has {right_ahead} local commit(s) ahead of origin"
|
|
538
|
+
)
|
|
539
|
+
except subprocess.CalledProcessError:
|
|
540
|
+
console.print(" â ī¸ Could not check Flutter update status")
|
|
541
|
+
|
|
542
|
+
# Run flutter doctor
|
|
543
|
+
if (
|
|
544
|
+
self.flutter_root.exists()
|
|
545
|
+
and (self.flutter_root / "bin" / "flutter").exists()
|
|
546
|
+
):
|
|
547
|
+
console.print(" đĨ Running Flutter doctor...")
|
|
548
|
+
try:
|
|
549
|
+
result = subprocess.run(
|
|
550
|
+
[str(self.flutter_root / "bin" / "flutter"), "doctor", "-v"],
|
|
551
|
+
capture_output=True,
|
|
552
|
+
text=True,
|
|
553
|
+
check=False,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# Flutter doctor outputs to stdout, but may also use stderr
|
|
557
|
+
output = result.stdout or result.stderr or ""
|
|
558
|
+
|
|
559
|
+
if result.returncode == 0:
|
|
560
|
+
console.print(" â
Flutter doctor passed")
|
|
561
|
+
else:
|
|
562
|
+
console.print(" â ī¸ Flutter doctor found issues:")
|
|
563
|
+
# Print only the summary, not the full verbose output
|
|
564
|
+
# Check both stdout (primary) and stderr (secondary)
|
|
565
|
+
lines = output.split("\n")
|
|
566
|
+
summary_lines = [
|
|
567
|
+
line
|
|
568
|
+
for line in lines
|
|
569
|
+
if "âĸ" in line or "!" in line or "â" in line or "â" in line
|
|
570
|
+
]
|
|
571
|
+
if summary_lines:
|
|
572
|
+
for line in summary_lines[:10]: # Limit output
|
|
573
|
+
console.print(f" {line}")
|
|
574
|
+
all_ok = False
|
|
575
|
+
except Exception as e:
|
|
576
|
+
console.print(f" â ī¸ Flutter doctor warning: {e}")
|
|
577
|
+
all_ok = False
|
|
578
|
+
|
|
579
|
+
return all_ok
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Platform detection helpers."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
SetupPlatform = Literal["darwin", "linux", "unsupported"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def detect_runtime_platform() -> SetupPlatform:
|
|
10
|
+
"""Return the normalized runtime platform for setup flows."""
|
|
11
|
+
if sys.platform == "darwin":
|
|
12
|
+
return "darwin"
|
|
13
|
+
if sys.platform.startswith("linux"):
|
|
14
|
+
return "linux"
|
|
15
|
+
return "unsupported"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Platform-aware prerequisites facade for Flutter setup."""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from .config import Config
|
|
6
|
+
from .exceptions import PrerequisitesError
|
|
7
|
+
from .platform import detect_runtime_platform
|
|
8
|
+
from .prerequisites_linux import LinuxPrerequisites
|
|
9
|
+
from .prerequisites_macos import MacOSPrerequisites
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _PrerequisitesBackend(Protocol):
|
|
13
|
+
def check_and_install(self) -> None: ...
|
|
14
|
+
def check_only(self) -> bool: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PrerequisitesManager:
|
|
18
|
+
"""Manages system prerequisites via platform-specific backends."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: Config):
|
|
21
|
+
"""Initialize PrerequisitesManager."""
|
|
22
|
+
self.config = config
|
|
23
|
+
self.platform = detect_runtime_platform()
|
|
24
|
+
self.backend = self._select_backend()
|
|
25
|
+
|
|
26
|
+
def _select_backend(self) -> _PrerequisitesBackend:
|
|
27
|
+
if self.platform == "darwin":
|
|
28
|
+
return MacOSPrerequisites(self.config)
|
|
29
|
+
if self.platform == "linux":
|
|
30
|
+
return LinuxPrerequisites(self.config)
|
|
31
|
+
raise PrerequisitesError(f"Unsupported host platform: {self.platform}")
|
|
32
|
+
|
|
33
|
+
def check_and_install(self) -> None:
|
|
34
|
+
"""Check and install prerequisites for the active platform."""
|
|
35
|
+
self.backend.check_and_install()
|
|
36
|
+
|
|
37
|
+
def check_only(self) -> bool:
|
|
38
|
+
"""Check prerequisites for the active platform."""
|
|
39
|
+
return self.backend.check_only()
|