ndev-stack 0.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.
Files changed (79) hide show
  1. ndev/__init__.py +8 -0
  2. ndev/__main__.py +4 -0
  3. ndev/cli.py +24 -0
  4. ndev/common/__init__.py +3 -0
  5. ndev/common/config.py +114 -0
  6. ndev/common/constants.py +51 -0
  7. ndev/common/github.py +13 -0
  8. ndev/common/logger.py +11 -0
  9. ndev/common/manifest.py +41 -0
  10. ndev/common/utils.py +96 -0
  11. ndev/linux/__init__.py +1 -0
  12. ndev/linux/chroot/manager.py +63 -0
  13. ndev/linux/chroot/packages.py +91 -0
  14. ndev/linux/chroot/shell.py +9 -0
  15. ndev/linux/cli.py +235 -0
  16. ndev/linux/commands/available.py +38 -0
  17. ndev/linux/commands/clean.py +25 -0
  18. ndev/linux/commands/ctl.py +192 -0
  19. ndev/linux/commands/current.py +11 -0
  20. ndev/linux/commands/db.py +319 -0
  21. ndev/linux/commands/doctor.py +56 -0
  22. ndev/linux/commands/grok.py +75 -0
  23. ndev/linux/commands/install.py +39 -0
  24. ndev/linux/commands/list.py +47 -0
  25. ndev/linux/commands/logs.py +36 -0
  26. ndev/linux/commands/mailpit.py +82 -0
  27. ndev/linux/commands/reload.py +26 -0
  28. ndev/linux/commands/restart.py +34 -0
  29. ndev/linux/commands/setup.py +113 -0
  30. ndev/linux/commands/start.py +34 -0
  31. ndev/linux/commands/status.py +81 -0
  32. ndev/linux/commands/stop.py +34 -0
  33. ndev/linux/commands/uninstall.py +69 -0
  34. ndev/linux/commands/update.py +57 -0
  35. ndev/linux/commands/upgrade.py +81 -0
  36. ndev/linux/commands/use.py +108 -0
  37. ndev/linux/commands/vhost.py +350 -0
  38. ndev/linux/php/builder.py +183 -0
  39. ndev/linux/php/downloader.py +58 -0
  40. ndev/linux/php/extensions.py +146 -0
  41. ndev/linux/php/installer.py +42 -0
  42. ndev/linux/php/resolver.py +59 -0
  43. ndev/linux/php/templates.py +128 -0
  44. ndev/linux/runtime/fpm.py +117 -0
  45. ndev/linux/runtime/mailpit.py +244 -0
  46. ndev/linux/runtime/pma.py +223 -0
  47. ndev/linux/runtime/process.py +37 -0
  48. ndev/linux/runtime/sockets.py +16 -0
  49. ndev/linux/runtime/upgrade.py +431 -0
  50. ndev/linux/tui.py +1423 -0
  51. ndev/main.py +52 -0
  52. ndev/tui.py +23 -0
  53. ndev/win/__init__.py +1 -0
  54. ndev/win/cli.py +1898 -0
  55. ndev/win/commands/__init__.py +0 -0
  56. ndev/win/core/__init__.py +0 -0
  57. ndev/win/core/db.py +265 -0
  58. ndev/win/core/elevate.py +94 -0
  59. ndev/win/core/ext.py +241 -0
  60. ndev/win/core/fcgi.py +216 -0
  61. ndev/win/core/grok.py +55 -0
  62. ndev/win/core/logs.py +66 -0
  63. ndev/win/core/mailpit.py +236 -0
  64. ndev/win/core/mkcert.py +65 -0
  65. ndev/win/core/paths.py +85 -0
  66. ndev/win/core/php.py +533 -0
  67. ndev/win/core/pma.py +190 -0
  68. ndev/win/core/services.py +349 -0
  69. ndev/win/core/setup.py +361 -0
  70. ndev/win/core/upgrade.py +513 -0
  71. ndev/win/core/vhost.py +289 -0
  72. ndev/win/templates/vhost.conf.tmpl +33 -0
  73. ndev/win/templates/vhost_ssl.conf.tmpl +43 -0
  74. ndev/win/tui.py +1313 -0
  75. ndev_stack-0.1.0.dist-info/METADATA +553 -0
  76. ndev_stack-0.1.0.dist-info/RECORD +79 -0
  77. ndev_stack-0.1.0.dist-info/WHEEL +5 -0
  78. ndev_stack-0.1.0.dist-info/entry_points.txt +4 -0
  79. ndev_stack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,513 @@
1
+ """
2
+ Component version check and upgrade management for Windows.
3
+ Manages updates for Nginx, Mailpit, MariaDB, phpMyAdmin, mkcert, and Composer.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import re
9
+ import shutil
10
+ import subprocess
11
+ import urllib.request
12
+ import zipfile
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ from . import fcgi, logs, mailpit as mailpit_core, paths, pma as pma_core, services, setup as setup_core
18
+
19
+ USER_AGENT = "ndev/0.1.0"
20
+
21
+
22
+ @dataclass
23
+ class ComponentInfo:
24
+ name: str
25
+ display_name: str
26
+ current_version: Optional[str]
27
+ latest_version: Optional[str]
28
+ update_available: bool
29
+ installed: bool
30
+ status: str
31
+ error: Optional[str] = None
32
+
33
+
34
+ def _http_get_json(url: str, timeout: int = 10) -> dict:
35
+ req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
36
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
37
+ return json.loads(resp.read().decode("utf-8"))
38
+
39
+
40
+ def _http_get_text(url: str, timeout: int = 10) -> str:
41
+ req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
42
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
43
+ return resp.read().decode("utf-8", errors="ignore")
44
+
45
+
46
+ def _clean_ver(v: Optional[str]) -> str:
47
+ if not v:
48
+ return ""
49
+ v = v.strip().lower()
50
+ if v.startswith("v"):
51
+ v = v[1:]
52
+ # Remove git commit hashes or dates, e.g. "2.10.3 2026-..."
53
+ return v.split()[0]
54
+
55
+
56
+ # ── COMPONENT DETECTORS & UPDATERS ──────────────────────────────────────────
57
+
58
+ # 1. NGINX
59
+ def get_nginx_info() -> ComponentInfo:
60
+ installed = services.nginx_is_installed()
61
+ curr_ver = None
62
+ latest_ver = None
63
+ err = None
64
+
65
+ if installed:
66
+ try:
67
+ res = subprocess.run([str(services.nginx_exe()), "-v"], capture_output=True, text=True, timeout=5)
68
+ out = res.stderr or res.stdout
69
+ m = re.search(r"nginx/(\d+\.\d+\.\d+)", out)
70
+ if m:
71
+ curr_ver = m.group(1)
72
+ except Exception as e:
73
+ err = str(e)
74
+
75
+ try:
76
+ html = _http_get_text("https://nginx.org/en/download.html")
77
+ matches = re.findall(r"nginx-(\d+\.\d+\.\d+)\.zip", html)
78
+ if matches:
79
+ latest_ver = matches[0]
80
+ else:
81
+ latest_ver = setup_core.DEFAULT_NGINX_VERSION
82
+ except Exception as e:
83
+ latest_ver = setup_core.DEFAULT_NGINX_VERSION
84
+ if not err:
85
+ err = f"Could not query remote version: {e}"
86
+
87
+ update_avail = False
88
+ if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
89
+ update_avail = True
90
+
91
+ status = "Up-to-date"
92
+ if not installed:
93
+ status = "Not Installed"
94
+ elif update_avail:
95
+ status = f"Update Available ({curr_ver} -> {latest_ver})"
96
+
97
+ return ComponentInfo("nginx", "Nginx Web Server", curr_ver, latest_ver, update_avail, installed, status, err)
98
+
99
+
100
+ def upgrade_nginx() -> tuple[bool, str]:
101
+ was_running = services.nginx_is_running()
102
+ if was_running:
103
+ services.nginx_stop()
104
+
105
+ info = get_nginx_info()
106
+ target_ver = info.latest_version or setup_core.DEFAULT_NGINX_VERSION
107
+
108
+ # 1. Create a persistent safety backup of conf/
109
+ conf_dir = paths.NGINX_DIR / "conf"
110
+ if conf_dir.exists() and any(conf_dir.iterdir()):
111
+ import datetime
112
+ ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
113
+ backup_dest = paths.BACKUPS_DIR / f"nginx_conf_{ts}"
114
+ try:
115
+ shutil.copytree(conf_dir, backup_dest)
116
+ except Exception:
117
+ pass
118
+
119
+ try:
120
+ setup_core.install_nginx(version=target_ver)
121
+
122
+ if was_running:
123
+ services.nginx_start()
124
+
125
+ return True, f"Nginx upgraded successfully to {target_ver} (configuration preserved)."
126
+ except Exception as e:
127
+ if was_running:
128
+ try:
129
+ services.nginx_start()
130
+ except Exception:
131
+ pass
132
+ return False, f"Nginx upgrade failed: {e}"
133
+
134
+
135
+ # 2. MAILPIT
136
+ def get_mailpit_info() -> ComponentInfo:
137
+ installed = mailpit_core.is_installed()
138
+ curr_ver = None
139
+ latest_ver = None
140
+ err = None
141
+
142
+ if installed:
143
+ try:
144
+ res = subprocess.run([str(mailpit_core.BINARY_PATH), "version"], capture_output=True, text=True, timeout=5)
145
+ out = res.stdout.strip() or res.stderr.strip()
146
+ m = re.search(r"v?(\d+\.\d+\.\d+)", out)
147
+ if m:
148
+ curr_ver = f"v{m.group(1)}"
149
+ else:
150
+ curr_ver = out
151
+ except Exception as e:
152
+ err = str(e)
153
+
154
+ try:
155
+ rel = mailpit_core._fetch_latest_release()
156
+ latest_ver = rel.get("tag_name")
157
+ except Exception as e:
158
+ if not err:
159
+ err = f"Could not query GitHub releases: {e}"
160
+
161
+ update_avail = False
162
+ if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
163
+ update_avail = True
164
+
165
+ status = "Up-to-date"
166
+ if not installed:
167
+ status = "Not Installed"
168
+ elif update_avail:
169
+ status = f"Update Available ({curr_ver} -> {latest_ver})"
170
+
171
+ return ComponentInfo("mailpit", "Mailpit Email Sandbox", curr_ver, latest_ver, update_avail, installed, status, err)
172
+
173
+
174
+ def upgrade_mailpit() -> tuple[bool, str]:
175
+ was_running = bool(mailpit_core.status())
176
+ if was_running:
177
+ mailpit_core.stop()
178
+
179
+ try:
180
+ paths.ensure_dirs()
181
+ release = mailpit_core._fetch_latest_release()
182
+ version = release.get("tag_name", "unknown")
183
+ asset = mailpit_core._find_windows_asset(release)
184
+ if not asset:
185
+ raise RuntimeError("No Windows asset found in latest Mailpit release.")
186
+
187
+ url = asset["browser_download_url"]
188
+ dl_path = paths.DOWNLOADS_DIR / asset["name"]
189
+ if dl_path.exists():
190
+ dl_path.unlink()
191
+
192
+ setup_core._download(url, dl_path)
193
+ with zipfile.ZipFile(dl_path) as zf:
194
+ for member in zf.namelist():
195
+ if member.lower().endswith(".exe") and "mailpit" in member.lower():
196
+ with zf.open(member) as src, open(mailpit_core.BINARY_PATH, "wb") as dst:
197
+ shutil.copyfileobj(src, dst)
198
+ break
199
+
200
+ if was_running:
201
+ mailpit_core.start()
202
+
203
+ return True, f"Mailpit upgraded successfully to {version}."
204
+ except Exception as e:
205
+ if was_running:
206
+ try:
207
+ mailpit_core.start()
208
+ except Exception:
209
+ pass
210
+ return False, f"Mailpit upgrade failed: {e}"
211
+
212
+
213
+ # 3. MARIADB
214
+ def get_mariadb_info() -> ComponentInfo:
215
+ installed = services.mariadb_is_installed()
216
+ curr_ver = None
217
+ latest_ver = None
218
+ err = None
219
+
220
+ if installed:
221
+ try:
222
+ mysqld = paths.MARIADB_DIR / "bin" / "mariadbd.exe"
223
+ if not mysqld.exists():
224
+ mysqld = paths.MARIADB_DIR / "bin" / "mysqld.exe"
225
+ if mysqld.exists():
226
+ res = subprocess.run([str(mysqld), "--version"], capture_output=True, text=True, timeout=5)
227
+ m = re.search(r"(\d+\.\d+\.\d+)", res.stdout or res.stderr)
228
+ if m:
229
+ curr_ver = m.group(1)
230
+ except Exception as e:
231
+ err = str(e)
232
+
233
+ try:
234
+ data = _http_get_json("https://downloads.mariadb.org/rest-api/mariadb/11.4/")
235
+ releases = list(data.get("releases", {}).keys())
236
+ if releases:
237
+ latest_ver = releases[0]
238
+ else:
239
+ latest_ver = setup_core.DEFAULT_MARIADB_VERSION
240
+ except Exception as e:
241
+ latest_ver = setup_core.DEFAULT_MARIADB_VERSION
242
+ if not err:
243
+ err = f"Could not query MariaDB release API: {e}"
244
+
245
+ update_avail = False
246
+ if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
247
+ update_avail = True
248
+
249
+ status = "Up-to-date"
250
+ if not installed:
251
+ status = "Not Installed"
252
+ elif update_avail:
253
+ status = f"Update Available ({curr_ver} -> {latest_ver})"
254
+
255
+ return ComponentInfo("mariadb", "MariaDB Server", curr_ver, latest_ver, update_avail, installed, status, err)
256
+
257
+
258
+ def upgrade_mariadb() -> tuple[bool, str]:
259
+ mb_st = services.mariadb_status()
260
+ was_running = mb_st and mb_st.get("running")
261
+ if was_running:
262
+ services.mariadb_stop()
263
+
264
+ info = get_mariadb_info()
265
+ target_ver = info.latest_version or setup_core.DEFAULT_MARIADB_VERSION
266
+
267
+ # 1. Create a persistent safety snapshot backup of data/ and my.ini
268
+ data_dir = paths.MARIADB_DIR / "data"
269
+ my_ini = paths.MARIADB_DIR / "my.ini"
270
+ if data_dir.exists() and any(data_dir.iterdir()):
271
+ import datetime
272
+ ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
273
+ backup_dest = paths.BACKUPS_DIR / f"mariadb_backup_{ts}"
274
+ try:
275
+ backup_dest.mkdir(parents=True, exist_ok=True)
276
+ shutil.copytree(data_dir, backup_dest / "data")
277
+ if my_ini.exists():
278
+ shutil.copy2(my_ini, backup_dest / "my.ini")
279
+ except Exception:
280
+ pass
281
+
282
+ try:
283
+ setup_core.install_mariadb(version=target_ver)
284
+
285
+ if was_running:
286
+ services.mariadb_start()
287
+
288
+ return True, f"MariaDB upgraded successfully to {target_ver} (database and configuration preserved)."
289
+ except Exception as e:
290
+ if was_running:
291
+ try:
292
+ services.mariadb_start()
293
+ except Exception:
294
+ pass
295
+ return False, f"MariaDB upgrade failed: {e}"
296
+
297
+
298
+ # 4. PHPMYADMIN
299
+ def get_pma_info() -> ComponentInfo:
300
+ installed = (paths.PMA_DIR / "index.php").exists()
301
+ curr_ver = None
302
+ latest_ver = None
303
+ err = None
304
+
305
+ if installed:
306
+ for f in paths.PMA_DIR.glob("RELEASE-DATE-*"):
307
+ curr_ver = f.name.replace("RELEASE-DATE-", "")
308
+ break
309
+ if not curr_ver:
310
+ curr_ver = pma_core.DEFAULT_VERSION
311
+
312
+ try:
313
+ data = _http_get_json("https://www.phpmyadmin.net/home_page/version.json")
314
+ latest_ver = data.get("version", pma_core.DEFAULT_VERSION)
315
+ except Exception as e:
316
+ latest_ver = pma_core.DEFAULT_VERSION
317
+ if not err:
318
+ err = f"Could not query PMA version API: {e}"
319
+
320
+ update_avail = False
321
+ if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
322
+ update_avail = True
323
+
324
+ status = "Up-to-date"
325
+ if not installed:
326
+ status = "Not Installed"
327
+ elif update_avail:
328
+ status = f"Update Available ({curr_ver} -> {latest_ver})"
329
+
330
+ return ComponentInfo("pma", "PMA (phpMyAdmin)", curr_ver, latest_ver, update_avail, installed, status, err)
331
+
332
+
333
+ def upgrade_pma() -> tuple[bool, str]:
334
+ was_running = bool(pma_core.status())
335
+ if was_running:
336
+ pma_core.stop()
337
+
338
+ info = get_pma_info()
339
+ target_ver = info.latest_version or pma_core.DEFAULT_VERSION
340
+
341
+ # 1. Create safety backup of config.inc.php
342
+ config_inc = paths.PMA_DIR / "config.inc.php"
343
+ config_backup = None
344
+ if config_inc.exists():
345
+ import datetime
346
+ ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
347
+ config_backup = paths.BACKUPS_DIR / f"pma_config_{ts}.inc.php"
348
+ try:
349
+ shutil.copy2(config_inc, config_backup)
350
+ except Exception:
351
+ pass
352
+
353
+ try:
354
+ pma_core.install(version=target_ver)
355
+ if config_backup and config_backup.exists():
356
+ shutil.copy2(config_backup, paths.PMA_DIR / "config.inc.php")
357
+
358
+ if was_running:
359
+ pma_core.start()
360
+
361
+ return True, f"PMA upgraded successfully to {target_ver} (configuration preserved)."
362
+ except Exception as e:
363
+ if was_running:
364
+ try:
365
+ pma_core.start()
366
+ except Exception:
367
+ pass
368
+ return False, f"PMA upgrade failed: {e}"
369
+
370
+
371
+ # 5. MKCERT
372
+ def get_mkcert_info() -> ComponentInfo:
373
+ mkcert_exe = paths.SHIM_DIR / "mkcert.exe"
374
+ installed = mkcert_exe.exists()
375
+ curr_ver = None
376
+ latest_ver = None
377
+ err = None
378
+
379
+ if installed:
380
+ try:
381
+ res = subprocess.run([str(mkcert_exe), "-version"], capture_output=True, text=True, timeout=5)
382
+ curr_ver = res.stdout.strip() or res.stderr.strip()
383
+ except Exception as e:
384
+ err = str(e)
385
+
386
+ try:
387
+ rel = _http_get_json("https://api.github.com/repos/FiloSottile/mkcert/releases/latest")
388
+ latest_ver = rel.get("tag_name")
389
+ except Exception as e:
390
+ latest_ver = f"v{setup_core.DEFAULT_MKCERT_VERSION}"
391
+ if not err:
392
+ err = f"Could not query GitHub releases: {e}"
393
+
394
+ update_avail = False
395
+ if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
396
+ update_avail = True
397
+
398
+ status = "Up-to-date"
399
+ if not installed:
400
+ status = "Not Installed"
401
+ elif update_avail:
402
+ status = f"Update Available ({curr_ver} -> {latest_ver})"
403
+
404
+ return ComponentInfo("mkcert", "mkcert Local SSL", curr_ver, latest_ver, update_avail, installed, status, err)
405
+
406
+
407
+ def upgrade_mkcert() -> tuple[bool, str]:
408
+ info = get_mkcert_info()
409
+ target_ver = _clean_ver(info.latest_version) or setup_core.DEFAULT_MKCERT_VERSION
410
+ try:
411
+ dest = setup_core.install_mkcert(version=target_ver)
412
+ return True, f"mkcert upgraded successfully to v{target_ver}."
413
+ except Exception as e:
414
+ return False, f"mkcert upgrade failed: {e}"
415
+
416
+
417
+ # 6. COMPOSER
418
+ def get_composer_info() -> ComponentInfo:
419
+ phar = paths.SHIM_DIR / "composer.phar"
420
+ installed = phar.exists()
421
+ curr_ver = None
422
+ latest_ver = None
423
+ err = None
424
+
425
+ if installed:
426
+ try:
427
+ bat = paths.SHIM_DIR / "composer.bat"
428
+ res = subprocess.run([str(bat), "--version"], capture_output=True, text=True, timeout=10)
429
+ m = re.search(r"Composer (?:version )?(\d+\.\d+\.\d+)", res.stdout or res.stderr)
430
+ if m:
431
+ curr_ver = m.group(1)
432
+ except Exception as e:
433
+ err = str(e)
434
+
435
+ try:
436
+ data = _http_get_json("https://getcomposer.org/versions")
437
+ stables = data.get("stable", [])
438
+ if stables:
439
+ latest_ver = stables[0].get("version")
440
+ except Exception as e:
441
+ if not err:
442
+ err = f"Could not query Composer versions API: {e}"
443
+
444
+ update_avail = False
445
+ if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
446
+ update_avail = True
447
+
448
+ status = "Up-to-date"
449
+ if not installed:
450
+ status = "Not Installed"
451
+ elif update_avail:
452
+ status = f"Update Available ({curr_ver} -> {latest_ver})"
453
+
454
+ return ComponentInfo("composer", "Composer PHP Package Manager", curr_ver, latest_ver, update_avail, installed, status, err)
455
+
456
+
457
+ def upgrade_composer() -> tuple[bool, str]:
458
+ try:
459
+ bat = paths.SHIM_DIR / "composer.bat"
460
+ if bat.exists():
461
+ res = subprocess.run([str(bat), "self-update"], capture_output=True, text=True, timeout=60)
462
+ if res.returncode == 0:
463
+ info = get_composer_info()
464
+ return True, f"Composer upgraded successfully ({info.current_version})."
465
+
466
+ setup_core.install_composer()
467
+ info = get_composer_info()
468
+ return True, f"Composer upgraded successfully ({info.current_version or info.latest_version})."
469
+ except Exception as e:
470
+ return False, f"Composer upgrade failed: {e}"
471
+
472
+
473
+ # ── UNIFIED API ─────────────────────────────────────────────────────────────
474
+
475
+ COMPONENTS = ["nginx", "mailpit", "mariadb", "pma", "mkcert", "composer"]
476
+
477
+
478
+ def check_all() -> list[ComponentInfo]:
479
+ """Inspect installed versions and query latest upstream releases for all stack components."""
480
+ results = [
481
+ get_nginx_info(),
482
+ get_mailpit_info(),
483
+ get_mariadb_info(),
484
+ get_pma_info(),
485
+ get_mkcert_info(),
486
+ get_composer_info(),
487
+ ]
488
+ return results
489
+
490
+
491
+ def upgrade_component(name: str) -> tuple[bool, str]:
492
+ name = name.lower()
493
+ if name == "nginx":
494
+ return upgrade_nginx()
495
+ elif name in ["mailpit", "mail"]:
496
+ return upgrade_mailpit()
497
+ elif name in ["mariadb", "mysql"]:
498
+ return upgrade_mariadb()
499
+ elif name in ["pma", "phpmyadmin"]:
500
+ return upgrade_pma()
501
+ elif name == "mkcert":
502
+ return upgrade_mkcert()
503
+ elif name == "composer":
504
+ return upgrade_composer()
505
+ else:
506
+ return False, f"Unknown component '{name}'. Available: {', '.join(COMPONENTS)}"
507
+
508
+
509
+ def upgrade_all() -> dict[str, tuple[bool, str]]:
510
+ results = {}
511
+ for comp in COMPONENTS:
512
+ results[comp] = upgrade_component(comp)
513
+ return results