amv-diffusion 2.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.
- amv_diffusion-2.1.0.dist-info/METADATA +180 -0
- amv_diffusion-2.1.0.dist-info/RECORD +39 -0
- amv_diffusion-2.1.0.dist-info/WHEEL +5 -0
- amv_diffusion-2.1.0.dist-info/entry_points.txt +7 -0
- amv_diffusion-2.1.0.dist-info/licenses/LICENSE +21 -0
- amv_diffusion-2.1.0.dist-info/top_level.txt +1 -0
- termux_diffusion/__init__.py +185 -0
- termux_diffusion/__main__.py +7 -0
- termux_diffusion/_version.py +3 -0
- termux_diffusion/adapter.py +41 -0
- termux_diffusion/canvas.py +364 -0
- termux_diffusion/cli.py +492 -0
- termux_diffusion/cluster.py +340 -0
- termux_diffusion/control/__init__.py +5 -0
- termux_diffusion/control/component.py +309 -0
- termux_diffusion/control/status.py +15 -0
- termux_diffusion/core.py +2936 -0
- termux_diffusion/data/bootstrap-manifest.json +15 -0
- termux_diffusion/data/prompt_cache/108a7149c4e70550.json +10 -0
- termux_diffusion/data/release-public-keys.json +10 -0
- termux_diffusion/data/validated-vulkan-profiles.json +298 -0
- termux_diffusion/detailer.py +283 -0
- termux_diffusion/doctor.py +128 -0
- termux_diffusion/downloader.py +111 -0
- termux_diffusion/engine.py +226 -0
- termux_diffusion/exceptions.py +158 -0
- termux_diffusion/hardware.py +985 -0
- termux_diffusion/hub.py +686 -0
- termux_diffusion/installer.py +499 -0
- termux_diffusion/locking.py +134 -0
- termux_diffusion/manifest.py +128 -0
- termux_diffusion/npu.py +267 -0
- termux_diffusion/platform.py +533 -0
- termux_diffusion/purger.py +145 -0
- termux_diffusion/py.typed +1 -0
- termux_diffusion/safety.py +150 -0
- termux_diffusion/selftest.py +178 -0
- termux_diffusion/web.py +3370 -0
- termux_diffusion/workflow.py +1345 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
"""Automated C++ core engine provisioning, binary locator, build healer, and doctor diagnostics."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import io
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
16
|
+
try:
|
|
17
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
18
|
+
except (AttributeError, io.UnsupportedOperation) as _out_err:
|
|
19
|
+
_ = _out_err
|
|
20
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
21
|
+
try:
|
|
22
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
23
|
+
except (AttributeError, io.UnsupportedOperation) as _err_err:
|
|
24
|
+
_ = _err_err
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
from .exceptions import ProvisioningError
|
|
29
|
+
from .platform import (
|
|
30
|
+
TERMUX_PREFIX,
|
|
31
|
+
check_memory_safety,
|
|
32
|
+
get_default_cache_dir,
|
|
33
|
+
get_memory_info,
|
|
34
|
+
is_android_termux,
|
|
35
|
+
is_arm64,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from .engine import (
|
|
39
|
+
get_engine_bin_dir,
|
|
40
|
+
get_engine_lib_dir,
|
|
41
|
+
get_binary_path,
|
|
42
|
+
locate_sd_cli,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
from ._version import __version__
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger("termux_diffusion.installer")
|
|
48
|
+
|
|
49
|
+
SD_CPP_REPO = "https://github.com/leejet/stable-diffusion.cpp"
|
|
50
|
+
|
|
51
|
+
TERMUX_DIFFUSION_RELEASE_LATEST = "https://github.com/uno-km/termux-diffusion/releases/latest/download"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_prebuilt_base_url() -> str:
|
|
55
|
+
"""Resolve dynamic base URL for prebuilt binary assets."""
|
|
56
|
+
if custom := os.environ.get("TERMUX_DIFFUSION_RELEASE_BASE") or os.environ.get("AMEVA_RELEASE_BASE"):
|
|
57
|
+
return custom.rstrip("/")
|
|
58
|
+
if custom_tag := os.environ.get("TERMUX_DIFFUSION_RELEASE_TAG") or os.environ.get("AMEVA_RELEASE_TAG"):
|
|
59
|
+
tag = custom_tag if custom_tag.startswith("v") else f"v{custom_tag}"
|
|
60
|
+
return f"https://github.com/uno-km/termux-diffusion/releases/download/{tag}"
|
|
61
|
+
return f"https://github.com/uno-km/termux-diffusion/releases/download/v{__version__}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
PREBUILT_BASE_URL = get_prebuilt_base_url()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_candidate_prebuilt_urls(filename: str = "sd-cli-cpu-android-arm64.tar.gz") -> List[str]:
|
|
68
|
+
"""Resolve prioritized candidate URLs for downloading prebuilt engine packages."""
|
|
69
|
+
urls: List[str] = []
|
|
70
|
+
if custom_base := os.environ.get("TERMUX_DIFFUSION_RELEASE_BASE") or os.environ.get("AMEVA_RELEASE_BASE"):
|
|
71
|
+
urls.append(f"{custom_base.rstrip('/')}/{filename}")
|
|
72
|
+
# Tier 2: GitHub Releases latest canonical endpoint (Zero-Hardcoding SSOT)
|
|
73
|
+
urls.append(f"{TERMUX_DIFFUSION_RELEASE_LATEST}/{filename}")
|
|
74
|
+
|
|
75
|
+
# Tier 3: Installed package dynamic version matching
|
|
76
|
+
urls.append(f"https://github.com/uno-km/termux-diffusion/releases/download/v{__version__}/{filename}")
|
|
77
|
+
return urls
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
import uuid
|
|
81
|
+
from .locking import InstallLock
|
|
82
|
+
from .downloader import atomic_download_file
|
|
83
|
+
from .selftest import run_binary_self_test
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def activate_binary(bin_dir: Path, target_name: str) -> Path:
|
|
87
|
+
"""Atomically activate target_name symlink to sd-cli using os.replace in same directory."""
|
|
88
|
+
allowed = {
|
|
89
|
+
"sd-cli-cpu",
|
|
90
|
+
"sd-cli-source-cpu",
|
|
91
|
+
"sd-cli",
|
|
92
|
+
"sd-cli-vulkan",
|
|
93
|
+
}
|
|
94
|
+
if target_name not in allowed:
|
|
95
|
+
raise ValueError(f"Unauthorized activation target: {target_name}")
|
|
96
|
+
|
|
97
|
+
target = bin_dir / target_name
|
|
98
|
+
active = bin_dir / "sd-cli"
|
|
99
|
+
temporary = bin_dir / f".sd-cli.{uuid.uuid4().hex}.tmp"
|
|
100
|
+
|
|
101
|
+
if not target.is_file() or target.is_symlink():
|
|
102
|
+
raise RuntimeError(f"Activation target {target_name} is not a valid regular file")
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
try:
|
|
106
|
+
os.symlink(target_name, temporary)
|
|
107
|
+
os.replace(temporary, active)
|
|
108
|
+
except (NotImplementedError, OSError):
|
|
109
|
+
# symlink 미지원 OS(일부 Windows, FAT32 등) — copy2 fallback.
|
|
110
|
+
logger.warning(
|
|
111
|
+
"[installer] symlink not supported; using copy2 fallback for %s -> %s",
|
|
112
|
+
target_name, active,
|
|
113
|
+
)
|
|
114
|
+
shutil.copy2(target, active)
|
|
115
|
+
return active
|
|
116
|
+
finally:
|
|
117
|
+
if temporary.exists() or temporary.is_symlink():
|
|
118
|
+
try:
|
|
119
|
+
temporary.unlink()
|
|
120
|
+
except (PermissionError, OSError) as _tmp_err:
|
|
121
|
+
logger.warning(
|
|
122
|
+
"[installer] Failed to clean up temporary symlink %s: %s. "
|
|
123
|
+
"Manual cleanup may be required.",
|
|
124
|
+
temporary, _tmp_err,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def is_valid_elf(path: Path) -> bool:
|
|
131
|
+
"""Verifies that the target path is a valid ELF executable/library via magic bytes."""
|
|
132
|
+
try:
|
|
133
|
+
p = path.resolve() if path.is_symlink() else path
|
|
134
|
+
if not p.is_file():
|
|
135
|
+
return False
|
|
136
|
+
with open(p, "rb") as f:
|
|
137
|
+
return f.read(4) == b"\x7fELF"
|
|
138
|
+
except (OSError, PermissionError):
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def fetch_prebuilt_binary(
|
|
143
|
+
install_mode: str = "prebuilt-first",
|
|
144
|
+
force: bool = False,
|
|
145
|
+
dedicate: bool = False,
|
|
146
|
+
) -> Optional[Path]:
|
|
147
|
+
"""Try acquiring prebuilt Bionic ARM64 CPU baseline binary with integrity verification and self-test."""
|
|
148
|
+
if not (is_android_termux() and is_arm64()):
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
bin_dir = get_engine_bin_dir()
|
|
152
|
+
lock_file = get_default_cache_dir() / "install.lock"
|
|
153
|
+
|
|
154
|
+
with InstallLock(lock_file, timeout_sec=5.0):
|
|
155
|
+
if install_mode == "source-only":
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
cpu_bin = bin_dir / "sd-cli-cpu"
|
|
159
|
+
lib_dir = get_engine_lib_dir()
|
|
160
|
+
omp_so = lib_dir / "libomp.so"
|
|
161
|
+
|
|
162
|
+
# Dedicated triage mode
|
|
163
|
+
if dedicate:
|
|
164
|
+
active_link = bin_dir / "sd-cli"
|
|
165
|
+
if active_link.is_symlink():
|
|
166
|
+
target = str(active_link.resolve())
|
|
167
|
+
if ".local/share/ameva" in target:
|
|
168
|
+
print("[termux-diffusion] [DEDICATE] AMEVA Runtime managed engine detected. Preserving co-existence (<0.002s).")
|
|
169
|
+
return active_link
|
|
170
|
+
|
|
171
|
+
# If force, clear existing binaries to guarantee fresh acquisition
|
|
172
|
+
if force:
|
|
173
|
+
if cpu_bin.is_file():
|
|
174
|
+
cpu_bin.unlink(missing_ok=True)
|
|
175
|
+
if omp_so.is_file():
|
|
176
|
+
omp_so.unlink(missing_ok=True)
|
|
177
|
+
|
|
178
|
+
# "있어? 넘어가" - Skip if verified ELF binary already exists
|
|
179
|
+
if not force and cpu_bin.is_file() and is_valid_elf(cpu_bin):
|
|
180
|
+
if omp_so.is_file() and is_valid_elf(omp_so):
|
|
181
|
+
active = activate_binary(bin_dir, "sd-cli-cpu")
|
|
182
|
+
print("[termux-diffusion] [OK] Verified native CPU engine already present. Skipping download (<0.002s).")
|
|
183
|
+
return active
|
|
184
|
+
|
|
185
|
+
print("[termux-diffusion] Attempting Prebuilt CPU Baseline Engine installation...")
|
|
186
|
+
try:
|
|
187
|
+
# 1. Ensure OpenMP companion library (libomp.so)
|
|
188
|
+
if force or not omp_so.is_file() or not is_valid_elf(omp_so):
|
|
189
|
+
omp_urls = get_candidate_prebuilt_urls("libomp-android-arm64.so")
|
|
190
|
+
print("[termux-diffusion] Provisioning OpenMP parallel runtime (libomp.so)...")
|
|
191
|
+
for o_url in omp_urls:
|
|
192
|
+
try:
|
|
193
|
+
atomic_download_file(o_url, omp_so)
|
|
194
|
+
if omp_so.is_file() and is_valid_elf(omp_so):
|
|
195
|
+
break
|
|
196
|
+
except Exception as o_err:
|
|
197
|
+
logger.debug("OpenMP download candidate failed from %s: %s", o_url, o_err)
|
|
198
|
+
|
|
199
|
+
if omp_so.is_file():
|
|
200
|
+
try:
|
|
201
|
+
omp_so.chmod(0o755)
|
|
202
|
+
except OSError:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
# 2. Ensure sd-cli-cpu binary
|
|
206
|
+
if force or not cpu_bin.is_file() or not is_valid_elf(cpu_bin):
|
|
207
|
+
candidate_urls = get_candidate_prebuilt_urls("sd-cli-cpu-android-arm64.tar.gz")
|
|
208
|
+
staging_dir = get_default_cache_dir() / ".staging-diffusion-cpu"
|
|
209
|
+
staging_dir.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
tar_dest = staging_dir / "cpu-prebuilt.tar.gz"
|
|
211
|
+
downloaded = False
|
|
212
|
+
for pkg_url in candidate_urls:
|
|
213
|
+
try:
|
|
214
|
+
print(f"[termux-diffusion] Downloading CPU baseline binary from: {pkg_url} ...")
|
|
215
|
+
atomic_download_file(pkg_url, tar_dest)
|
|
216
|
+
downloaded = True
|
|
217
|
+
break
|
|
218
|
+
except Exception as dl_err:
|
|
219
|
+
logger.debug("CPU prebuilt download candidate failed from %s: %s", pkg_url, dl_err)
|
|
220
|
+
|
|
221
|
+
if downloaded and tar_dest.is_file():
|
|
222
|
+
import tarfile
|
|
223
|
+
with tarfile.open(tar_dest, "r:gz") as tar:
|
|
224
|
+
for member in tar.getmembers():
|
|
225
|
+
member_path = os.path.realpath(os.path.join(staging_dir, member.name))
|
|
226
|
+
if not member_path.startswith(os.path.realpath(staging_dir)):
|
|
227
|
+
raise ProvisioningError(
|
|
228
|
+
f"[termux-diffusion] E_TAR_PATH_ESCAPE: tarball 내 경로 탈출 시도가 감지되어 추출을 중단했습니다: {member.name}"
|
|
229
|
+
)
|
|
230
|
+
tar.extractall(path=staging_dir)
|
|
231
|
+
tar_dest.unlink(missing_ok=True)
|
|
232
|
+
|
|
233
|
+
for cand in staging_dir.rglob("sd-cli*"):
|
|
234
|
+
if cand.is_file() and is_valid_elf(cand):
|
|
235
|
+
shutil.copy2(cand, cpu_bin)
|
|
236
|
+
cpu_bin.chmod(0o755)
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
|
240
|
+
|
|
241
|
+
if cpu_bin.is_file() and is_valid_elf(cpu_bin) and run_binary_self_test(cpu_bin, expected_backend="cpu").stage1_load_passed:
|
|
242
|
+
active = activate_binary(bin_dir, "sd-cli-cpu")
|
|
243
|
+
print("[termux-diffusion] Fast-Track: Prebuilt CPU Baseline binary validated and activated.")
|
|
244
|
+
return active
|
|
245
|
+
except ProvisioningError:
|
|
246
|
+
raise
|
|
247
|
+
except Exception as exc:
|
|
248
|
+
logger.debug("CPU prebuilt attempt failed: %s", exc)
|
|
249
|
+
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def provision_engine(
|
|
254
|
+
force: bool = False,
|
|
255
|
+
install_mode: str = "prebuilt-first",
|
|
256
|
+
jobs: Optional[int] = None,
|
|
257
|
+
make_jobs: Optional[int] = None,
|
|
258
|
+
backend: str = "cpu",
|
|
259
|
+
dedicate: bool = False,
|
|
260
|
+
) -> Path:
|
|
261
|
+
"""Download, verify, or compile stable-diffusion.cpp into ~/.cache/termux-diffusion/bin/sd-cli."""
|
|
262
|
+
bin_dir = get_engine_bin_dir()
|
|
263
|
+
|
|
264
|
+
# Prebuilt-First Pipeline (Pure CPU Baseline Engine)
|
|
265
|
+
if install_mode in ("prebuilt-first", "prebuilt-only"):
|
|
266
|
+
prebuilt = fetch_prebuilt_binary(install_mode=install_mode, force=force, dedicate=dedicate)
|
|
267
|
+
if prebuilt:
|
|
268
|
+
return prebuilt
|
|
269
|
+
|
|
270
|
+
# COMPILATION GATE: Never compile automatically without explicit user opt-in (Zero Silent Fallback)
|
|
271
|
+
allow_source = os.environ.get("TERMUX_DIFFUSION_ALLOW_SOURCE_BUILD") == "1"
|
|
272
|
+
if not allow_source or install_mode == "prebuilt-only":
|
|
273
|
+
raise ProvisioningError(
|
|
274
|
+
"E_PREBUILT_UNAVAILABLE: Precompiled native CPU engine (sd-cli-cpu) is not installed.\n"
|
|
275
|
+
"No precompiled binary was found locally and automated acquisition from GitHub Releases failed.\n"
|
|
276
|
+
"On-device compilation is disabled by default to prevent thermal throttling, excessive battery drain, and memory exhaustion.\n\n"
|
|
277
|
+
"--> ACTION REQUIRED:\n"
|
|
278
|
+
"1. Ensure internet access to GitHub and run: 'termux-diffusion install'\n"
|
|
279
|
+
"2. Or manually download 'sd-cli-cpu-android-arm64.tar.gz' and 'libomp-android-arm64.so' from:\n"
|
|
280
|
+
" https://github.com/uno-km/termux-diffusion/releases/latest\n"
|
|
281
|
+
"3. If you explicitly want to compile from C++ source on this device, you MUST specify:\n"
|
|
282
|
+
" export TERMUX_DIFFUSION_ALLOW_SOURCE_BUILD=1\n"
|
|
283
|
+
" and optionally set compilation cores:\n"
|
|
284
|
+
" export TERMUX_DIFFUSION_JOBS=<cores>\n",
|
|
285
|
+
code="E_PREBUILT_UNAVAILABLE"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
print("[termux-diffusion] Initializing native ARM64 Bionic CPU engine source compilation...")
|
|
289
|
+
|
|
290
|
+
# Step 1: Ensure required system packages
|
|
291
|
+
if is_android_termux() and shutil.which("pkg"):
|
|
292
|
+
print("[termux-diffusion] Checking build toolchains...")
|
|
293
|
+
try:
|
|
294
|
+
subprocess.run(
|
|
295
|
+
[
|
|
296
|
+
"pkg", "install", "-y",
|
|
297
|
+
"git", "cmake", "clang", "make", "termux-api", "wget"
|
|
298
|
+
],
|
|
299
|
+
capture_output=False,
|
|
300
|
+
check=False,
|
|
301
|
+
timeout=180.0
|
|
302
|
+
)
|
|
303
|
+
except Exception as exc:
|
|
304
|
+
logger.warning("pkg install invocation warning: %s", exc)
|
|
305
|
+
|
|
306
|
+
# Verify toolchain existence
|
|
307
|
+
has_compiler = bool(shutil.which("clang") or shutil.which("gcc") or shutil.which("clang++"))
|
|
308
|
+
has_cmake = bool(shutil.which("cmake"))
|
|
309
|
+
has_git = bool(shutil.which("git"))
|
|
310
|
+
|
|
311
|
+
if not (has_compiler and has_cmake and has_git):
|
|
312
|
+
missing = []
|
|
313
|
+
if not has_compiler:
|
|
314
|
+
missing.append("clang / gcc")
|
|
315
|
+
if not has_cmake:
|
|
316
|
+
missing.append("cmake")
|
|
317
|
+
if not has_git:
|
|
318
|
+
missing.append("git")
|
|
319
|
+
raise ProvisioningError(
|
|
320
|
+
f"Missing required build tools: {', '.join(missing)}. "
|
|
321
|
+
f"Please run 'pkg install -y clang cmake git termux-api' before provisioning."
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Step 2: Set up build directory
|
|
325
|
+
build_root = get_default_cache_dir() / "build_src"
|
|
326
|
+
build_root.mkdir(parents=True, exist_ok=True)
|
|
327
|
+
repo_dir = build_root / "stable-diffusion.cpp"
|
|
328
|
+
|
|
329
|
+
if not repo_dir.exists():
|
|
330
|
+
print(f"[termux-diffusion] Cloning {SD_CPP_REPO} (depth=1, recursive)...")
|
|
331
|
+
try:
|
|
332
|
+
res = subprocess.run(
|
|
333
|
+
["git", "clone", "--depth", "1", "--recursive", SD_CPP_REPO, str(repo_dir)],
|
|
334
|
+
capture_output=True,
|
|
335
|
+
text=True,
|
|
336
|
+
timeout=180.0
|
|
337
|
+
)
|
|
338
|
+
if res.returncode != 0:
|
|
339
|
+
raise ProvisioningError(f"Failed cloning stable-diffusion.cpp repository: {res.stderr.strip()}")
|
|
340
|
+
except subprocess.TimeoutExpired as exc:
|
|
341
|
+
raise ProvisioningError(
|
|
342
|
+
"Network timeout (180s) while cloning stable-diffusion.cpp. "
|
|
343
|
+
"Please check your internet connection or install sd-cli manually."
|
|
344
|
+
) from exc
|
|
345
|
+
|
|
346
|
+
# Step 3: Crucial Submodule Update (Ensures ggml is present)
|
|
347
|
+
print("[termux-diffusion] Synchronizing tensor submodules (ggml)...")
|
|
348
|
+
try:
|
|
349
|
+
sub_res = subprocess.run(
|
|
350
|
+
["git", "submodule", "update", "--init", "--recursive", "--depth", "1"],
|
|
351
|
+
cwd=str(repo_dir),
|
|
352
|
+
capture_output=True,
|
|
353
|
+
text=True,
|
|
354
|
+
timeout=180.0,
|
|
355
|
+
check=False
|
|
356
|
+
)
|
|
357
|
+
if sub_res.returncode != 0:
|
|
358
|
+
logger.warning("Git submodule sync note: %s", sub_res.stderr)
|
|
359
|
+
except subprocess.TimeoutExpired:
|
|
360
|
+
logger.warning("Submodule sync timed out after 45s. Proceeding with existing source files.")
|
|
361
|
+
|
|
362
|
+
# Bionic Healer: Fix Termux libwebp NDK cpu-features dependency
|
|
363
|
+
webp_cmake = repo_dir / "thirdparty" / "libwebp" / "CMakeLists.txt"
|
|
364
|
+
if webp_cmake.is_file():
|
|
365
|
+
try:
|
|
366
|
+
content = webp_cmake.read_text(encoding="utf-8")
|
|
367
|
+
content = content.replace("if(ANDROID)", "if(FALSE)").replace("if (ANDROID)", "if(FALSE)")
|
|
368
|
+
webp_cmake.write_text(content, encoding="utf-8")
|
|
369
|
+
logger.info("Patched libwebp CMakeLists.txt for Termux Bionic compatibility.")
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logger.warning("libwebp patch note: %s", e)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# Step 4: CMake & Compilation - Use hardware-detected optimal flags
|
|
376
|
+
build_dir = repo_dir / "build"
|
|
377
|
+
if build_dir.exists():
|
|
378
|
+
shutil.rmtree(build_dir, ignore_errors=True)
|
|
379
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
380
|
+
|
|
381
|
+
from .hardware import detect_hardware_profile, format_hardware_report, _build_cmake_flags
|
|
382
|
+
hw_profile = detect_hardware_profile()
|
|
383
|
+
print(f"[termux-diffusion] Detected SoC: {hw_profile.soc_name}, GPU: {hw_profile.gpu_name}")
|
|
384
|
+
print(f"[termux-diffusion] Vulkan: {'Available' if hw_profile.vulkan_available else 'Not Found'}, "
|
|
385
|
+
f"OpenCL: {'Available' if hw_profile.opencl_available else 'Not Found'}")
|
|
386
|
+
print(f"[termux-diffusion] CPU Extensions: DotProd={'Y' if hw_profile.has_dotprod else 'N'} "
|
|
387
|
+
f"FP16={'Y' if hw_profile.has_fp16 else 'N'} I8MM={'Y' if hw_profile.has_i8mm else 'N'}")
|
|
388
|
+
print("[termux-diffusion] Target build backend: cpu")
|
|
389
|
+
|
|
390
|
+
print("[termux-diffusion] Configuring CMake build with device-optimized flags...")
|
|
391
|
+
use_ninja = bool(shutil.which("ninja"))
|
|
392
|
+
cmake_cmd = [
|
|
393
|
+
"cmake", "..",
|
|
394
|
+
"-DCMAKE_BUILD_TYPE=Release",
|
|
395
|
+
"-DCMAKE_SYSTEM_NAME=Linux",
|
|
396
|
+
"-DANDROID=OFF",
|
|
397
|
+
"-DSD_BUILD_EXAMPLES=ON",
|
|
398
|
+
"-DGGML_OPENMP=OFF",
|
|
399
|
+
"-DWEBP_ENABLE_SIMD=OFF",
|
|
400
|
+
]
|
|
401
|
+
if use_ninja:
|
|
402
|
+
cmake_cmd.extend(["-G", "Ninja"])
|
|
403
|
+
|
|
404
|
+
# Append hardware-specific flags (DotProd, FP16, etc. with pure CPU backend)
|
|
405
|
+
flags = _build_cmake_flags(hw_profile, backend="cpu")
|
|
406
|
+
cmake_cmd.extend(flags)
|
|
407
|
+
cmake_res = subprocess.run(
|
|
408
|
+
cmake_cmd,
|
|
409
|
+
cwd=str(build_dir),
|
|
410
|
+
capture_output=True,
|
|
411
|
+
text=True
|
|
412
|
+
)
|
|
413
|
+
if cmake_res.returncode != 0:
|
|
414
|
+
raise ProvisioningError(f"CMake configuration failed: {cmake_res.stderr.strip()}")
|
|
415
|
+
|
|
416
|
+
# Determine parallel compilation jobs
|
|
417
|
+
env_jobs = os.environ.get("TERMUX_DIFFUSION_MAKE_JOBS") or os.environ.get("TERMUX_DIFFUSION_JOBS")
|
|
418
|
+
if jobs is not None:
|
|
419
|
+
actual_jobs = max(1, int(jobs))
|
|
420
|
+
elif make_jobs is not None:
|
|
421
|
+
actual_jobs = max(1, int(make_jobs))
|
|
422
|
+
elif env_jobs and env_jobs.strip().isdigit():
|
|
423
|
+
actual_jobs = max(1, int(env_jobs.strip()))
|
|
424
|
+
else:
|
|
425
|
+
mem_info = get_memory_info()
|
|
426
|
+
cpu_cores = os.cpu_count() or 2
|
|
427
|
+
total_ram = mem_info.get("effective_total_mb") or mem_info.get("mem_total_mb", 4096)
|
|
428
|
+
if total_ram < 4096:
|
|
429
|
+
actual_jobs = 1 # RAM < 4GB: Single job to prevent Clang compiler OOM/LMK
|
|
430
|
+
elif total_ram < 8192:
|
|
431
|
+
actual_jobs = min(2, cpu_cores) # RAM 4-8GB: 2 parallel jobs
|
|
432
|
+
else:
|
|
433
|
+
actual_jobs = min(4, cpu_cores) # RAM 8GB+: Up to 4 parallel jobs
|
|
434
|
+
|
|
435
|
+
builder_name = "ninja" if use_ninja else "make"
|
|
436
|
+
print(f"[termux-diffusion] Compiling native Bionic binary with clang ({builder_name} sd-cli -j{actual_jobs})...")
|
|
437
|
+
build_run_cmd = ["ninja", "sd-cli", f"-j{actual_jobs}"] if use_ninja else ["make", "sd-cli", f"-j{actual_jobs}"]
|
|
438
|
+
make_res = subprocess.run(
|
|
439
|
+
build_run_cmd,
|
|
440
|
+
cwd=str(build_dir),
|
|
441
|
+
capture_output=True,
|
|
442
|
+
text=True
|
|
443
|
+
)
|
|
444
|
+
if make_res.returncode != 0:
|
|
445
|
+
full_err = (make_res.stdout or "") + "\n" + (make_res.stderr or "")
|
|
446
|
+
err_tail = "\n".join(full_err.strip().splitlines()[-15:]) if full_err.strip() else "No compiler error output"
|
|
447
|
+
raise ProvisioningError(
|
|
448
|
+
f"Compilation failed with exit code {make_res.returncode}.\n"
|
|
449
|
+
f"Compiler Error:\n{err_tail}\n"
|
|
450
|
+
"Please run 'termux-diffusion doctor' to diagnose missing headers."
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# Locate compiled binary
|
|
454
|
+
compiled_bin = None
|
|
455
|
+
for candidate in (build_dir / "bin" / "sd-cli", build_dir / "bin" / "sd", build_dir / "sd-cli", build_dir / "sd"):
|
|
456
|
+
if candidate.is_file():
|
|
457
|
+
compiled_bin = candidate
|
|
458
|
+
break
|
|
459
|
+
|
|
460
|
+
if not compiled_bin:
|
|
461
|
+
raise ProvisioningError("Could not locate compiled binary in build directory.")
|
|
462
|
+
|
|
463
|
+
# Install into cache bin directory using atomic rename
|
|
464
|
+
target_bin = get_engine_bin_dir() / "sd-cli"
|
|
465
|
+
temp_bin = target_bin.with_name(f"sd-cli.{os.getpid()}.part")
|
|
466
|
+
shutil.copy2(compiled_bin, temp_bin)
|
|
467
|
+
temp_bin.chmod(0o755)
|
|
468
|
+
os.replace(temp_bin, target_bin)
|
|
469
|
+
|
|
470
|
+
print(f"[termux-diffusion] Engine provisioned successfully at: {target_bin}")
|
|
471
|
+
install_auxiliary_assets()
|
|
472
|
+
return target_bin.resolve()
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def install_auxiliary_assets() -> None:
|
|
476
|
+
"""Assetize and deploy pre-cached prompt embeddings and auxiliary models into user cache."""
|
|
477
|
+
cache_root = get_default_cache_dir()
|
|
478
|
+
prompt_cache_target = cache_root / "prompt_cache"
|
|
479
|
+
prompt_cache_target.mkdir(parents=True, exist_ok=True)
|
|
480
|
+
|
|
481
|
+
bundled_prompts = Path(__file__).parent / "data" / "prompt_cache"
|
|
482
|
+
if bundled_prompts.is_dir():
|
|
483
|
+
copied_count = 0
|
|
484
|
+
for asset in bundled_prompts.iterdir():
|
|
485
|
+
if asset.is_file():
|
|
486
|
+
dest = prompt_cache_target / asset.name
|
|
487
|
+
if not dest.exists():
|
|
488
|
+
shutil.copy2(asset, dest)
|
|
489
|
+
copied_count += 1
|
|
490
|
+
logger.info("[termux-diffusion] Assetized prompt cache entry: %s", asset.name)
|
|
491
|
+
if copied_count > 0:
|
|
492
|
+
print(f"[termux-diffusion] Deployed {copied_count} bundled prompt cache asset(s).")
|
|
493
|
+
|
|
494
|
+
print(f"[termux-diffusion] Prompt cache assets initialized at: {prompt_cache_target}")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# Re-export diagnostic utilities from .doctor for backward compatibility
|
|
498
|
+
from .doctor import run_doctor
|
|
499
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR Commercial-License
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from .exceptions import InstallLockError
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("termux_diffusion.locking")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InstallLock:
|
|
16
|
+
"""File lock manager controlling full installer state machine execution with stale PID auto-recovery."""
|
|
17
|
+
def __init__(self, lock_file: Path, timeout_sec: float = 5.0):
|
|
18
|
+
self.lock_file = lock_file
|
|
19
|
+
self.timeout_sec = timeout_sec
|
|
20
|
+
self._acquired = False
|
|
21
|
+
|
|
22
|
+
def _is_pid_alive(self, pid: int) -> bool | None:
|
|
23
|
+
"""Check if PID is alive.
|
|
24
|
+
|
|
25
|
+
반환:
|
|
26
|
+
True → 프로세스 확인됨
|
|
27
|
+
False → 프로세스 종료됨
|
|
28
|
+
None → 측정 불가 (PermissionError 등)
|
|
29
|
+
→ 호출자는 None을 alive=True로 변환하지 말 것
|
|
30
|
+
"""
|
|
31
|
+
if pid <= 0:
|
|
32
|
+
return False
|
|
33
|
+
if sys.platform == "win32":
|
|
34
|
+
try:
|
|
35
|
+
import ctypes
|
|
36
|
+
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
37
|
+
h = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
|
38
|
+
if h:
|
|
39
|
+
ctypes.windll.kernel32.CloseHandle(h)
|
|
40
|
+
return True
|
|
41
|
+
# OpenProcess가 NULL 반환 — 프로세스 없음 또는 접근 거부
|
|
42
|
+
# GetLastError()로 구분 가능하나 여기서는 False(종료)로 처리
|
|
43
|
+
return False
|
|
44
|
+
except PermissionError:
|
|
45
|
+
# 측정 불가 — 스테일 판정을 False로 하지 않음
|
|
46
|
+
logger.warning("[locking] _is_pid_alive PermissionError for pid=%d (unmeasurable)", pid)
|
|
47
|
+
return None
|
|
48
|
+
except (AttributeError, OSError) as _win_err:
|
|
49
|
+
# ctypes.windll 미지원 또는 Win32 API 오류
|
|
50
|
+
logger.warning("[locking] _is_pid_alive Win32 error for pid=%d: %s (unmeasurable)", pid, _win_err)
|
|
51
|
+
return None
|
|
52
|
+
# 예상 밖 예외는 재발생
|
|
53
|
+
else:
|
|
54
|
+
try:
|
|
55
|
+
os.kill(pid, 0)
|
|
56
|
+
return True
|
|
57
|
+
except ProcessLookupError:
|
|
58
|
+
return False # Allowed: process gone -> correctly identified as not alive.
|
|
59
|
+
except PermissionError:
|
|
60
|
+
# 권한 없음 — 측정 불가
|
|
61
|
+
logger.warning("[locking] _is_pid_alive PermissionError for pid=%d (unmeasurable)", pid)
|
|
62
|
+
return None
|
|
63
|
+
except OSError as _os_err:
|
|
64
|
+
logger.warning("[locking] _is_pid_alive OSError for pid=%d: %s", pid, _os_err)
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _check_and_clear_stale_lock(self) -> bool:
|
|
70
|
+
"""Check if existing lock file belongs to a deceased process, removing it if stale."""
|
|
71
|
+
if not self.lock_file.exists():
|
|
72
|
+
return True
|
|
73
|
+
try:
|
|
74
|
+
content = self.lock_file.read_text(encoding="utf-8")
|
|
75
|
+
for line in content.splitlines():
|
|
76
|
+
if line.startswith("pid="):
|
|
77
|
+
pid_str = line.split("=", 1)[1].strip()
|
|
78
|
+
if pid_str.isdigit():
|
|
79
|
+
pid = int(pid_str)
|
|
80
|
+
alive = self._is_pid_alive(pid)
|
|
81
|
+
if alive is None:
|
|
82
|
+
# 측정 불가 — 스테일 판정 금지. 잠금을 보존 (fail-closed).
|
|
83
|
+
logger.warning(
|
|
84
|
+
"[locking] Cannot determine if PID %d is alive (unmeasurable). "
|
|
85
|
+
"Preserving lock to avoid data corruption.",
|
|
86
|
+
pid,
|
|
87
|
+
)
|
|
88
|
+
return False
|
|
89
|
+
if not alive:
|
|
90
|
+
logger.warning(
|
|
91
|
+
"[termux-diffusion] Stale installation lock detected (PID: %d is dead). Auto-reclaiming lock.",
|
|
92
|
+
pid,
|
|
93
|
+
)
|
|
94
|
+
self.lock_file.unlink(missing_ok=True)
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.debug("Stale lock inspection note: %s", e)
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def acquire(self):
|
|
102
|
+
self.lock_file.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
self._check_and_clear_stale_lock()
|
|
104
|
+
start = time.time()
|
|
105
|
+
while time.time() - start < self.timeout_sec:
|
|
106
|
+
try:
|
|
107
|
+
fd = os.open(str(self.lock_file), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
108
|
+
os.write(fd, f"pid={os.getpid()}\nstarted_at={time.time()}\n".encode("utf-8"))
|
|
109
|
+
os.close(fd)
|
|
110
|
+
self._acquired = True
|
|
111
|
+
return
|
|
112
|
+
except FileExistsError:
|
|
113
|
+
if self._check_and_clear_stale_lock():
|
|
114
|
+
continue
|
|
115
|
+
time.sleep(0.2)
|
|
116
|
+
raise InstallLockError(
|
|
117
|
+
f"E_INSTALL_LOCKED: Installation process locked by another running process ({self.lock_file})."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def release(self):
|
|
121
|
+
if self._acquired and self.lock_file.exists():
|
|
122
|
+
try:
|
|
123
|
+
self.lock_file.unlink(missing_ok=True)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.warning("Failed removing install.lock: %s", e)
|
|
126
|
+
self._acquired = False
|
|
127
|
+
|
|
128
|
+
def __enter__(self):
|
|
129
|
+
self.acquire()
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
133
|
+
self.release()
|
|
134
|
+
|