Skip to content

Commit ab7dbc7

Browse files
committed
Fix Magic setup for SKY130 PDK validation
1 parent 6037dc6 commit ab7dbc7

6 files changed

Lines changed: 214 additions & 19 deletions

File tree

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ Create `.venv` as your normal user. Do not create or repair the repository virtu
6767

6868
If the GUI already opens on your machine, go to `⬢ Entorno / Setup` and follow the wizard.
6969

70-
The Ubuntu bootstrap currently installs Ubuntu system packages only:
70+
The Ubuntu bootstrap installs Ubuntu system packages plus the official Magic `8.3.634` source release:
7171

7272
- `xschem`
7373
- `ngspice`
74-
- `magic`
74+
- `magic` from apt as a baseline, then `/usr/local/bin/magic` built from the official `magic-8.3.634.tgz` release
7575
- `netgen-lvs` (accepted by the app as Netgen)
7676
- `klayout`
7777
- `python3`, `python3-pip`, `python3-venv`
@@ -87,6 +87,15 @@ python3 -m venv .venv
8787
.venv/bin/python -m pip install -r requirements.txt
8888
```
8989

90+
Some current SKY130 PDK techfiles require a newer Magic revision than the one shipped by older Ubuntu apt repositories. The Setup Assistant handles this during a clean tool install. If you are repairing an existing machine manually, install the official Magic `8.3.634` source release:
91+
92+
```bash
93+
bash scripts/install_magic_8_3_634_ubuntu.sh
94+
/usr/local/bin/magic -dnull -noconsole -version
95+
```
96+
97+
Then set the Magic executable in Preferences to `/usr/local/bin/magic` if `which magic` still resolves to `/usr/bin/magic`.
98+
9099
## Ubuntu Environment Notes
91100

92101
The app works best when these are available:
@@ -104,8 +113,9 @@ The Setup Assistant can detect common installations and apply discovered paths a
104113

105114
Important:
106115

107-
- the tool bootstrap is implemented for Ubuntu/Debian-style systems using `apt`
116+
- the tool bootstrap is implemented for Ubuntu/Debian-style systems using `apt` plus the official Magic source release
108117
- installing apt packages does not install the SKY130 PDK automatically
118+
- older Ubuntu apt repositories may ship Magic revisions too old for current SKY130 techfiles; use `scripts/install_magic_8_3_634_ubuntu.sh` when the validator reports a Magic/PDK incompatibility
109119
- the validator looks for `sky130A` in `PDK_ROOT`, `/usr/local/share/pdk`, `/usr/share/pdk`, `~/pdk`, `~/.volare`, and `~/eda/pdk`
110120
- the app checks `sky130A/libs.tech/magic`, `netgen`, `klayout`, `ngspice`, and `xschem` separately, so an incomplete PDK is reported as incomplete rather than OK
111121
- a repository under `/opt` may be readable but not writable for the current user; in that case `.venv` creation is intentionally reported as a permissions problem

app/core/env_validator.py

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import ctypes.util
66
import os
77
import pwd
8+
import re
89
import shutil
910
import subprocess
1011
from collections import OrderedDict
@@ -137,7 +138,7 @@ def diagnose(self, settings: AppSettings, lang: str = "en") -> EnvironmentDiagno
137138
(name, self._detect_tool(name, getattr(settings.tool_paths, name, ""), lang))
138139
for name in ("xschem", "ngspice", "magic", "netgen", "klayout")
139140
)
140-
pdk = self._detect_pdk(settings, lang)
141+
pdk = self._detect_pdk(settings, lang, magic_version=tools["magic"].version)
141142
python_env = self._detect_python_environment(lang)
142143
gui_dependencies = self._detect_gui_dependencies(lang)
143144
recommendations = self._build_recommendations(tools, pdk, python_env, gui_dependencies, lang)
@@ -255,7 +256,7 @@ def _detect_tool(self, logical_name: str, configured_value: str, lang: str) -> T
255256
),
256257
)
257258

258-
def _detect_pdk(self, settings: AppSettings, lang: str) -> PdkDiagnosis:
259+
def _detect_pdk(self, settings: AppSettings, lang: str, magic_version: str = "") -> PdkDiagnosis:
259260
sky130a = self._find_sky130a(settings)
260261
if sky130a is None:
261262
return PdkDiagnosis(
@@ -273,20 +274,29 @@ def _detect_pdk(self, settings: AppSettings, lang: str) -> PdkDiagnosis:
273274
for relative in REQUIRED_PDK_SUBDIRS.values()
274275
if not sky130a.joinpath(relative).exists()
275276
]
276-
status = "present" if not missing_subdirs else "incomplete"
277-
message = (
278-
pick(
279-
lang,
280-
f"PDK `sky130A` detectado en: {sky130a}",
281-
f"`sky130A` PDK detected at: {sky130a}",
282-
)
283-
if status == "present"
284-
else pick(
277+
magic_requirement = self._detect_magic_tech_requirement(sky130a, magic_version)
278+
if missing_subdirs:
279+
status = "incomplete"
280+
message = pick(
285281
lang,
286282
f"El PDK `sky130A` existe en {sky130a}, pero está incompleto. Faltan: {', '.join(missing_subdirs)}.",
287283
f"The `sky130A` PDK exists at {sky130a}, but it is incomplete. Missing: {', '.join(missing_subdirs)}.",
288284
)
289-
)
285+
elif magic_requirement:
286+
required, detected = magic_requirement
287+
status = "incompatible"
288+
message = pick(
289+
lang,
290+
f"El PDK `sky130A` requiere Magic {required} o superior, pero se detectó Magic {detected}. Actualiza Magic o usa un PDK compatible.",
291+
f"The `sky130A` PDK requires Magic {required} or newer, but Magic {detected} was detected. Upgrade Magic or use a compatible PDK.",
292+
)
293+
else:
294+
status = "present"
295+
message = pick(
296+
lang,
297+
f"PDK `sky130A` detectado en: {sky130a}",
298+
f"`sky130A` PDK detected at: {sky130a}",
299+
)
290300
return PdkDiagnosis(
291301
found=True,
292302
status=status,
@@ -470,6 +480,14 @@ def _build_recommendations(
470480
"Complete `sky130A/libs.tech/*` or fix `PDK_ROOT` so the app uses a complete PDK.",
471481
)
472482
)
483+
elif pdk.status == "incompatible":
484+
recommendations.append(
485+
pick(
486+
lang,
487+
"Actualiza Magic para que cumpla la versión mínima declarada por `sky130A.tech`, o apunta la app a un PDK compatible con tu Magic instalado.",
488+
"Upgrade Magic so it meets the minimum version declared by `sky130A.tech`, or point the app to a PDK compatible with your installed Magic.",
489+
)
490+
)
473491
if python_env.problems:
474492
recommendations.append(
475493
pick(
@@ -589,6 +607,42 @@ def _check_python_requirements(python_bin: Path) -> bool:
589607
return False
590608
return result.returncode == 0 and result.stdout.strip() == "OK"
591609

610+
@classmethod
611+
def _detect_magic_tech_requirement(cls, sky130a: Path, magic_version: str) -> tuple[str, str] | None:
612+
techfile = sky130a / "libs.tech" / "magic" / "sky130A.tech"
613+
if not techfile.is_file() or not magic_version:
614+
return None
615+
required_text = cls._read_magic_tech_required_version(techfile)
616+
required = cls._parse_version_tuple(required_text)
617+
detected = cls._parse_version_tuple(magic_version)
618+
if required is None or detected is None:
619+
return None
620+
if detected < required:
621+
return (required_text, ".".join(str(part) for part in detected))
622+
return None
623+
624+
@staticmethod
625+
def _read_magic_tech_required_version(techfile: Path) -> str:
626+
try:
627+
text = techfile.read_text(encoding="utf-8", errors="replace")
628+
except OSError:
629+
return ""
630+
for line in text.splitlines()[:80]:
631+
match = re.search(r"\bversion\s+([0-9]+(?:\.[0-9]+){1,3})\b", line, flags=re.IGNORECASE)
632+
if match:
633+
return match.group(1)
634+
return ""
635+
636+
@staticmethod
637+
def _parse_version_tuple(text: str) -> tuple[int, ...] | None:
638+
revision_match = re.search(r"\bMagic\s+([0-9]+)\.([0-9]+)\s+revision\s+([0-9]+)\b", text, flags=re.IGNORECASE)
639+
if revision_match:
640+
return tuple(int(part) for part in revision_match.groups())
641+
match = re.search(r"\b([0-9]+(?:\.[0-9]+){1,3})\b", text)
642+
if not match:
643+
return None
644+
return tuple(int(part) for part in match.group(1).split("."))
645+
592646
@staticmethod
593647
def _query_version(logical_name: str, executable: str) -> str:
594648
for args in TOOL_VERSION_ARGS.get(logical_name, (("--version",),)):
@@ -663,6 +717,7 @@ def _tool_status_label(status: str, lang: str) -> str:
663717
def _pdk_status_label(status: str, lang: str) -> str:
664718
mapping = {
665719
"present": pick(lang, "PRESENTE", "PRESENT"),
720+
"incompatible": pick(lang, "INCOMPATIBLE", "INCOMPATIBLE"),
666721
"incomplete": pick(lang, "INCOMPLETO", "INCOMPLETE"),
667722
"missing": pick(lang, "AUSENTE", "ABSENT"),
668723
}

app/ui/setup_tab.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,8 @@ def _build_install_page(self) -> QWidget:
314314
self._page_hint(
315315
pick(
316316
self.lang,
317-
"Este paso instala paquetes del sistema para Ubuntu: herramientas EDA base y librerías Qt/X11. No crea `.venv` ni toca el entorno Python del usuario.",
318-
"This step installs Ubuntu system packages: base EDA tools and Qt/X11 runtime libraries. It does not create `.venv` or touch the user-owned Python environment.",
317+
"Este paso instala paquetes del sistema para Ubuntu y compila Magic 8.3.634 desde la fuente oficial para SKY130. No crea `.venv` ni toca el entorno Python del usuario.",
318+
"This step installs Ubuntu system packages and builds Magic 8.3.634 from the official source release for SKY130. It does not create `.venv` or touch the user-owned Python environment.",
319319
)
320320
)
321321
)
@@ -788,8 +788,8 @@ def install_environment(self) -> None:
788788
self.log.append(
789789
pick(
790790
self.lang,
791-
"Lanzando bootstrap de Ubuntu con privilegios. Este paso solo instala paquetes del sistema; `.venv` debe prepararse después como usuario normal.\n",
792-
"Launching Ubuntu bootstrap with privileges. This step only installs system packages; `.venv` must be prepared afterwards as the normal user.\n",
791+
"Lanzando bootstrap de Ubuntu con privilegios. Este paso instala paquetes del sistema y Magic 8.3.634; `.venv` debe prepararse después como usuario normal.\n",
792+
"Launching Ubuntu bootstrap with privileges. This step installs system packages and Magic 8.3.634; `.venv` must be prepared afterwards as the normal user.\n",
793793
)
794794
)
795795
self.send_status.emit(pick(self.lang, "Instalación en progreso", "Installation in progress"))
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
MAGIC_VERSION="${MAGIC_VERSION:-8.3.634}"
5+
MAGIC_TARBALL_URL="${MAGIC_TARBALL_URL:-http://www.opencircuitdesign.com/magic/archive/magic-${MAGIC_VERSION}.tgz}"
6+
PREFIX="${PREFIX:-/usr/local}"
7+
BUILD_ROOT="${BUILD_ROOT:-$HOME/src/magic-build}"
8+
9+
SUDO=""
10+
if [ "$(id -u)" -ne 0 ]; then
11+
SUDO="sudo"
12+
fi
13+
14+
download() {
15+
local url="$1"
16+
local output="$2"
17+
18+
if command -v curl >/dev/null 2>&1; then
19+
curl -L --fail --output "$output" "$url"
20+
return
21+
fi
22+
if command -v wget >/dev/null 2>&1; then
23+
wget -O "$output" "$url"
24+
return
25+
fi
26+
27+
echo "curl or wget is required to download Magic." >&2
28+
exit 1
29+
}
30+
31+
echo "== Magic ${MAGIC_VERSION} source install =="
32+
echo "Source: $MAGIC_TARBALL_URL"
33+
echo "Prefix: $PREFIX"
34+
echo "Build root: $BUILD_ROOT"
35+
echo
36+
37+
if ! command -v apt-get >/dev/null 2>&1; then
38+
echo "This installer currently supports Ubuntu/Debian systems with apt-get." >&2
39+
exit 1
40+
fi
41+
42+
echo "Installing Magic build dependencies..."
43+
$SUDO apt-get update
44+
$SUDO apt-get install -y \
45+
build-essential \
46+
m4 \
47+
tcsh \
48+
tcl-dev \
49+
tk-dev \
50+
libcairo2-dev \
51+
libx11-dev \
52+
libxft-dev \
53+
libxrender-dev \
54+
libglu1-mesa-dev \
55+
curl \
56+
ca-certificates
57+
58+
mkdir -p "$BUILD_ROOT"
59+
cd "$BUILD_ROOT"
60+
61+
tarball="magic-${MAGIC_VERSION}.tgz"
62+
srcdir="magic-${MAGIC_VERSION}"
63+
64+
if [ ! -f "$tarball" ]; then
65+
echo "Downloading $tarball..."
66+
download "$MAGIC_TARBALL_URL" "$tarball"
67+
else
68+
echo "Using existing $BUILD_ROOT/$tarball"
69+
fi
70+
71+
rm -rf "$srcdir"
72+
tar -xzf "$tarball"
73+
cd "$srcdir"
74+
75+
echo "Configuring Magic..."
76+
./configure --prefix="$PREFIX"
77+
78+
echo "Building Magic..."
79+
make
80+
81+
echo "Installing Magic into $PREFIX..."
82+
$SUDO make install
83+
84+
echo
85+
echo "Installed Magic version:"
86+
"$PREFIX/bin/magic" -dnull -noconsole -version
87+
echo
88+
echo "If your shell still finds an older Magic first, put $PREFIX/bin before /usr/bin in PATH"
89+
echo "or set the Magic path in the app Preferences to:"
90+
echo " $PREFIX/bin/magic"

scripts/install_vlsi_env_ubuntu.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ apt-get install -y "${APT_PACKAGES[@]}"
6060
echo
6161
echo "Package installation finished."
6262
echo
63+
echo "Installing official Magic 8.3.634 source release for current SKY130 techfiles..."
64+
MAGIC_INSTALLER="$REPO_ROOT/scripts/install_magic_8_3_634_ubuntu.sh"
65+
if [ ! -f "$MAGIC_INSTALLER" ]; then
66+
echo "Magic source installer not found at $MAGIC_INSTALLER."
67+
exit 1
68+
fi
69+
/bin/bash "$MAGIC_INSTALLER"
70+
71+
echo
72+
echo "Magic source installation finished."
73+
echo
6374
echo "System bootstrap completed."
6475
echo "This script intentionally does NOT create or modify $REPO_ROOT/.venv."
6576
echo "Create the Python environment later as the normal user, for example:"

tests/test_env_validator.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,35 @@ def test_pdk_incomplete_is_not_reported_as_ready(self) -> None:
6464
self.assertEqual(pdk.status, "incomplete")
6565
self.assertIn("libs.tech/netgen", pdk.missing_subdirs)
6666

67+
def test_pdk_reports_magic_techfile_version_mismatch(self) -> None:
68+
with tempfile.TemporaryDirectory() as tmpdir:
69+
sky130a = Path(tmpdir) / "pdk" / "sky130A"
70+
for relative in (
71+
"libs.tech/magic",
72+
"libs.tech/netgen",
73+
"libs.tech/klayout",
74+
"libs.tech/ngspice",
75+
"libs.tech/xschem",
76+
):
77+
(sky130a / relative).mkdir(parents=True)
78+
(sky130a / "libs.tech" / "magic" / "sky130A.tech").write_text(
79+
"tech\nversion 8.3.411\n",
80+
encoding="utf-8",
81+
)
82+
self.settings.pdk_paths.pdk_root = str(Path(tmpdir) / "pdk")
83+
84+
with mock.patch.dict(os.environ, {}, clear=True):
85+
pdk = self.validator._detect_pdk(
86+
self.settings,
87+
lang="en",
88+
magic_version="Magic 8.3 revision 105 - Compiled on Mon, 06 Dec 2021 22:32:27 +0200.",
89+
)
90+
91+
self.assertTrue(pdk.found)
92+
self.assertEqual(pdk.status, "incompatible")
93+
self.assertIn("requires Magic 8.3.411", pdk.message)
94+
self.assertIn("Magic 8.3.105 was detected", pdk.message)
95+
6796
def test_root_owned_venv_problem_is_reported(self) -> None:
6897
with tempfile.TemporaryDirectory() as tmpdir:
6998
repo_root = Path(tmpdir)

0 commit comments

Comments
 (0)