Grio43 commited on
Commit
96992fa
·
verified ·
1 Parent(s): cbbe80b

Improve ONNX setup: proactive VC++/AVX2 checks, friendlier errors, post-install verification

Browse files
Files changed (1) hide show
  1. web_interface/app.py +242 -35
web_interface/app.py CHANGED
@@ -77,6 +77,180 @@ REQUIREMENTS = [
77
  ]
78
 
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  # ---------------------------------------------------------------------------
81
  # Bootstrap
82
  # ---------------------------------------------------------------------------
@@ -115,23 +289,32 @@ def _in_target_venv() -> bool:
115
 
116
 
117
  def _bootstrap(force_reinstall: bool) -> None:
 
 
 
 
118
  if not VENV_DIR.exists():
119
- print(f"[bootstrap] Creating virtualenv at {VENV_DIR} ...")
120
- venv.EnvBuilder(with_pip=True, clear=False, upgrade_deps=False).create(VENV_DIR)
 
 
 
 
 
121
 
122
  py = _venv_python()
123
- needs_install = force_reinstall or not MARKER.exists()
124
  if needs_install:
125
- print("[bootstrap] Upgrading pip ...")
126
- subprocess.check_call([str(py), "-m", "pip", "install", "--upgrade", "pip"])
127
- print(f"[bootstrap] Installing: {', '.join(REQUIREMENTS)}")
128
- subprocess.check_call([str(py), "-m", "pip", "install", *REQUIREMENTS])
129
- MARKER.write_text("ok\n", encoding="utf-8")
 
 
130
  else:
131
- print("[bootstrap] Requirements already installed (delete .venv/.bootstrapped to redo).")
132
 
133
  args = [a for a in sys.argv[1:] if a != "--reinstall"]
134
- print("[bootstrap] Re-launching inside venv ...\n")
135
  sys.exit(subprocess.call([str(py), str(Path(__file__).resolve()), *args]))
136
 
137
 
@@ -155,6 +338,10 @@ def _bootstrap_portable(force_reinstall: bool) -> None:
155
  we're already running inside the target interpreter.
156
  """
157
  py = Path(sys.executable)
 
 
 
 
158
 
159
  # 1) Enable site-packages by uncommenting `import site` in pythonXY._pth.
160
  pth_files = sorted(py.parent.glob("python*._pth"))
@@ -170,9 +357,9 @@ def _bootstrap_portable(force_reinstall: bool) -> None:
170
  if new_text != text:
171
  try:
172
  pth.write_text(new_text, encoding="utf-8")
173
- print(f"[bootstrap] Enabled site-packages in {pth.name}.")
174
  except OSError as e:
175
- print(f"[bootstrap] Could not edit {pth}: {e}")
176
 
177
  # 2) Install pip if missing.
178
  has_pip = subprocess.call(
@@ -181,12 +368,21 @@ def _bootstrap_portable(force_reinstall: bool) -> None:
181
  stderr=subprocess.DEVNULL,
182
  ) == 0
183
  if not has_pip:
184
- print("[bootstrap] Installing pip into portable Python ...")
185
  import urllib.request
186
  getpip = py.parent / "_get-pip.py"
187
  try:
188
- urllib.request.urlretrieve("https://bootstrap.pypa.io/get-pip.py", getpip)
189
- subprocess.check_call([str(py), str(getpip), "--no-warn-script-location"])
 
 
 
 
 
 
 
 
 
190
  finally:
191
  try:
192
  getpip.unlink()
@@ -194,15 +390,15 @@ def _bootstrap_portable(force_reinstall: bool) -> None:
194
  pass
195
 
196
  # 3) Install requirements (skipped if marker says we already did it).
197
- needs_install = force_reinstall or not PYENV_MARKER.exists()
198
  if needs_install:
199
- print("[bootstrap] Upgrading pip ...")
200
- subprocess.check_call([str(py), "-m", "pip", "install", "--upgrade", "pip"])
201
- print(f"[bootstrap] Installing: {', '.join(REQUIREMENTS)}")
202
- subprocess.check_call([str(py), "-m", "pip", "install", *REQUIREMENTS])
203
- PYENV_MARKER.write_text("ok\n", encoding="utf-8")
 
204
  else:
205
- print("[bootstrap] Requirements already installed (delete .pyenv/.bootstrapped to redo).")
206
 
207
 
208
  # ---------------------------------------------------------------------------
@@ -228,12 +424,19 @@ def _looks_like_native_load_failure(err: BaseException) -> bool:
228
 
229
  def _has_vcruntime() -> bool:
230
  """True if the VC++ 2015-2022 runtime DLLs are present in System32. We
231
- look at vcruntime140.dll and msvcp140.dll because onnxruntime's native
232
- code links against both."""
 
 
 
233
  if os.name != "nt":
234
  return True
235
  sys32 = Path(os.environ.get("WINDIR", r"C:\Windows")) / "System32"
236
- return (sys32 / "vcruntime140.dll").exists() and (sys32 / "msvcp140.dll").exists()
 
 
 
 
237
 
238
 
239
  def _has_avx2() -> bool:
@@ -949,17 +1152,21 @@ def main() -> None:
949
 
950
  force = "--reinstall" in sys.argv[1:]
951
 
952
- if _running_portable_python():
953
- # run.bat downloaded an embeddable Python because the user had none.
954
- # Install requirements directly into it; no venv re-exec needed.
955
- _bootstrap_portable(force_reinstall=force)
956
- _run_app()
957
- return
 
958
 
959
- if not _in_target_venv():
960
- _bootstrap(force_reinstall=force)
961
- return # _bootstrap re-execs and exits
962
- _run_app()
 
 
 
963
 
964
 
965
  if __name__ == "__main__":
 
77
  ]
78
 
79
 
80
+ # ---------------------------------------------------------------------------
81
+ # Setup helpers (pre-flight checks, friendlier install, post-install verify)
82
+ #
83
+ # These are called from _bootstrap / _bootstrap_portable below. They depend
84
+ # on the platform/runtime checks defined later in this file (_has_vcruntime,
85
+ # _has_avx2, _install_vcredist_with_uac) — Python resolves those at call
86
+ # time, so definition order doesn't matter.
87
+ # ---------------------------------------------------------------------------
88
+
89
+ def _check_internet(timeout: float = 4.0) -> bool:
90
+ """Cheap reachability probe for pypi.org.
91
+
92
+ Conservative on unknown errors: only a clean URLError (no DNS, refused,
93
+ timeout) returns False. We don't want a flaky probe to falsely block
94
+ setup when the real install would have worked."""
95
+ try:
96
+ import urllib.error
97
+ import urllib.request
98
+ urllib.request.urlopen("https://pypi.org/simple/", timeout=timeout)
99
+ return True
100
+ except urllib.error.URLError:
101
+ return False
102
+ except Exception: # noqa: BLE001
103
+ return True
104
+
105
+
106
+ def _friendly_pip_install(py: Path, packages: list[str], action: str) -> None:
107
+ """Run `pip install` and translate non-zero exits into actionable hints.
108
+
109
+ pip prints its own error output, so we add a one-line summary plus a
110
+ short list of common fixes — enough for a non-tech user to unblock
111
+ themselves without reading a stack trace."""
112
+ cmd = [str(py), "-m", "pip", "install", *packages]
113
+ try:
114
+ rc = subprocess.call(cmd)
115
+ except KeyboardInterrupt:
116
+ print("\n[setup] Cancelled by user.")
117
+ sys.exit(130)
118
+ if rc == 0:
119
+ return
120
+ print()
121
+ print(f"[setup] Failed to {action} (pip exit code {rc}).")
122
+ print("Common fixes:")
123
+ print(" - Check your internet connection (corporate proxy/VPN can block pypi.org)")
124
+ print(" - Make sure you have at least ~600 MB of free disk space")
125
+ print(" - Run this script from a folder you own (Desktop/Documents) so pip can write")
126
+ print(" - Once the issue is fixed, re-run with --reinstall to retry")
127
+ sys.exit(1)
128
+
129
+
130
+ def _requirements_for_this_cpu() -> list[str]:
131
+ """Return REQUIREMENTS adjusted for this CPU.
132
+
133
+ On non-AVX2 CPUs, pin onnxruntime up front to the last AVX-only release
134
+ so the install matches what the loader will accept — instead of pulling
135
+ a modern wheel pip is happy with but the import will reject."""
136
+ if not _has_avx2():
137
+ print(
138
+ "[setup] This CPU lacks AVX2 — pinning onnxruntime to "
139
+ f"{ORT_NO_AVX2_FALLBACK_VERSION} (last release with AVX-only wheels)."
140
+ )
141
+ return [
142
+ f"onnxruntime=={ORT_NO_AVX2_FALLBACK_VERSION}" if r.startswith("onnxruntime") else r
143
+ for r in REQUIREMENTS
144
+ ]
145
+ return list(REQUIREMENTS)
146
+
147
+
148
+ def _pre_install_environment_setup() -> None:
149
+ """Catch known-broken environments before we spend ~2 minutes on pip.
150
+
151
+ Verifies pypi.org is reachable and (on Windows) installs the VC++
152
+ runtime up front when missing. Doing this here saves the user from a
153
+ confusing install -> launch -> crash -> install -> launch dance, since
154
+ a missing VC++ runtime would otherwise only surface when onnxruntime is
155
+ first imported."""
156
+ if not _check_internet():
157
+ print("[setup] Could not reach https://pypi.org/.")
158
+ print(" Check your internet connection, then re-run this script.")
159
+ print(" If you're behind a corporate proxy, set HTTPS_PROXY first, e.g.")
160
+ print(" set HTTPS_PROXY=http://proxy.example.com:8080")
161
+ sys.exit(1)
162
+
163
+ if os.name == "nt" and not _has_vcruntime():
164
+ print("[setup] Microsoft Visual C++ runtime is missing — installing it now.")
165
+ print(" (one-time, ~25 MB; please accept the UAC prompt that appears)")
166
+ if not _install_vcredist_with_uac():
167
+ print("[setup] Could not install the VC++ runtime automatically.")
168
+ print(f" Download it from {VC_REDIST_URL}, run it manually,")
169
+ print(" then re-run this script.")
170
+ sys.exit(1)
171
+
172
+
173
+ def _verify_onnxruntime_loads(py: Path) -> tuple[bool, str]:
174
+ """Spawn `<py> -c 'import onnxruntime'` and report whether it succeeded.
175
+
176
+ A clean pip install does not guarantee the wheel will load: missing
177
+ vcruntime140_1.dll, antivirus quarantine of onnxruntime_pybind11_state.pyd,
178
+ and 32/64-bit Python mismatches all manifest only at first import.
179
+ Probing here lets the bootstrap recover before writing the success
180
+ marker, so the user only sees one set of progress messages."""
181
+ code = (
182
+ "import sys\n"
183
+ "try:\n"
184
+ " import onnxruntime # noqa: F401\n"
185
+ "except BaseException as e:\n"
186
+ " sys.stderr.write(repr(e))\n"
187
+ " sys.exit(1)\n"
188
+ )
189
+ try:
190
+ proc = subprocess.run(
191
+ [str(py), "-c", code], check=False, capture_output=True, text=True
192
+ )
193
+ except OSError as e:
194
+ return False, str(e)
195
+ if proc.returncode == 0:
196
+ return True, ""
197
+ return False, (proc.stderr or "").strip() or "unknown import failure"
198
+
199
+
200
+ def _bootstrap_recover(py: Path, err_repr: str) -> bool:
201
+ """Single-shot recovery for a failing onnxruntime import during setup.
202
+
203
+ Mirrors the in-app recovery in _recover_after_onnxruntime_load_failure
204
+ but stays in the setup stage (no re-exec), so the caller can re-run the
205
+ verification probe and only mark the install good once it actually works.
206
+ Returns True if a recovery action was attempted."""
207
+ if os.name != "nt":
208
+ return False
209
+ if "DLL load failed" not in err_repr and "ImportError" not in err_repr:
210
+ return False
211
+ if not _has_vcruntime():
212
+ print("[setup] VC++ runtime still missing — retrying installer.")
213
+ return _install_vcredist_with_uac()
214
+ if not _has_avx2():
215
+ print(
216
+ "[setup] CPU lacks AVX2 — reinstalling onnxruntime "
217
+ f"{ORT_NO_AVX2_FALLBACK_VERSION} ..."
218
+ )
219
+ try:
220
+ subprocess.check_call(
221
+ [str(py), "-m", "pip", "install", "--force-reinstall",
222
+ f"onnxruntime=={ORT_NO_AVX2_FALLBACK_VERSION}"]
223
+ )
224
+ return True
225
+ except subprocess.CalledProcessError:
226
+ return False
227
+ return False
228
+
229
+
230
+ def _finalize_install(py: Path, marker_path: Path) -> None:
231
+ """Verify onnxruntime imports, run one round of recovery if needed, then
232
+ write the success marker. Exits with a tailored diagnostic if recovery
233
+ can't make the import work."""
234
+ print("[setup] Verifying onnxruntime can load ...")
235
+ ok, err = _verify_onnxruntime_loads(py)
236
+ if not ok:
237
+ print(f"[setup] Initial import failed: {err}")
238
+ if _bootstrap_recover(py, err):
239
+ ok, err = _verify_onnxruntime_loads(py)
240
+ if not ok:
241
+ print()
242
+ print("[setup] onnxruntime still fails to load.")
243
+ print(f" Last error: {err}")
244
+ print("Likely causes:")
245
+ print(" - Antivirus quarantined onnxruntime_pybind11_state.pyd")
246
+ print(" (open Windows Defender's Protection History and restore it)")
247
+ print(" - Corrupt install — re-run this script with --reinstall")
248
+ print(" - 32-bit Python loading 64-bit wheels (or vice versa)")
249
+ sys.exit(1)
250
+ marker_path.write_text("ok\n", encoding="utf-8")
251
+ print("[setup] Done.")
252
+
253
+
254
  # ---------------------------------------------------------------------------
255
  # Bootstrap
256
  # ---------------------------------------------------------------------------
 
289
 
290
 
291
  def _bootstrap(force_reinstall: bool) -> None:
292
+ needs_install = force_reinstall or not VENV_DIR.exists() or not MARKER.exists()
293
+ if needs_install:
294
+ _pre_install_environment_setup()
295
+
296
  if not VENV_DIR.exists():
297
+ print(f"[setup] Creating Python environment in {VENV_DIR.name}/ ...")
298
+ try:
299
+ venv.EnvBuilder(with_pip=True, clear=False, upgrade_deps=False).create(VENV_DIR)
300
+ except Exception as e: # noqa: BLE001
301
+ print(f"[setup] Could not create virtualenv: {e}")
302
+ print(" Try running this script from a folder you own (Desktop/Documents).")
303
+ sys.exit(1)
304
 
305
  py = _venv_python()
 
306
  if needs_install:
307
+ print("[setup] Upgrading pip ...")
308
+ _friendly_pip_install(py, ["--upgrade", "pip"], "upgrade pip")
309
+ reqs = _requirements_for_this_cpu()
310
+ print("[setup] Installing dependencies (~500 MB, takes ~1-3 minutes) ...")
311
+ _friendly_pip_install(py, reqs, "install required packages")
312
+ _finalize_install(py, MARKER)
313
+ print("[setup] Launching the app.\n")
314
  else:
315
+ print("[setup] Environment ready (delete .venv/.bootstrapped to re-run setup).")
316
 
317
  args = [a for a in sys.argv[1:] if a != "--reinstall"]
 
318
  sys.exit(subprocess.call([str(py), str(Path(__file__).resolve()), *args]))
319
 
320
 
 
338
  we're already running inside the target interpreter.
339
  """
340
  py = Path(sys.executable)
341
+ needs_install = force_reinstall or not PYENV_MARKER.exists()
342
+
343
+ if needs_install:
344
+ _pre_install_environment_setup()
345
 
346
  # 1) Enable site-packages by uncommenting `import site` in pythonXY._pth.
347
  pth_files = sorted(py.parent.glob("python*._pth"))
 
357
  if new_text != text:
358
  try:
359
  pth.write_text(new_text, encoding="utf-8")
360
+ print(f"[setup] Enabled site-packages in {pth.name}.")
361
  except OSError as e:
362
+ print(f"[setup] Could not edit {pth}: {e}")
363
 
364
  # 2) Install pip if missing.
365
  has_pip = subprocess.call(
 
368
  stderr=subprocess.DEVNULL,
369
  ) == 0
370
  if not has_pip:
371
+ print("[setup] Installing pip into portable Python ...")
372
  import urllib.request
373
  getpip = py.parent / "_get-pip.py"
374
  try:
375
+ try:
376
+ urllib.request.urlretrieve("https://bootstrap.pypa.io/get-pip.py", getpip)
377
+ except Exception as e: # noqa: BLE001
378
+ print(f"[setup] Could not download get-pip.py: {e}")
379
+ print(" Check your internet connection, then re-run this script.")
380
+ sys.exit(1)
381
+ try:
382
+ subprocess.check_call([str(py), str(getpip), "--no-warn-script-location"])
383
+ except subprocess.CalledProcessError:
384
+ print("[setup] get-pip.py failed. Try deleting .pyenv\\ and re-running.")
385
+ sys.exit(1)
386
  finally:
387
  try:
388
  getpip.unlink()
 
390
  pass
391
 
392
  # 3) Install requirements (skipped if marker says we already did it).
 
393
  if needs_install:
394
+ print("[setup] Upgrading pip ...")
395
+ _friendly_pip_install(py, ["--upgrade", "pip"], "upgrade pip")
396
+ reqs = _requirements_for_this_cpu()
397
+ print("[setup] Installing dependencies (~500 MB, takes ~1-3 minutes) ...")
398
+ _friendly_pip_install(py, reqs, "install required packages")
399
+ _finalize_install(py, PYENV_MARKER)
400
  else:
401
+ print("[setup] Environment ready (delete .pyenv\\.bootstrapped to re-run setup).")
402
 
403
 
404
  # ---------------------------------------------------------------------------
 
424
 
425
  def _has_vcruntime() -> bool:
426
  """True if the VC++ 2015-2022 runtime DLLs are present in System32. We
427
+ look at vcruntime140.dll, vcruntime140_1.dll, and msvcp140.dll because
428
+ onnxruntime's native code links against all three — vcruntime140_1.dll
429
+ in particular ships the SEH unwinding helpers used by C++ exceptions on
430
+ x64, and a host with the older 14.0 runtime but no _1.dll still fails to
431
+ load with the same generic "DLL load failed" message."""
432
  if os.name != "nt":
433
  return True
434
  sys32 = Path(os.environ.get("WINDIR", r"C:\Windows")) / "System32"
435
+ return (
436
+ (sys32 / "vcruntime140.dll").exists()
437
+ and (sys32 / "vcruntime140_1.dll").exists()
438
+ and (sys32 / "msvcp140.dll").exists()
439
+ )
440
 
441
 
442
  def _has_avx2() -> bool:
 
1152
 
1153
  force = "--reinstall" in sys.argv[1:]
1154
 
1155
+ try:
1156
+ if _running_portable_python():
1157
+ # run.bat downloaded an embeddable Python because the user had none.
1158
+ # Install requirements directly into it; no venv re-exec needed.
1159
+ _bootstrap_portable(force_reinstall=force)
1160
+ _run_app()
1161
+ return
1162
 
1163
+ if not _in_target_venv():
1164
+ _bootstrap(force_reinstall=force)
1165
+ return # _bootstrap re-execs and exits
1166
+ _run_app()
1167
+ except KeyboardInterrupt:
1168
+ print("\n[setup] Cancelled by user.")
1169
+ sys.exit(130)
1170
 
1171
 
1172
  if __name__ == "__main__":