-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathsetup.py
More file actions
201 lines (167 loc) · 6.43 KB
/
Copy pathsetup.py
File metadata and controls
201 lines (167 loc) · 6.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
from __future__ import annotations
import hashlib
import os
from pathlib import Path
import re
import shutil
import site
import subprocess
from setuptools import setup
from setuptools.command.build_py import build_py as _build_py
from setuptools.command.install import install as _install
try:
from setuptools.command.develop import develop as _develop
except Exception: # pragma: no cover
_develop = None
try:
from setuptools.command.editable_wheel import editable_wheel as _editable_wheel
except Exception: # pragma: no cover
_editable_wheel = None
_WEBUI_BUILT = False
def _iter_site_packages_dirs() -> list[Path]:
out: list[Path] = []
for getter in (site.getsitepackages, lambda: [site.getusersitepackages()]):
try:
values = getter()
except Exception:
continue
for entry in values:
try:
path = Path(str(entry)).resolve()
except Exception:
continue
if path not in out:
out.append(path)
return out
def _cleanup_legacy_snowl_egg(announce: callable) -> None:
pattern = re.compile(r"(^|/)\.?snowl-[^/]+\.egg$")
for sp_dir in _iter_site_packages_dirs():
if not sp_dir.exists():
continue
easy_install = sp_dir / "easy-install.pth"
if easy_install.exists():
try:
lines = easy_install.read_text(encoding="utf-8").splitlines()
kept: list[str] = []
removed: list[str] = []
for line in lines:
stripped = line.strip()
if stripped and pattern.search(stripped.replace("\\", "/")):
removed.append(stripped)
continue
kept.append(line)
if removed:
easy_install.write_text("\n".join(kept) + ("\n" if kept else ""), encoding="utf-8")
announce(f"[install] removed stale snowl egg entries from {easy_install}", 2)
except Exception as exc:
announce(f"[install] warning: failed to rewrite {easy_install}: {exc}", 2)
for entry in sp_dir.glob("snowl-*.egg"):
try:
if entry.is_dir():
shutil.rmtree(entry)
else:
entry.unlink()
announce(f"[install] removed stale egg artifact: {entry}", 2)
except Exception as exc:
announce(f"[install] warning: failed to remove {entry}: {exc}", 2)
def _lock_hash(source_dir: Path) -> str:
lock_path = source_dir / "package-lock.json"
pkg_path = source_dir / "package.json"
if lock_path.exists():
payload = lock_path.read_bytes()
elif pkg_path.exists():
payload = pkg_path.read_bytes()
else:
raise RuntimeError(f"webui package metadata missing: {pkg_path}")
return hashlib.sha256(payload).hexdigest()
def _check_node_available() -> bool:
"""Return True if Node.js + npm (>=18) are available, False otherwise."""
node_path = shutil.which("node")
npm_path = shutil.which("npm")
if node_path is None or npm_path is None:
return False
try:
done = subprocess.run([node_path, "--version"], check=True, capture_output=True, text=True)
except Exception: # pragma: no cover
return False
version_text = (done.stdout or done.stderr or "").strip()
m = re.search(r"v?(\d+)\.", version_text)
if not m or int(m.group(1)) < 18:
return False
return True
def _resolve_webui_targets(project_root: Path) -> list[Path]:
targets: list[Path] = []
repo_webui = project_root / "webui"
bundled_webui = project_root / "snowl" / "_webui"
if (repo_webui / "package.json").exists():
targets.append(repo_webui)
if (bundled_webui / "package.json").exists() and bundled_webui not in targets:
targets.append(bundled_webui)
return targets
def _build_webui_once(announce: callable) -> None:
global _WEBUI_BUILT
_cleanup_legacy_snowl_egg(announce)
if _WEBUI_BUILT:
return
if os.getenv("SNOWL_SKIP_WEBUI_BUILD", "0").lower() in {"1", "true", "on", "yes"}:
announce("[webui] skipped by SNOWL_SKIP_WEBUI_BUILD=1", 2)
_WEBUI_BUILT = True
return
project_root = Path(__file__).resolve().parent
targets = _resolve_webui_targets(project_root)
if not targets:
announce("[webui] no webui package.json found; skip install-time build", 2)
_WEBUI_BUILT = True
return
if not _check_node_available():
announce("[webui] Node.js + npm (>=18) not found; skipping Web UI build. "
"Set SNOWL_SKIP_WEBUI_BUILD=1 to silence this message.", 2)
_WEBUI_BUILT = True
return
for app_dir in targets:
announce(f"[webui] npm ci ({app_dir})", 2)
subprocess.run(["npm", "ci"], cwd=str(app_dir), check=True)
announce(f"[webui] npm run build ({app_dir})", 2)
subprocess.run(["npm", "run", "build"], cwd=str(app_dir), check=True)
(app_dir / ".deps-lock.sha256").write_text(_lock_hash(app_dir), encoding="utf-8")
_WEBUI_BUILT = True
class BuildPy(_build_py):
def run(self):
_cleanup_legacy_snowl_egg(self.announce)
_build_webui_once(self.announce)
super().run()
_cleanup_legacy_snowl_egg(self.announce)
class Install(_install):
def run(self):
_cleanup_legacy_snowl_egg(self.announce)
_build_webui_once(self.announce)
super().run()
_cleanup_legacy_snowl_egg(self.announce)
if _develop is not None:
class Develop(_develop):
def run(self):
_cleanup_legacy_snowl_egg(self.announce)
_build_webui_once(self.announce)
super().run()
_cleanup_legacy_snowl_egg(self.announce)
else: # pragma: no cover
Develop = None
if _editable_wheel is not None:
class EditableWheel(_editable_wheel):
def run(self):
_cleanup_legacy_snowl_egg(self.announce)
_build_webui_once(self.announce)
super().run()
_cleanup_legacy_snowl_egg(self.announce)
else: # pragma: no cover
EditableWheel = None
_CMDCLASS: dict[str, type] = {
"build_py": BuildPy,
"install": Install,
}
if Develop is not None:
_CMDCLASS["develop"] = Develop
if EditableWheel is not None:
_CMDCLASS["editable_wheel"] = EditableWheel
# Build metadata is declared in pyproject.toml (PEP 621).
setup(cmdclass=_CMDCLASS)