Skip to content

Commit 5d8e171

Browse files
committed
Add downloadable SKY130 PDK bundle flow
1 parent 3dc6a28 commit 5d8e171

8 files changed

Lines changed: 470 additions & 1 deletion

app/core/dependency_manifest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ class ChannelPolicy:
2424
pdk_source_build_open_pdks_repo: str
2525
pdk_source_build_open_pdks_ref: str
2626
pdk_source_build_configure_args: tuple[str, ...]
27+
pdk_bundle_enabled: bool
28+
pdk_bundle_name: str
29+
pdk_bundle_version: str
30+
pdk_bundle_install_root: str
31+
pdk_bundle_cache_root: str
32+
pdk_bundle_minimum_free_gb: int
33+
pdk_bundle_asset_url: str
34+
pdk_bundle_asset_filename: str
35+
pdk_bundle_asset_sha256: str
2736
pdk_preferred_sources: tuple[str, ...]
2837
pdk_search_roots: tuple[str, ...]
2938

@@ -64,6 +73,15 @@ def channel(self, name: str | None = None) -> ChannelPolicy:
6473
pdk_source_build_open_pdks_repo=str(pdk.get("source_build_open_pdks_repo", "")),
6574
pdk_source_build_open_pdks_ref=str(pdk.get("source_build_open_pdks_ref", "")),
6675
pdk_source_build_configure_args=tuple(str(item) for item in pdk.get("source_build_configure_args", [])),
76+
pdk_bundle_enabled=bool(pdk.get("bundle", {}).get("enabled", False)),
77+
pdk_bundle_name=str(pdk.get("bundle", {}).get("name", "tt-pdk-sky130a")),
78+
pdk_bundle_version=str(pdk.get("bundle", {}).get("version", "")),
79+
pdk_bundle_install_root=str(pdk.get("bundle", {}).get("install_root", "~/tt-pdk")),
80+
pdk_bundle_cache_root=str(pdk.get("bundle", {}).get("cache_root", "~/.cache/sky130-flow-gui/pdk")),
81+
pdk_bundle_minimum_free_gb=int(pdk.get("bundle", {}).get("minimum_free_gb", 10)),
82+
pdk_bundle_asset_url=str(pdk.get("bundle", {}).get("asset_url", "")),
83+
pdk_bundle_asset_filename=str(pdk.get("bundle", {}).get("asset_filename", "")),
84+
pdk_bundle_asset_sha256=str(pdk.get("bundle", {}).get("asset_sha256", "")),
6785
pdk_preferred_sources=tuple(str(item) for item in pdk.get("preferred_sources", [])),
6886
pdk_search_roots=tuple(str(item) for item in pdk.get("search_roots", [])),
6987
)

app/core/setup_manager.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
from pathlib import Path
88
import shutil
9+
import sys
910

1011
from app.core.dependency_manifest import DependencyManifest
1112
from app.core.env_validator import EnvValidator, REQUIRED_PDK_SUBDIRS
@@ -68,6 +69,28 @@ class PdkSourceBuildPreflight:
6869
message: str
6970

7071

72+
@dataclass(frozen=True)
73+
class PdkBundlePreflight:
74+
"""Precheck summary for downloading and installing a TT-style PDK bundle."""
75+
76+
enabled: bool
77+
bundle_name: str
78+
bundle_version: str
79+
install_root: str
80+
target_sky130a: str
81+
cache_root: str
82+
asset_url: str
83+
asset_filename: str
84+
asset_sha256: str
85+
minimum_free_bytes: int
86+
free_bytes: int
87+
enough_space: bool
88+
target_available: bool
89+
target_status: str
90+
ready: bool
91+
message: str
92+
93+
7194
class SetupManager:
7295
"""Detect common installations and provide installer entrypoints."""
7396

@@ -82,12 +105,18 @@ def installer_script(self) -> Path:
82105
def pdk_source_build_script(self) -> Path:
83106
return self.repo_root / "scripts" / "build_sky130_pdk_from_sources.sh"
84107

108+
def pdk_bundle_install_script(self) -> Path:
109+
return self.repo_root / "scripts" / "install_tt_pdk_bundle.py"
110+
85111
def installer_command(self) -> list[str]:
86112
return ["pkexec", "/bin/bash", str(self.installer_script()), self.manifest.default_channel()]
87113

88114
def pdk_source_build_command(self) -> list[str]:
89115
return ["/bin/bash", str(self.pdk_source_build_script()), self.manifest.default_channel()]
90116

117+
def pdk_bundle_install_command(self) -> list[str]:
118+
return [sys.executable, str(self.pdk_bundle_install_script()), self.manifest.default_channel()]
119+
91120
def bootstrap_packages(self, channel: str | None = None) -> tuple[str, ...]:
92121
return self.manifest.channel(channel).apt_packages
93122

@@ -312,6 +341,59 @@ def pdk_source_build_preflight(self, settings: AppSettings) -> PdkSourceBuildPre
312341
message="; ".join(message_parts),
313342
)
314343

344+
def pdk_bundle_preflight(self, settings: AppSettings) -> PdkBundlePreflight:
345+
policy = self.manifest.channel()
346+
install_root = Path(policy.pdk_bundle_install_root).expanduser()
347+
target_sky130a = install_root / "sky130A"
348+
cache_root = Path(policy.pdk_bundle_cache_root).expanduser()
349+
usage_root = install_root if install_root.exists() else install_root.parent if install_root.parent.exists() else Path.home()
350+
free_bytes = shutil.disk_usage(usage_root).free
351+
minimum_free_bytes = policy.pdk_bundle_minimum_free_gb * 1024 * 1024 * 1024
352+
enough_space = free_bytes >= minimum_free_bytes
353+
target_status = self._classify_sky130a_path(target_sky130a)
354+
target_available = not os.path.lexists(target_sky130a)
355+
has_asset = bool(policy.pdk_bundle_enabled and policy.pdk_bundle_asset_url and policy.pdk_bundle_asset_filename)
356+
ready = has_asset and enough_space and target_available
357+
358+
message_parts = [
359+
f"bundle={policy.pdk_bundle_name}",
360+
f"install_root={install_root}",
361+
f"target={target_sky130a}",
362+
f"cache_root={cache_root}",
363+
f"free_gb={free_bytes / (1024 ** 3):.1f}",
364+
]
365+
if policy.pdk_bundle_version:
366+
message_parts.append(f"version={policy.pdk_bundle_version}")
367+
if not policy.pdk_bundle_enabled:
368+
message_parts.append("bundle_disabled")
369+
if not policy.pdk_bundle_asset_url:
370+
message_parts.append("missing_asset_url")
371+
if not policy.pdk_bundle_asset_filename:
372+
message_parts.append("missing_asset_filename")
373+
if not enough_space:
374+
message_parts.append(f"requires_at_least={policy.pdk_bundle_minimum_free_gb}GB")
375+
if not target_available:
376+
message_parts.append(f"target_status={target_status}")
377+
378+
return PdkBundlePreflight(
379+
enabled=policy.pdk_bundle_enabled,
380+
bundle_name=policy.pdk_bundle_name,
381+
bundle_version=policy.pdk_bundle_version,
382+
install_root=str(install_root),
383+
target_sky130a=str(target_sky130a),
384+
cache_root=str(cache_root),
385+
asset_url=policy.pdk_bundle_asset_url,
386+
asset_filename=policy.pdk_bundle_asset_filename,
387+
asset_sha256=policy.pdk_bundle_asset_sha256,
388+
minimum_free_bytes=minimum_free_bytes,
389+
free_bytes=free_bytes,
390+
enough_space=enough_space,
391+
target_available=target_available,
392+
target_status=target_status,
393+
ready=ready,
394+
message="; ".join(message_parts),
395+
)
396+
315397
def apply_detected_defaults(self, settings: AppSettings) -> bool:
316398
changed = False
317399

app/data/dependency_manifest.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@
5555
"source_build_configure_args": [
5656
"--enable-sky130-pdk"
5757
],
58+
"bundle": {
59+
"enabled": true,
60+
"name": "tt-pdk-sky130a",
61+
"version": "0.2.0-beta.2",
62+
"install_root": "~/pdk",
63+
"cache_root": "~/.cache/sky130-flow-gui/pdk",
64+
"minimum_free_gb": 10,
65+
"asset_url": "https://github.com/ROMERUU-dev/sky130-flow-gui/releases/download/v0.2.0-beta.2/tt-pdk-sky130a_0.2.0-beta.2.tar.gz",
66+
"asset_filename": "tt-pdk-sky130a_0.2.0-beta.2.tar.gz",
67+
"asset_sha256": "25acfcdaace6e6b8ca0ca828407ecf1a896a1c0e04560465d2259cda8b9b4c24"
68+
},
5869
"preferred_sources": [
5970
"settings",
6071
"environment",
@@ -125,6 +136,17 @@
125136
"source_build_configure_args": [
126137
"--enable-sky130-pdk"
127138
],
139+
"bundle": {
140+
"enabled": true,
141+
"name": "tt-pdk-sky130a",
142+
"version": "0.2.0-beta.2",
143+
"install_root": "~/pdk",
144+
"cache_root": "~/.cache/sky130-flow-gui/pdk",
145+
"minimum_free_gb": 10,
146+
"asset_url": "https://github.com/ROMERUU-dev/sky130-flow-gui/releases/download/v0.2.0-beta.2/tt-pdk-sky130a_0.2.0-beta.2.tar.gz",
147+
"asset_filename": "tt-pdk-sky130a_0.2.0-beta.2.tar.gz",
148+
"asset_sha256": "25acfcdaace6e6b8ca0ca828407ecf1a896a1c0e04560465d2259cda8b9b4c24"
149+
},
128150
"preferred_sources": [
129151
"settings",
130152
"environment",

app/ui/setup_tab.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def __init__(self, settings: AppSettings) -> None:
9292
self._pdk_candidates_available = False
9393
self._pdk_preflight = None
9494
self._pdk_source_preflight = None
95+
self._pdk_bundle_preflight = None
9596
self._runner_action = ""
9697

9798
self.validate_btn = QPushButton(pick(self.lang, "Validar entorno", "Validate environment"))
@@ -101,6 +102,7 @@ def __init__(self, settings: AppSettings) -> None:
101102
self.detect_pdk_btn = QPushButton(pick(self.lang, "Buscar PDK reutilizable", "Find reusable PDK"))
102103
self.use_pdk_btn = QPushButton(pick(self.lang, "Usar PDK seleccionado", "Use selected PDK"))
103104
self.install_managed_pdk_btn = QPushButton(pick(self.lang, "Instalar PDK gestionado", "Install managed PDK"))
105+
self.install_bundle_pdk_btn = QPushButton(pick(self.lang, "Descargar bundle PDK", "Download PDK bundle"))
104106
self.check_source_build_btn = QPushButton(pick(self.lang, "Precheck build desde fuentes", "Source-build precheck"))
105107
self.build_from_sources_btn = QPushButton(pick(self.lang, "Build PDK desde fuentes", "Build PDK from sources"))
106108
self.pdk_candidate_combo = QComboBox()
@@ -109,6 +111,8 @@ def __init__(self, settings: AppSettings) -> None:
109111
self.pdk_candidate_summary.setWordWrap(True)
110112
self.pdk_preflight_summary = QLabel()
111113
self.pdk_preflight_summary.setWordWrap(True)
114+
self.pdk_bundle_summary = QLabel()
115+
self.pdk_bundle_summary.setWordWrap(True)
112116
self.pdk_source_preflight_summary = QLabel()
113117
self.pdk_source_preflight_summary.setWordWrap(True)
114118
self.prev_btn = QPushButton(pick(self.lang, "Atrás", "Back"))
@@ -357,10 +361,13 @@ def _build_pdk_page(self) -> QWidget:
357361
actions.addWidget(self.detect_pdk_btn)
358362
actions.addWidget(self.use_pdk_btn)
359363
actions.addWidget(self.install_managed_pdk_btn)
364+
actions.addWidget(self.install_bundle_pdk_btn)
360365
actions.addStretch(1)
361366
layout.addLayout(actions)
362367
layout.addWidget(QLabel(pick(self.lang, "Preflight de instalación gestionada", "Managed install preflight")))
363368
layout.addWidget(self.pdk_preflight_summary)
369+
layout.addWidget(QLabel(pick(self.lang, "Bundle PDK", "PDK bundle")))
370+
layout.addWidget(self.pdk_bundle_summary)
364371
source_actions = QHBoxLayout()
365372
source_actions.addWidget(self.check_source_build_btn)
366373
source_actions.addWidget(self.build_from_sources_btn)
@@ -451,6 +458,7 @@ def _wire(self) -> None:
451458
self.detect_pdk_btn.clicked.connect(self.refresh_pdk_candidates)
452459
self.use_pdk_btn.clicked.connect(self.apply_selected_pdk_candidate)
453460
self.install_managed_pdk_btn.clicked.connect(self.install_managed_pdk)
461+
self.install_bundle_pdk_btn.clicked.connect(self.install_bundle_pdk)
454462
self.check_source_build_btn.clicked.connect(self.refresh_pdk_source_preflight)
455463
self.build_from_sources_btn.clicked.connect(self.build_pdk_from_sources)
456464
self.pdk_candidate_combo.currentIndexChanged.connect(self._update_pdk_candidate_summary)
@@ -581,6 +589,7 @@ def refresh_pdk_candidates(self) -> None:
581589
if self.pdk_candidate_combo.count() and self.pdk_candidate_combo.currentIndex() < 0:
582590
self.pdk_candidate_combo.setCurrentIndex(0)
583591
self._refresh_pdk_preflight()
592+
self.refresh_pdk_bundle_preflight()
584593
self.refresh_pdk_source_preflight()
585594
self._update_pdk_candidate_summary()
586595
self._sync_action_gates()
@@ -661,6 +670,64 @@ def refresh_pdk_source_preflight(self) -> None:
661670
)
662671
self._sync_action_gates()
663672

673+
def refresh_pdk_bundle_preflight(self) -> None:
674+
self._pdk_bundle_preflight = self.setup_mgr.pdk_bundle_preflight(self.settings)
675+
summary = self._pdk_bundle_preflight
676+
self.pdk_bundle_summary.setText(
677+
pick(
678+
self.lang,
679+
f"Bundle: {summary.bundle_name}\n"
680+
f"Versión: {summary.bundle_version or 'pendiente'}\n"
681+
f"Instalación TT: {summary.target_sky130a}\n"
682+
f"Cache: {summary.cache_root}\n"
683+
f"Espacio libre: {summary.free_bytes / (1024 ** 3):.1f} GB\n"
684+
f"Asset configurado: {'sí' if bool(summary.asset_url and summary.asset_filename) else 'no'}\n"
685+
f"Listo para instalar: {'sí' if summary.ready else 'no'}",
686+
f"Bundle: {summary.bundle_name}\n"
687+
f"Version: {summary.bundle_version or 'pending'}\n"
688+
f"TT install: {summary.target_sky130a}\n"
689+
f"Cache: {summary.cache_root}\n"
690+
f"Free space: {summary.free_bytes / (1024 ** 3):.1f} GB\n"
691+
f"Asset configured: {'yes' if bool(summary.asset_url and summary.asset_filename) else 'no'}\n"
692+
f"Ready to install: {'yes' if summary.ready else 'no'}",
693+
)
694+
)
695+
self._sync_action_gates()
696+
697+
def install_bundle_pdk(self) -> None:
698+
self.refresh_pdk_bundle_preflight()
699+
summary = self._pdk_bundle_preflight
700+
if summary is None or not summary.ready:
701+
self.log.append(
702+
pick(
703+
self.lang,
704+
"El bundle PDK todavía no está listo para descargarse e instalarse. Revisa el panel del bundle.\n",
705+
"The PDK bundle is not ready to download and install yet. Review the bundle panel.\n",
706+
)
707+
)
708+
return
709+
script_path = self.setup_mgr.pdk_bundle_install_script()
710+
if not script_path.exists():
711+
self.log.append(
712+
pick(
713+
self.lang,
714+
f"Script del bundle no encontrado: {script_path}\n",
715+
f"Bundle script not found: {script_path}\n",
716+
)
717+
)
718+
return
719+
self._begin_activity(pick(self.lang, "Descargando bundle PDK...", "Downloading PDK bundle..."))
720+
self.log.append(
721+
pick(
722+
self.lang,
723+
"Descargando e instalando el bundle PDK en la ruta canónica del usuario. Esto puede tardar bastante.\n",
724+
"Downloading and installing the PDK bundle into the user's canonical path. This can take a while.\n",
725+
)
726+
)
727+
self.send_status.emit(pick(self.lang, "Instalando bundle PDK", "Installing PDK bundle"))
728+
self._runner_action = "install_pdk_bundle"
729+
self.runner.run(CommandSpec(command=self.setup_mgr.pdk_bundle_install_command()))
730+
664731
def build_pdk_from_sources(self) -> None:
665732
self.refresh_pdk_source_preflight()
666733
summary = self._pdk_source_preflight
@@ -756,6 +823,22 @@ def _on_finished(self, code: int, status: str) -> None:
756823
self._finish_activity(True, pick(self.lang, "Build de PDK listo", "PDK build ready"))
757824
return
758825

826+
if code == 0 and action == "install_pdk_bundle":
827+
self.log.append(
828+
pick(
829+
self.lang,
830+
"\nBundle PDK instalado. Refrescando detección y validación.\n",
831+
"\nPDK bundle installed. Refreshing detection and validation.\n",
832+
)
833+
)
834+
self.send_status.emit(pick(self.lang, "Bundle PDK listo", "PDK bundle ready"))
835+
self.refresh_detection()
836+
self.refresh_validation()
837+
self._apply_detected_defaults(automatic=True)
838+
self._set_step(3)
839+
self._finish_activity(True, pick(self.lang, "Bundle PDK listo", "PDK bundle ready"))
840+
return
841+
759842
if action == "build_pdk_sources":
760843
self.log.append(
761844
pick(
@@ -769,6 +852,19 @@ def _on_finished(self, code: int, status: str) -> None:
769852
self._finish_activity(False, pick(self.lang, "Build de PDK falló", "PDK build failed"))
770853
return
771854

855+
if action == "install_pdk_bundle":
856+
self.log.append(
857+
pick(
858+
self.lang,
859+
f"\nLa instalación del bundle PDK falló (exit={code}, status={status}). Revisa el log y la configuración del asset.\n",
860+
f"\nPDK bundle installation failed (exit={code}, status={status}). Review the log and the asset configuration.\n",
861+
)
862+
)
863+
self.send_status.emit(pick(self.lang, "Bundle PDK falló", "PDK bundle failed"))
864+
self.refresh_pdk_bundle_preflight()
865+
self._finish_activity(False, pick(self.lang, "Bundle PDK falló", "PDK bundle failed"))
866+
return
867+
772868
if action == "install_tools":
773869
self.log.append(
774870
pick(
@@ -938,6 +1034,7 @@ def _set_action_buttons_enabled(self, enabled: bool) -> None:
9381034
self.detect_pdk_btn.setEnabled(False)
9391035
self.use_pdk_btn.setEnabled(False)
9401036
self.install_managed_pdk_btn.setEnabled(False)
1037+
self.install_bundle_pdk_btn.setEnabled(False)
9411038
self.check_source_build_btn.setEnabled(False)
9421039
self.build_from_sources_btn.setEnabled(False)
9431040
self.prev_btn.setEnabled(False)
@@ -964,6 +1061,11 @@ def _sync_action_gates(self) -> None:
9641061
and self._pdk_preflight.existing_status == "missing"
9651062
and bool(self._pdk_preflight.selected_candidate)
9661063
)
1064+
self.install_bundle_pdk_btn.setEnabled(
1065+
self._verification_completed
1066+
and self._pdk_bundle_preflight is not None
1067+
and self._pdk_bundle_preflight.ready
1068+
)
9671069
self.check_source_build_btn.setEnabled(self._verification_completed)
9681070
self.build_from_sources_btn.setEnabled(
9691071
self._verification_completed
@@ -1022,6 +1124,21 @@ def _sync_action_gates(self) -> None:
10221124
"Install a managed PDK in the configured canonical path.",
10231125
)
10241126
)
1127+
self.install_bundle_pdk_btn.setToolTip(
1128+
pick(
1129+
self.lang,
1130+
"Primero verifica el sistema y configura un asset del bundle PDK en el manifiesto." if not self._verification_completed else
1131+
"El bundle PDK sigue bloqueado por configuración, espacio o porque ya existe el destino." if (
1132+
self._pdk_bundle_preflight is None or not self._pdk_bundle_preflight.ready
1133+
) else
1134+
"Descarga e instala un bundle `sky130A` ya preparado en `~/pdk`.",
1135+
"Verify the system first and configure a PDK bundle asset in the manifest." if not self._verification_completed else
1136+
"The PDK bundle is still blocked by configuration, space, or because the target already exists." if (
1137+
self._pdk_bundle_preflight is None or not self._pdk_bundle_preflight.ready
1138+
) else
1139+
"Download and install a prebuilt `sky130A` bundle into `~/pdk`.",
1140+
)
1141+
)
10251142
self.build_from_sources_btn.setToolTip(
10261143
pick(
10271144
self.lang,

0 commit comments

Comments
 (0)