gitinstall 1.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.
- gitinstall/__init__.py +61 -0
- gitinstall/_sdk.py +541 -0
- gitinstall/academic.py +831 -0
- gitinstall/admin.html +327 -0
- gitinstall/auto_update.py +384 -0
- gitinstall/autopilot.py +349 -0
- gitinstall/badge.py +476 -0
- gitinstall/checkpoint.py +330 -0
- gitinstall/cicd.py +499 -0
- gitinstall/clawhub.html +718 -0
- gitinstall/config_schema.py +353 -0
- gitinstall/db.py +984 -0
- gitinstall/db_backend.py +445 -0
- gitinstall/dep_chain.py +337 -0
- gitinstall/dependency_audit.py +1153 -0
- gitinstall/detector.py +542 -0
- gitinstall/doctor.py +493 -0
- gitinstall/education.py +869 -0
- gitinstall/enterprise.py +802 -0
- gitinstall/error_fixer.py +953 -0
- gitinstall/event_bus.py +251 -0
- gitinstall/executor.py +577 -0
- gitinstall/feature_flags.py +138 -0
- gitinstall/fetcher.py +921 -0
- gitinstall/huggingface.py +922 -0
- gitinstall/hw_detect.py +988 -0
- gitinstall/i18n.py +664 -0
- gitinstall/installer_registry.py +362 -0
- gitinstall/knowledge_base.py +379 -0
- gitinstall/license_check.py +605 -0
- gitinstall/llm.py +569 -0
- gitinstall/log.py +236 -0
- gitinstall/main.py +1408 -0
- gitinstall/mcp_agent.py +841 -0
- gitinstall/mcp_server.py +386 -0
- gitinstall/monorepo.py +810 -0
- gitinstall/multi_source.py +425 -0
- gitinstall/onboard.py +276 -0
- gitinstall/planner.py +222 -0
- gitinstall/planner_helpers.py +323 -0
- gitinstall/planner_known_projects.py +1010 -0
- gitinstall/planner_templates.py +996 -0
- gitinstall/remote_gpu.py +633 -0
- gitinstall/resilience.py +608 -0
- gitinstall/run_tests.py +572 -0
- gitinstall/skills.py +476 -0
- gitinstall/tool_schemas.py +324 -0
- gitinstall/trending.py +279 -0
- gitinstall/uninstaller.py +415 -0
- gitinstall/validate_top100.py +607 -0
- gitinstall/watchdog.py +180 -0
- gitinstall/web.py +1277 -0
- gitinstall/web_ui.html +2277 -0
- gitinstall-1.1.0.dist-info/METADATA +275 -0
- gitinstall-1.1.0.dist-info/RECORD +59 -0
- gitinstall-1.1.0.dist-info/WHEEL +5 -0
- gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
- gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
- gitinstall-1.1.0.dist-info/top_level.txt +1 -0
gitinstall/resilience.py
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
"""
|
|
2
|
+
resilience.py — 安装韧性层(系统性解决任意 GitHub 项目安装问题)
|
|
3
|
+
================================================================
|
|
4
|
+
|
|
5
|
+
核心思想:
|
|
6
|
+
不需要测试 GitHub 上每一个项目。只需要让安装策略足够聪明——
|
|
7
|
+
像浏览器不需要测试每个网页,只要 HTML 解析器足够健壮。
|
|
8
|
+
|
|
9
|
+
三大机制:
|
|
10
|
+
1. 预检层 (Preflight) — 执行前检测缺失工具,提前安装
|
|
11
|
+
2. 多策略回退 (Fallback) — Plan A 失败 → 自动 Plan B → Plan C
|
|
12
|
+
3. Brew 探测 (BrewProbe) — 自动探测 brew/apt 是否有现成包
|
|
13
|
+
|
|
14
|
+
策略优先级(可靠性从高到低):
|
|
15
|
+
Tier 1 — 包管理器安装 (brew install X) → 99% 成功率
|
|
16
|
+
Tier 2 — 语言包管理器 (cargo/pip/go install) → 90% 成功率
|
|
17
|
+
Tier 3 — 源码编译 (git clone + build) → 70% 成功率
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import platform
|
|
24
|
+
import re
|
|
25
|
+
import shutil
|
|
26
|
+
import subprocess
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ─────────────────────────────────────────────
|
|
32
|
+
# Brew / Apt 包探测
|
|
33
|
+
# ─────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
def _run_quiet(cmd: list[str], timeout: int = 10) -> tuple[int, str]:
|
|
36
|
+
"""安全执行命令,返回 (exit_code, stdout)"""
|
|
37
|
+
try:
|
|
38
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
39
|
+
return r.returncode, r.stdout.strip()
|
|
40
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
41
|
+
return -1, ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def brew_has_package(name: str) -> bool:
|
|
45
|
+
"""检查 brew 中是否有该包(不下载,只查询)"""
|
|
46
|
+
code, _ = _run_quiet(["brew", "info", "--json=v2", name])
|
|
47
|
+
return code == 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def apt_has_package(name: str) -> bool:
|
|
51
|
+
"""检查 apt 中是否有该包"""
|
|
52
|
+
code, _ = _run_quiet(["apt-cache", "show", name])
|
|
53
|
+
return code == 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_macos() -> bool:
|
|
57
|
+
return platform.system() == "Darwin"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_linux() -> bool:
|
|
61
|
+
return platform.system() == "Linux"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _has_command(cmd: str) -> bool:
|
|
65
|
+
"""检查系统命令是否可用"""
|
|
66
|
+
return shutil.which(cmd) is not None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _dep_names(dependency_files: dict[str, str] | None) -> set[str]:
|
|
70
|
+
if not dependency_files:
|
|
71
|
+
return set()
|
|
72
|
+
return {Path(path).name for path in dependency_files}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _dep_content(dependency_files: dict[str, str] | None, name: str) -> str:
|
|
76
|
+
if not dependency_files:
|
|
77
|
+
return ""
|
|
78
|
+
for path, content in dependency_files.items():
|
|
79
|
+
if Path(path).name == name and "/" not in path:
|
|
80
|
+
return content
|
|
81
|
+
for path, content in dependency_files.items():
|
|
82
|
+
if Path(path).name == name:
|
|
83
|
+
return content
|
|
84
|
+
return ""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _is_maturin_project(project_types: set[str], dependency_files: dict[str, str] | None) -> bool:
|
|
88
|
+
if not ({"python", "rust"} <= project_types):
|
|
89
|
+
return False
|
|
90
|
+
pyproject = _dep_content(dependency_files, "pyproject.toml")
|
|
91
|
+
return bool(pyproject and re.search(r"maturin|setuptools-rust", pyproject, re.IGNORECASE))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _zig_uses_legacy_build_api(dependency_files: dict[str, str] | None) -> bool:
|
|
95
|
+
build_zig = _dep_content(dependency_files, "build.zig")
|
|
96
|
+
if not build_zig:
|
|
97
|
+
return False
|
|
98
|
+
build_zon = _dep_content(dependency_files, "build.zig.zon")
|
|
99
|
+
match = re.search(r'\.minimum_zig_version\s*=\s*"([^"]+)"', build_zon)
|
|
100
|
+
if match:
|
|
101
|
+
version = tuple(int(part) for part in re.findall(r"\d+", match.group(1))[:3])
|
|
102
|
+
if version >= (0, 15, 0):
|
|
103
|
+
return False
|
|
104
|
+
legacy_markers = [
|
|
105
|
+
".root_source_file",
|
|
106
|
+
".source_file",
|
|
107
|
+
"Build.ExecutableOptions",
|
|
108
|
+
]
|
|
109
|
+
return any(marker in build_zig for marker in legacy_markers)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _zig_minimum_version(dependency_files: dict[str, str] | None) -> str:
|
|
113
|
+
build_zon = _dep_content(dependency_files, "build.zig.zon")
|
|
114
|
+
match = re.search(r'\.minimum_zig_version\s*=\s*"([^"]+)"', build_zon)
|
|
115
|
+
if match:
|
|
116
|
+
return match.group(1)
|
|
117
|
+
return _dep_content(dependency_files, ".zig-version").strip()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _version_tuple(version: str) -> tuple[int, ...]:
|
|
121
|
+
numbers = [int(part) for part in re.findall(r"\d+", version)]
|
|
122
|
+
return tuple(numbers[:3]) if numbers else ()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _zig_fallback_version(dependency_files: dict[str, str] | None) -> str:
|
|
126
|
+
min_version = _zig_minimum_version(dependency_files)
|
|
127
|
+
if min_version and _version_tuple(min_version) < (0, 15, 0):
|
|
128
|
+
return min_version
|
|
129
|
+
return "0.14.0"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _zig_download_info(env: dict, version: str) -> tuple[str, str] | None:
|
|
133
|
+
os_info = env.get("os", {})
|
|
134
|
+
os_type = os_info.get("type", "")
|
|
135
|
+
arch = os_info.get("arch", "")
|
|
136
|
+
|
|
137
|
+
if os_type == "macos" and arch == "arm64":
|
|
138
|
+
artifact = f"zig-macos-aarch64-{version}"
|
|
139
|
+
elif os_type == "macos" and arch in {"x86_64", "amd64"}:
|
|
140
|
+
artifact = f"zig-macos-x86_64-{version}"
|
|
141
|
+
elif os_type == "linux" and arch == "arm64":
|
|
142
|
+
artifact = f"zig-linux-aarch64-{version}"
|
|
143
|
+
elif os_type == "linux" and arch in {"x86_64", "amd64"}:
|
|
144
|
+
artifact = f"zig-linux-x86_64-{version}"
|
|
145
|
+
else:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
return artifact, f"https://ziglang.org/download/{version}/{artifact}.tar.xz"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ─────────────────────────────────────────────
|
|
152
|
+
# Repo 名称到包管理器名称的智能映射
|
|
153
|
+
# ─────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
# GitHub repo 名 → brew/apt 包名(很多时候就是 repo 名本身)
|
|
156
|
+
# 只列出 repo 名 ≠ brew 名 的映射
|
|
157
|
+
_BREW_NAME_MAP = {
|
|
158
|
+
"fd": "fd",
|
|
159
|
+
"bat": "bat",
|
|
160
|
+
"ripgrep": "ripgrep",
|
|
161
|
+
"exa": "exa",
|
|
162
|
+
"fzf": "fzf",
|
|
163
|
+
"jq": "jq",
|
|
164
|
+
"yq": "yq",
|
|
165
|
+
"gh": "gh",
|
|
166
|
+
"lazygit": "lazygit",
|
|
167
|
+
"delta": "git-delta",
|
|
168
|
+
"difftastic": "difftastic",
|
|
169
|
+
"hyperfine": "hyperfine",
|
|
170
|
+
"tokei": "tokei",
|
|
171
|
+
"dust": "dust",
|
|
172
|
+
"procs": "procs",
|
|
173
|
+
"bottom": "bottom",
|
|
174
|
+
"zoxide": "zoxide",
|
|
175
|
+
"starship": "starship",
|
|
176
|
+
"nushell": "nushell",
|
|
177
|
+
"helix": "helix",
|
|
178
|
+
"neovim": "neovim",
|
|
179
|
+
"tmux": "tmux",
|
|
180
|
+
"hugo": "hugo",
|
|
181
|
+
"caddy": "caddy",
|
|
182
|
+
"traefik": "traefik",
|
|
183
|
+
"k9s": "k9s",
|
|
184
|
+
"terraform": "terraform",
|
|
185
|
+
"httpie": "httpie",
|
|
186
|
+
"wget": "wget",
|
|
187
|
+
"curl": "curl",
|
|
188
|
+
"tree-sitter": "tree-sitter",
|
|
189
|
+
"cmake": "cmake",
|
|
190
|
+
"ninja": "ninja",
|
|
191
|
+
"meson": "meson",
|
|
192
|
+
"lsd": "lsd",
|
|
193
|
+
"gitui": "gitui",
|
|
194
|
+
"just": "just",
|
|
195
|
+
"watchexec": "watchexec",
|
|
196
|
+
"miniserve": "miniserve",
|
|
197
|
+
"dog": "dog",
|
|
198
|
+
"glow": "glow",
|
|
199
|
+
"broot": "broot",
|
|
200
|
+
"xh": "xh",
|
|
201
|
+
"choose": "choose-rust",
|
|
202
|
+
"sd": "sd",
|
|
203
|
+
"grex": "grex",
|
|
204
|
+
"pastel": "pastel",
|
|
205
|
+
"vivid": "vivid",
|
|
206
|
+
"tealdeer": "tealdeer",
|
|
207
|
+
"bandwhich": "bandwhich",
|
|
208
|
+
"navi": "navi",
|
|
209
|
+
"zellij": "zellij",
|
|
210
|
+
"atuin": "atuin",
|
|
211
|
+
"yazi": "yazi",
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_APT_NAME_MAP = {
|
|
215
|
+
"fd": "fd-find",
|
|
216
|
+
"bat": "bat",
|
|
217
|
+
"ripgrep": "ripgrep",
|
|
218
|
+
"fzf": "fzf",
|
|
219
|
+
"jq": "jq",
|
|
220
|
+
"yq": "yq",
|
|
221
|
+
"neovim": "neovim",
|
|
222
|
+
"tmux": "tmux",
|
|
223
|
+
"hugo": "hugo",
|
|
224
|
+
"cmake": "cmake",
|
|
225
|
+
"ninja": "ninja-build",
|
|
226
|
+
"meson": "meson",
|
|
227
|
+
"httpie": "httpie",
|
|
228
|
+
"tree-sitter": "tree-sitter-cli",
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_brew_name(repo_name: str) -> str:
|
|
233
|
+
"""GitHub repo 名转 brew 包名"""
|
|
234
|
+
lower = repo_name.lower()
|
|
235
|
+
return _BREW_NAME_MAP.get(lower, lower)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_apt_name(repo_name: str) -> str:
|
|
239
|
+
"""GitHub repo 名转 apt 包名"""
|
|
240
|
+
lower = repo_name.lower()
|
|
241
|
+
return _APT_NAME_MAP.get(lower, lower)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ─────────────────────────────────────────────
|
|
245
|
+
# 预检层 (Preflight)
|
|
246
|
+
# ─────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
# 命令 → 安装方式
|
|
249
|
+
_TOOL_INSTALL = {
|
|
250
|
+
"git": {"brew": "git", "apt": "git"},
|
|
251
|
+
"python3": {"brew": "python@3", "apt": "python3"},
|
|
252
|
+
"pip": {"brew": "python@3", "apt": "python3-pip"},
|
|
253
|
+
"pip3": {"brew": "python@3", "apt": "python3-pip"},
|
|
254
|
+
"node": {"brew": "node", "apt": "nodejs"},
|
|
255
|
+
"npm": {"brew": "node", "apt": "npm"},
|
|
256
|
+
"npx": {"brew": "node", "apt": "npm"},
|
|
257
|
+
"yarn": {"brew": "yarn", "apt": "yarn"},
|
|
258
|
+
"pnpm": {"brew": "pnpm", "apt": "pnpm"},
|
|
259
|
+
"cargo": {"brew": "rust", "apt": "cargo"},
|
|
260
|
+
"rustc": {"brew": "rust", "apt": "rustc"},
|
|
261
|
+
"go": {"brew": "go", "apt": "golang"},
|
|
262
|
+
"java": {"brew": "openjdk", "apt": "default-jdk"},
|
|
263
|
+
"javac": {"brew": "openjdk", "apt": "default-jdk"},
|
|
264
|
+
"mvn": {"brew": "maven", "apt": "maven"},
|
|
265
|
+
"gradle": {"brew": "gradle", "apt": "gradle"},
|
|
266
|
+
"cmake": {"brew": "cmake", "apt": "cmake"},
|
|
267
|
+
"make": {"brew": "make", "apt": "build-essential"},
|
|
268
|
+
"gcc": {"brew": "gcc", "apt": "build-essential"},
|
|
269
|
+
"g++": {"brew": "gcc", "apt": "build-essential"},
|
|
270
|
+
"ruby": {"brew": "ruby", "apt": "ruby-full"},
|
|
271
|
+
"gem": {"brew": "ruby", "apt": "ruby-full"},
|
|
272
|
+
"bundle": {"brew": "ruby", "apt": "ruby-bundler"},
|
|
273
|
+
"php": {"brew": "php", "apt": "php"},
|
|
274
|
+
"composer": {"brew": "composer", "apt": "composer"},
|
|
275
|
+
"mix": {"brew": "elixir", "apt": "elixir"},
|
|
276
|
+
"swift": {"brew": "swift", "apt": ""},
|
|
277
|
+
"stack": {"brew": "haskell-stack", "apt": "haskell-stack"},
|
|
278
|
+
"sbt": {"brew": "sbt", "apt": ""},
|
|
279
|
+
"zig": {"brew": "zig", "apt": ""},
|
|
280
|
+
"lein": {"brew": "leiningen", "apt": "leiningen"},
|
|
281
|
+
"docker": {"brew": "docker", "apt": "docker.io"},
|
|
282
|
+
"uv": {"brew": "uv", "apt": ""},
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@dataclass
|
|
287
|
+
class PreflightResult:
|
|
288
|
+
"""预检结果"""
|
|
289
|
+
missing_tools: list[str] = field(default_factory=list)
|
|
290
|
+
install_commands: list[dict] = field(default_factory=list)
|
|
291
|
+
all_ready: bool = True
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def preflight_check(plan_steps: list[dict]) -> PreflightResult:
|
|
295
|
+
"""
|
|
296
|
+
扫描计划中所有命令,检测需要的工具是否已安装。
|
|
297
|
+
|
|
298
|
+
返回缺失工具列表和对应的安装命令。
|
|
299
|
+
这在执行前调用,比执行中报错再修复更可靠。
|
|
300
|
+
"""
|
|
301
|
+
result = PreflightResult()
|
|
302
|
+
needed_tools = set()
|
|
303
|
+
|
|
304
|
+
for step in plan_steps:
|
|
305
|
+
cmd = step.get("command", "")
|
|
306
|
+
if not cmd:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
# 提取命令中可能用到的工具名
|
|
310
|
+
# 支持:tool args, cd dir && tool args, source activate && tool args
|
|
311
|
+
for segment in re.split(r'\s*&&\s*|\s*\|\|\s*|\s*;\s*', cmd):
|
|
312
|
+
segment = segment.strip()
|
|
313
|
+
if not segment:
|
|
314
|
+
continue
|
|
315
|
+
# 跳过 cd, source, export 等 shell 内置
|
|
316
|
+
first_word = segment.split()[0] if segment.split() else ""
|
|
317
|
+
if first_word in ("cd", "source", "export", "echo", "mkdir", "test",
|
|
318
|
+
"cat", "ls", "pwd", "true", "false", "set", "if",
|
|
319
|
+
"then", "else", "fi", "for", "while", "do", "done"):
|
|
320
|
+
continue
|
|
321
|
+
needed_tools.add(first_word)
|
|
322
|
+
|
|
323
|
+
# 检查哪些工具缺失
|
|
324
|
+
for tool in sorted(needed_tools):
|
|
325
|
+
if _has_command(tool):
|
|
326
|
+
continue
|
|
327
|
+
if tool not in _TOOL_INSTALL:
|
|
328
|
+
continue # 不认识的工具跳过
|
|
329
|
+
|
|
330
|
+
result.missing_tools.append(tool)
|
|
331
|
+
info = _TOOL_INSTALL[tool]
|
|
332
|
+
|
|
333
|
+
if _is_macos() and info.get("brew"):
|
|
334
|
+
result.install_commands.append({
|
|
335
|
+
"command": f"brew install {info['brew']}",
|
|
336
|
+
"description": f"安装 {tool}(预检发现缺失)",
|
|
337
|
+
})
|
|
338
|
+
elif _is_linux() and info.get("apt"):
|
|
339
|
+
result.install_commands.append({
|
|
340
|
+
"command": f"sudo apt-get install -y {info['apt']}",
|
|
341
|
+
"description": f"安装 {tool}(预检发现缺失)",
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
result.all_ready = len(result.missing_tools) == 0
|
|
345
|
+
return result
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ─────────────────────────────────────────────
|
|
349
|
+
# 多策略回退引擎
|
|
350
|
+
# ─────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
@dataclass
|
|
353
|
+
class FallbackPlan:
|
|
354
|
+
"""一个回退方案"""
|
|
355
|
+
tier: int # 1=包管理器, 2=语言包管理器, 3=源码编译
|
|
356
|
+
strategy: str # 策略名称
|
|
357
|
+
steps: list[dict] # 执行步骤
|
|
358
|
+
confidence: str # high/medium/low
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def generate_fallback_plans(
|
|
362
|
+
owner: str,
|
|
363
|
+
repo: str,
|
|
364
|
+
project_types: list[str],
|
|
365
|
+
env: dict,
|
|
366
|
+
dependency_files: dict[str, str] | None = None,
|
|
367
|
+
) -> list[FallbackPlan]:
|
|
368
|
+
"""
|
|
369
|
+
为一个项目生成多个安装策略(按可靠性排序)。
|
|
370
|
+
|
|
371
|
+
主执行器按顺序尝试,Plan A 失败后自动切换到 Plan B。
|
|
372
|
+
这就是系统不需要测试每个项目的关键——
|
|
373
|
+
即使 Plan A 失败了,Plan B/C 通常能兜住。
|
|
374
|
+
"""
|
|
375
|
+
plans = []
|
|
376
|
+
types = set(project_types)
|
|
377
|
+
repo_lower = repo.lower()
|
|
378
|
+
dep_names = _dep_names(dependency_files)
|
|
379
|
+
is_maturin = _is_maturin_project(types, dependency_files)
|
|
380
|
+
os_type = "macos" if _is_macos() else ("linux" if _is_linux() else "windows")
|
|
381
|
+
clone_url = f"https://github.com/{owner}/{repo}.git"
|
|
382
|
+
|
|
383
|
+
# ── Tier 1: 包管理器安装(最可靠,~99% 成功率)──
|
|
384
|
+
# brew (macOS) / apt (Linux) / winget (Windows)
|
|
385
|
+
if _is_macos() and _has_command("brew"):
|
|
386
|
+
brew_name = get_brew_name(repo_lower)
|
|
387
|
+
if brew_has_package(brew_name):
|
|
388
|
+
plans.append(FallbackPlan(
|
|
389
|
+
tier=1,
|
|
390
|
+
strategy="brew_install",
|
|
391
|
+
steps=[{
|
|
392
|
+
"command": f"brew install {brew_name}",
|
|
393
|
+
"description": f"用 Homebrew 安装 {repo}(最可靠方式)",
|
|
394
|
+
}],
|
|
395
|
+
confidence="high",
|
|
396
|
+
))
|
|
397
|
+
elif _is_linux() and _has_command("apt-get"):
|
|
398
|
+
apt_name = get_apt_name(repo_lower)
|
|
399
|
+
if apt_has_package(apt_name):
|
|
400
|
+
plans.append(FallbackPlan(
|
|
401
|
+
tier=1,
|
|
402
|
+
strategy="apt_install",
|
|
403
|
+
steps=[{
|
|
404
|
+
"command": f"sudo apt-get install -y {apt_name}",
|
|
405
|
+
"description": f"用 apt 安装 {repo}(最可靠方式)",
|
|
406
|
+
}],
|
|
407
|
+
confidence="high",
|
|
408
|
+
))
|
|
409
|
+
|
|
410
|
+
# ── Tier 2: 语言包管理器安装(无需编译,通常很快)──
|
|
411
|
+
if is_maturin and _has_command("pip3"):
|
|
412
|
+
plans.append(FallbackPlan(
|
|
413
|
+
tier=2,
|
|
414
|
+
strategy="python_editable_install",
|
|
415
|
+
steps=[
|
|
416
|
+
{"command": f"git clone --depth 1 https://github.com/{owner}/{repo}.git", "description": "克隆代码"},
|
|
417
|
+
{"command": f"cd {repo}", "description": "进入目录"},
|
|
418
|
+
{"command": "python3 -m venv venv && source venv/bin/activate && pip install -e .", "description": "按 Python 包安装(含 Rust 扩展)"},
|
|
419
|
+
],
|
|
420
|
+
confidence="medium",
|
|
421
|
+
))
|
|
422
|
+
|
|
423
|
+
if types & {"rust"} and _has_command("cargo") and not is_maturin:
|
|
424
|
+
plans.append(FallbackPlan(
|
|
425
|
+
tier=2,
|
|
426
|
+
strategy="cargo_install",
|
|
427
|
+
steps=[{
|
|
428
|
+
"command": f"cargo install --git https://github.com/{owner}/{repo}",
|
|
429
|
+
"description": f"用 Cargo 编译安装 {repo}",
|
|
430
|
+
}],
|
|
431
|
+
confidence="medium",
|
|
432
|
+
))
|
|
433
|
+
|
|
434
|
+
if types & {"go"} and _has_command("go"):
|
|
435
|
+
plans.append(FallbackPlan(
|
|
436
|
+
tier=2,
|
|
437
|
+
strategy="go_build",
|
|
438
|
+
steps=[
|
|
439
|
+
{"command": f"git clone --depth 1 https://github.com/{owner}/{repo}.git", "description": "克隆代码"},
|
|
440
|
+
{"command": f"cd {repo}", "description": "进入目录"},
|
|
441
|
+
{"command": (
|
|
442
|
+
"test -f go.mod && go build ./... || "
|
|
443
|
+
"find . -name go.mod -maxdepth 3 -not -path '*/vendor/*' "
|
|
444
|
+
"-exec dirname {} \\; | head -5 | "
|
|
445
|
+
"while read d; do echo \"Building $d\"; (cd \"$d\" && go build ./...); done"
|
|
446
|
+
), "description": f"编译 {repo}(自动查找 Go 模块)"},
|
|
447
|
+
],
|
|
448
|
+
confidence="medium",
|
|
449
|
+
))
|
|
450
|
+
|
|
451
|
+
if types & {"python"} and _has_command("pip3"):
|
|
452
|
+
plans.append(FallbackPlan(
|
|
453
|
+
tier=2,
|
|
454
|
+
strategy="pip_install",
|
|
455
|
+
steps=[
|
|
456
|
+
{"command": "python3 -m venv venv", "description": "创建虚拟环境"},
|
|
457
|
+
{"command": "source venv/bin/activate", "description": "激活虚拟环境"},
|
|
458
|
+
{"command": f"pip install {repo_lower}", "description": f"用 pip 安装 {repo} 的已发布包"},
|
|
459
|
+
],
|
|
460
|
+
confidence="medium",
|
|
461
|
+
))
|
|
462
|
+
|
|
463
|
+
if types & {"node"} and "package.json" in dep_names and _has_command("npm"):
|
|
464
|
+
plans.append(FallbackPlan(
|
|
465
|
+
tier=2,
|
|
466
|
+
strategy="npm_install",
|
|
467
|
+
steps=[
|
|
468
|
+
{"command": f"git clone --depth 1 https://github.com/{owner}/{repo}.git", "description": "克隆代码"},
|
|
469
|
+
{"command": f"cd {repo}", "description": "进入目录"},
|
|
470
|
+
{"command": "npm install", "description": "安装依赖"},
|
|
471
|
+
],
|
|
472
|
+
confidence="medium",
|
|
473
|
+
))
|
|
474
|
+
|
|
475
|
+
if types & {"zig"} and _zig_uses_legacy_build_api(dependency_files):
|
|
476
|
+
legacy_version = _zig_fallback_version(dependency_files)
|
|
477
|
+
download_info = _zig_download_info(env, legacy_version)
|
|
478
|
+
if download_info:
|
|
479
|
+
artifact, url = download_info
|
|
480
|
+
zig_bin = f'$PWD/.gitinstall-zig/{artifact}/zig'
|
|
481
|
+
if os_type == "macos":
|
|
482
|
+
build_cmd = (
|
|
483
|
+
f'ZIG_BIN="{zig_bin}"; '
|
|
484
|
+
'if [ -d /Applications/Xcode.app/Contents/Developer ]; then '
|
|
485
|
+
'export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer; '
|
|
486
|
+
'fi; export SDKROOT="${SDKROOT:-$(xcrun --sdk macosx --show-sdk-path 2>/dev/null)}"; "$ZIG_BIN" build'
|
|
487
|
+
)
|
|
488
|
+
else:
|
|
489
|
+
build_cmd = f'ZIG_BIN="{zig_bin}"; "$ZIG_BIN" build'
|
|
490
|
+
plans.append(FallbackPlan(
|
|
491
|
+
tier=2,
|
|
492
|
+
strategy="zig_legacy_0_14_0_build",
|
|
493
|
+
steps=[
|
|
494
|
+
{"command": f"git clone --depth 1 --recurse-submodules {clone_url}", "description": "递归克隆代码与 Zig 子模块依赖"},
|
|
495
|
+
{"command": f"cd {repo}", "description": "进入目录"},
|
|
496
|
+
{"command": "git submodule update --init --recursive", "description": "补齐子模块依赖(兼容目录已存在场景)"},
|
|
497
|
+
{"command": "mkdir -p .gitinstall-zig", "description": "创建 Zig 兼容工具链目录"},
|
|
498
|
+
{"command": f"curl -L {url} -o .gitinstall-zig/zig.tar.xz && tar -xf .gitinstall-zig/zig.tar.xz -C .gitinstall-zig", "description": "下载 Zig 0.14.0 兼容工具链"},
|
|
499
|
+
{"command": build_cmd, "description": "使用 Zig 0.14.0 编译旧 build API 项目"},
|
|
500
|
+
],
|
|
501
|
+
confidence="medium",
|
|
502
|
+
))
|
|
503
|
+
|
|
504
|
+
if types & {"ruby"} and _has_command("gem"):
|
|
505
|
+
plans.append(FallbackPlan(
|
|
506
|
+
tier=2,
|
|
507
|
+
strategy="gem_install",
|
|
508
|
+
steps=[{
|
|
509
|
+
"command": f"gem install {repo_lower}",
|
|
510
|
+
"description": f"用 RubyGems 安装 {repo}",
|
|
511
|
+
}],
|
|
512
|
+
confidence="medium",
|
|
513
|
+
))
|
|
514
|
+
|
|
515
|
+
# ── Tier 3: 源码编译(最不可靠,但覆盖面最广)──
|
|
516
|
+
|
|
517
|
+
if types & {"rust"} and _has_command("cargo"):
|
|
518
|
+
plans.append(FallbackPlan(
|
|
519
|
+
tier=3,
|
|
520
|
+
strategy="source_cargo_build",
|
|
521
|
+
steps=[
|
|
522
|
+
{"command": f"git clone --depth 1 {clone_url}", "description": "克隆代码"},
|
|
523
|
+
{"command": f"cd {repo}", "description": "进入目录"},
|
|
524
|
+
{"command": "cargo build --release", "description": "编译(Release 模式)"},
|
|
525
|
+
],
|
|
526
|
+
confidence="low",
|
|
527
|
+
))
|
|
528
|
+
|
|
529
|
+
if types & {"cmake", "cpp", "c"} and _has_command("cmake"):
|
|
530
|
+
plans.append(FallbackPlan(
|
|
531
|
+
tier=3,
|
|
532
|
+
strategy="source_cmake_build",
|
|
533
|
+
steps=[
|
|
534
|
+
{"command": f"git clone --depth 1 {clone_url}", "description": "克隆代码"},
|
|
535
|
+
{"command": f"cd {repo}", "description": "进入目录"},
|
|
536
|
+
{"command": "mkdir -p build && cd build", "description": "创建构建目录"},
|
|
537
|
+
{"command": "cmake .. && make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu)", "description": "编译"},
|
|
538
|
+
],
|
|
539
|
+
confidence="low",
|
|
540
|
+
))
|
|
541
|
+
|
|
542
|
+
if types & {"make", "autotools", "c"} and _has_command("make"):
|
|
543
|
+
plans.append(FallbackPlan(
|
|
544
|
+
tier=3,
|
|
545
|
+
strategy="source_make_build",
|
|
546
|
+
steps=[
|
|
547
|
+
{"command": f"git clone --depth 1 {clone_url}", "description": "克隆代码"},
|
|
548
|
+
{"command": f"cd {repo}", "description": "进入目录"},
|
|
549
|
+
{"command": "./configure && make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu)", "description": "编译"},
|
|
550
|
+
],
|
|
551
|
+
confidence="low",
|
|
552
|
+
))
|
|
553
|
+
|
|
554
|
+
return plans
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
# ─────────────────────────────────────────────
|
|
558
|
+
# 统一对外接口
|
|
559
|
+
# ─────────────────────────────────────────────
|
|
560
|
+
|
|
561
|
+
def enhance_plan_with_preflight(plan: dict) -> dict:
|
|
562
|
+
"""
|
|
563
|
+
增强现有安装计划:在所有步骤前插入预检安装步骤。
|
|
564
|
+
"""
|
|
565
|
+
steps = plan.get("steps", [])
|
|
566
|
+
pf = preflight_check(steps)
|
|
567
|
+
|
|
568
|
+
if pf.install_commands:
|
|
569
|
+
# 在所有步骤前插入预检安装步骤
|
|
570
|
+
plan["steps"] = pf.install_commands + steps
|
|
571
|
+
plan["_preflight"] = {
|
|
572
|
+
"missing_tools": pf.missing_tools,
|
|
573
|
+
"install_count": len(pf.install_commands),
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return plan
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def get_fallback_plan_for_failure(
|
|
580
|
+
owner: str,
|
|
581
|
+
repo: str,
|
|
582
|
+
project_types: list[str],
|
|
583
|
+
env: dict,
|
|
584
|
+
failed_strategy: str,
|
|
585
|
+
dependency_files: dict[str, str] | None = None,
|
|
586
|
+
) -> FallbackPlan | None:
|
|
587
|
+
"""
|
|
588
|
+
当前策略失败后,获取下一个可用的回退策略。
|
|
589
|
+
|
|
590
|
+
参数 failed_strategy 是已经失败的策略名称,
|
|
591
|
+
返回列表中下一个不同的策略(跳过同一 tier 中已失败的)。
|
|
592
|
+
"""
|
|
593
|
+
all_plans = generate_fallback_plans(owner, repo, project_types, env, dependency_files=dependency_files)
|
|
594
|
+
|
|
595
|
+
# 找到当前失败策略的 tier
|
|
596
|
+
failed_tier = 0
|
|
597
|
+
for p in all_plans:
|
|
598
|
+
if p.strategy == failed_strategy:
|
|
599
|
+
failed_tier = p.tier
|
|
600
|
+
break
|
|
601
|
+
|
|
602
|
+
# 返回比失败策略 tier 更高(数字更大=更低可靠性)或同 tier 但不同策略的第一个
|
|
603
|
+
for p in all_plans:
|
|
604
|
+
if p.strategy != failed_strategy:
|
|
605
|
+
if p.tier >= failed_tier:
|
|
606
|
+
return p
|
|
607
|
+
|
|
608
|
+
return None
|