fc-data 0.2.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.
- datasmith/__init__.py +330 -0
- datasmith/__init__.pyi +194 -0
- datasmith/agents/__init__.py +31 -0
- datasmith/agents/classifiers.py +272 -0
- datasmith/agents/codex.py +25 -0
- datasmith/agents/config.py +108 -0
- datasmith/agents/extractors.py +197 -0
- datasmith/agents/installed/README.md +52 -0
- datasmith/agents/installed/__init__.py +22 -0
- datasmith/agents/installed/base.py +240 -0
- datasmith/agents/installed/claude.py +134 -0
- datasmith/agents/installed/codex.py +91 -0
- datasmith/agents/installed/gemini.py +118 -0
- datasmith/agents/installed/none.py +27 -0
- datasmith/agents/sandbox.py +547 -0
- datasmith/agents/synthesizer.py +439 -0
- datasmith/agents/templates/AGENTS.md.j2 +150 -0
- datasmith/agents/templates/sandbox_verify.py +428 -0
- datasmith/docker/__init__.py +31 -0
- datasmith/docker/context.py +112 -0
- datasmith/docker/images.py +158 -0
- datasmith/docker/publish.py +56 -0
- datasmith/docker/templates/Dockerfile.base +26 -0
- datasmith/docker/templates/Dockerfile.pr +42 -0
- datasmith/docker/templates/Dockerfile.repo +11 -0
- datasmith/docker/templates/docker_build_base.sh +780 -0
- datasmith/docker/templates/docker_build_env.sh +309 -0
- datasmith/docker/templates/docker_build_final.sh +106 -0
- datasmith/docker/templates/docker_build_pkg.sh +99 -0
- datasmith/docker/templates/docker_build_run.sh +124 -0
- datasmith/docker/templates/entrypoint.sh +62 -0
- datasmith/docker/templates/parser.py +1405 -0
- datasmith/docker/templates/profile.sh +199 -0
- datasmith/docker/templates/pytest_runner.py +692 -0
- datasmith/docker/templates/run-tests.sh +197 -0
- datasmith/docker/verifiers.py +131 -0
- datasmith/filters.py +154 -0
- datasmith/github/__init__.py +22 -0
- datasmith/github/client.py +333 -0
- datasmith/github/hooks.py +50 -0
- datasmith/github/links.py +110 -0
- datasmith/github/models.py +206 -0
- datasmith/github/render.py +173 -0
- datasmith/github/search.py +66 -0
- datasmith/github/templates/comment.md.j2 +5 -0
- datasmith/github/templates/final.md.j2 +66 -0
- datasmith/github/templates/issues.md.j2 +21 -0
- datasmith/github/templates/repo.md.j2 +1 -0
- datasmith/preflight.py +162 -0
- datasmith/publish/__init__.py +13 -0
- datasmith/publish/huggingface.py +104 -0
- datasmith/publish/pipeline.py +60 -0
- datasmith/publish/records.py +91 -0
- datasmith/py.typed +1 -0
- datasmith/resolution/__init__.py +14 -0
- datasmith/resolution/blocklist.py +145 -0
- datasmith/resolution/cache.py +120 -0
- datasmith/resolution/constants.py +277 -0
- datasmith/resolution/dependency_resolver.py +174 -0
- datasmith/resolution/git_utils.py +378 -0
- datasmith/resolution/import_analyzer.py +66 -0
- datasmith/resolution/metadata_parser.py +412 -0
- datasmith/resolution/models.py +41 -0
- datasmith/resolution/orchestrator.py +522 -0
- datasmith/resolution/package_filters.py +312 -0
- datasmith/resolution/python_manager.py +110 -0
- datasmith/runners/__init__.py +15 -0
- datasmith/runners/base.py +112 -0
- datasmith/runners/classify_prs.py +48 -0
- datasmith/runners/render_problems.py +113 -0
- datasmith/runners/resolve_packages.py +66 -0
- datasmith/runners/scrape_commits.py +166 -0
- datasmith/runners/scrape_repos.py +44 -0
- datasmith/runners/synthesize_images.py +310 -0
- datasmith/update/__init__.py +5 -0
- datasmith/update/cli.py +169 -0
- datasmith/update/offline.py +173 -0
- datasmith/update/pipeline.py +497 -0
- datasmith/utils/__init__.py +18 -0
- datasmith/utils/core.py +67 -0
- datasmith/utils/db.py +156 -0
- datasmith/utils/tokens.py +65 -0
- fc_data-0.2.0.dist-info/METADATA +441 -0
- fc_data-0.2.0.dist-info/RECORD +87 -0
- fc_data-0.2.0.dist-info/WHEEL +4 -0
- fc_data-0.2.0.dist-info/entry_points.txt +2 -0
- fc_data-0.2.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""Main orchestration for commit analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
import shutil
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import json5
|
|
14
|
+
|
|
15
|
+
from datasmith.utils import get_logger
|
|
16
|
+
|
|
17
|
+
from .cache import cache_completion
|
|
18
|
+
from .constants import ALLOWLIST_COMMON_PYPI, CACHE_LOCATION
|
|
19
|
+
from .dependency_resolver import (
|
|
20
|
+
rfc3339,
|
|
21
|
+
uv_build_and_read_metadata,
|
|
22
|
+
uv_compile,
|
|
23
|
+
uv_compile_from_pyproject,
|
|
24
|
+
uv_dry_run_install,
|
|
25
|
+
uv_install_real,
|
|
26
|
+
)
|
|
27
|
+
from .git_utils import asv_finder, prepare_repo_checkout
|
|
28
|
+
from .import_analyzer import infer_runtime_from_imports
|
|
29
|
+
from .metadata_parser import analyze_candidate_meta, discover_candidates, select_primary_candidate
|
|
30
|
+
from .models import ASVCfgAggregate
|
|
31
|
+
from .package_filters import (
|
|
32
|
+
clean_pinned,
|
|
33
|
+
extract_pkg_name,
|
|
34
|
+
extract_requested_extras,
|
|
35
|
+
filter_requirements_for_pypi,
|
|
36
|
+
normalize_requirement,
|
|
37
|
+
resolve_requirements_file,
|
|
38
|
+
split_shell_command,
|
|
39
|
+
)
|
|
40
|
+
from .python_manager import ensure_python_version_available, filter_python_versions_by_commit_date, run_uv
|
|
41
|
+
|
|
42
|
+
logger = get_logger("resolution.orchestrator")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@cache_completion(CACHE_LOCATION, table_name="commit_analysis")
|
|
46
|
+
def analyze_commit(sha: str, repo_name: str, bypass_cache: bool = False) -> dict[str, Any] | None: # noqa: C901
|
|
47
|
+
"""Analyze a commit to extract build/runtime information for benchmarking.
|
|
48
|
+
|
|
49
|
+
Returns a dictionary with resolution results, or None if analysis failed.
|
|
50
|
+
"""
|
|
51
|
+
commit_info: dict[str, Any] | None = None
|
|
52
|
+
|
|
53
|
+
python_version: str | None = None
|
|
54
|
+
resolved_dependencies: list[str] = []
|
|
55
|
+
resolution_strategy: str | None = None
|
|
56
|
+
can_install: bool = False
|
|
57
|
+
dry_run_log: str = ""
|
|
58
|
+
excluded_missing_on_pypi: dict[str, str] = {}
|
|
59
|
+
excluded_exists_incompatible: dict[str, str] = {}
|
|
60
|
+
excluded_other: dict[str, str] = {}
|
|
61
|
+
|
|
62
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
63
|
+
tmp_path = Path(tmpdir)
|
|
64
|
+
repo, tmpfile_pth, _cleanup_checkout = prepare_repo_checkout(repo_name, sha, tmp_path)
|
|
65
|
+
try:
|
|
66
|
+
commit = repo.commit(sha)
|
|
67
|
+
with contextlib.suppress(Exception):
|
|
68
|
+
repo.git.checkout(sha)
|
|
69
|
+
|
|
70
|
+
# A) ASV configs (optional — repos without ASV can still be resolved)
|
|
71
|
+
asv_cfg_files = asv_finder(commit)
|
|
72
|
+
|
|
73
|
+
asv_cfgs = []
|
|
74
|
+
for cfg_file in asv_cfg_files:
|
|
75
|
+
with contextlib.suppress(Exception):
|
|
76
|
+
asv_cfgs.append(json5.loads(cfg_file.read_text()))
|
|
77
|
+
|
|
78
|
+
cfg_items = ASVCfgAggregate()
|
|
79
|
+
for cfg in asv_cfgs:
|
|
80
|
+
pythons: set[tuple[int, ...]] = set()
|
|
81
|
+
for py in getattr(cfg, "pythons", []) or []:
|
|
82
|
+
with contextlib.suppress(Exception):
|
|
83
|
+
pythons.add(tuple(map(int, str(py).split("."))))
|
|
84
|
+
cfg_items.pythons.update(pythons)
|
|
85
|
+
bc = getattr(cfg, "build_command", None)
|
|
86
|
+
ic = getattr(cfg, "install_command", None)
|
|
87
|
+
if bc:
|
|
88
|
+
if isinstance(bc, (list, tuple)):
|
|
89
|
+
bc = " && ".join(bc).replace("-mpip", "-m pip")
|
|
90
|
+
cfg_items.build_commands.add(str(bc))
|
|
91
|
+
if ic:
|
|
92
|
+
if isinstance(ic, (list, tuple)):
|
|
93
|
+
ic = " && ".join(ic)
|
|
94
|
+
cfg_items.install_commands.add(str(ic))
|
|
95
|
+
mx = getattr(cfg, "matrix", None) or {}
|
|
96
|
+
for k, v in mx.items():
|
|
97
|
+
values = cfg_items.matrix.setdefault(k, set())
|
|
98
|
+
if isinstance(v, (list, tuple, set)):
|
|
99
|
+
values.update(map(str, v))
|
|
100
|
+
else:
|
|
101
|
+
values.add(str(v))
|
|
102
|
+
|
|
103
|
+
if not cfg_items.pythons:
|
|
104
|
+
cfg_items.pythons.update({(3, 8), (3, 9), (3, 10), (3, 11), (3, 12)})
|
|
105
|
+
|
|
106
|
+
# B) Choose Python version candidates
|
|
107
|
+
if (not cfg_items.pythons) or all(py < (3, 8) for py in cfg_items.pythons):
|
|
108
|
+
logger.debug("No Python >=3.8 available in ASV config: %s", cfg_items.pythons)
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
authored = commit.authored_datetime
|
|
112
|
+
cutoff = rfc3339(authored)
|
|
113
|
+
candidate_python_versions = filter_python_versions_by_commit_date(cfg_items.pythons, authored)
|
|
114
|
+
|
|
115
|
+
if not candidate_python_versions:
|
|
116
|
+
logger.debug("No suitable Python versions after temporal filtering from %s", cfg_items.pythons)
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
# C) Discover packaging candidates
|
|
120
|
+
candidates = discover_candidates(commit)
|
|
121
|
+
if not candidates:
|
|
122
|
+
return None
|
|
123
|
+
analyzed: dict[str, Any] = {root: analyze_candidate_meta(c) for root, c in candidates.items()}
|
|
124
|
+
primary_root = select_primary_candidate(repo_name, candidates, cfg_items.install_commands, analyzed)
|
|
125
|
+
primary_meta = analyzed[primary_root]
|
|
126
|
+
primary_cand = candidates[primary_root]
|
|
127
|
+
project_dir = tmpfile_pth / primary_root
|
|
128
|
+
all_sources = [
|
|
129
|
+
s
|
|
130
|
+
for s in (primary_cand.setup_py_path, primary_cand.pyproject_path, primary_cand.setup_cfg_path)
|
|
131
|
+
if s and s.exists()
|
|
132
|
+
]
|
|
133
|
+
if len(all_sources):
|
|
134
|
+
for source in all_sources:
|
|
135
|
+
skip_source = False
|
|
136
|
+
for py_ver in (".".join(map(str, t)) for t in candidate_python_versions[:3]):
|
|
137
|
+
if skip_source:
|
|
138
|
+
break
|
|
139
|
+
for strict_cutoff in (True, False):
|
|
140
|
+
source_name = source.name.replace(".", "_")
|
|
141
|
+
candidate_venv_path = Path(tmpdir) / f"venv_{source_name}_{py_ver.replace('.', '_')}"
|
|
142
|
+
try:
|
|
143
|
+
venv_cp = run_uv(["venv", str(candidate_venv_path), "--python", py_ver])
|
|
144
|
+
if venv_cp.returncode != 0:
|
|
145
|
+
logger.debug(
|
|
146
|
+
"Failed to create venv with Python %s: %s", py_ver, venv_cp.stderr.decode()
|
|
147
|
+
)
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
python_exe = candidate_venv_path / "bin" / "python"
|
|
151
|
+
if not python_exe.exists():
|
|
152
|
+
python_exe = candidate_venv_path / "Scripts" / "python.exe"
|
|
153
|
+
|
|
154
|
+
if not python_exe.exists():
|
|
155
|
+
logger.debug("Venv created but Python executable not found for version %s", py_ver)
|
|
156
|
+
shutil.rmtree(candidate_venv_path, ignore_errors=True)
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
resolved = uv_compile_from_pyproject(
|
|
160
|
+
project_dir / source.name,
|
|
161
|
+
python_version=python_exe.as_posix(),
|
|
162
|
+
cutoff_rfc3339=cutoff if strict_cutoff else None,
|
|
163
|
+
)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
shutil.rmtree(candidate_venv_path, ignore_errors=True)
|
|
166
|
+
logger.debug(
|
|
167
|
+
"uv_compile_from_pyproject failed for Python %s with cutoff %s: %s",
|
|
168
|
+
py_ver,
|
|
169
|
+
"strict" if strict_cutoff else "none",
|
|
170
|
+
e,
|
|
171
|
+
)
|
|
172
|
+
if "--no-build-isolation" in str(e):
|
|
173
|
+
skip_source = True
|
|
174
|
+
break
|
|
175
|
+
continue
|
|
176
|
+
strat = f"{'cutoff=strict' if strict_cutoff else 'cutoff=none'}, extras=on, python={py_ver}, source={source.name}"
|
|
177
|
+
|
|
178
|
+
if len(resolved) > 0:
|
|
179
|
+
candidate_can_install, candidate_dry_run_log = uv_dry_run_install(
|
|
180
|
+
resolved, python_version=py_ver, venv_path=candidate_venv_path
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if candidate_can_install:
|
|
184
|
+
ok_real, real_log = uv_install_real(
|
|
185
|
+
resolved, python_executable=python_exe.as_posix()
|
|
186
|
+
)
|
|
187
|
+
if ok_real:
|
|
188
|
+
shutil.rmtree(candidate_venv_path, ignore_errors=True)
|
|
189
|
+
commit_info = {
|
|
190
|
+
"sha": sha,
|
|
191
|
+
"repo_name": repo_name,
|
|
192
|
+
"package_name": primary_meta.name,
|
|
193
|
+
"package_version": primary_meta.version,
|
|
194
|
+
"python_version": py_ver,
|
|
195
|
+
"build_command": list(cfg_items.build_commands),
|
|
196
|
+
"install_command": list(cfg_items.install_commands),
|
|
197
|
+
"final_dependencies": list(dict.fromkeys(resolved)),
|
|
198
|
+
"can_install": True,
|
|
199
|
+
"dry_run_log": candidate_dry_run_log,
|
|
200
|
+
"primary_root": primary_root,
|
|
201
|
+
"resolution_strategy": strat,
|
|
202
|
+
"excluded_missing_on_pypi": {},
|
|
203
|
+
"excluded_exists_incompatible": {},
|
|
204
|
+
"excluded_other": {},
|
|
205
|
+
}
|
|
206
|
+
return commit_info
|
|
207
|
+
else:
|
|
208
|
+
logger.debug(
|
|
209
|
+
"Preflight install failed for Python %s (source=%s); trying next.\n%s",
|
|
210
|
+
py_ver,
|
|
211
|
+
source.name,
|
|
212
|
+
real_log[-800:],
|
|
213
|
+
)
|
|
214
|
+
shutil.rmtree(candidate_venv_path, ignore_errors=True)
|
|
215
|
+
else:
|
|
216
|
+
shutil.rmtree(candidate_venv_path, ignore_errors=True)
|
|
217
|
+
|
|
218
|
+
# D) Aggregate base requirements (unresolved, human-intent)
|
|
219
|
+
base_requirements: set[str] = set()
|
|
220
|
+
base_requirements.update(primary_meta.core_deps)
|
|
221
|
+
base_requirements.add("pytest")
|
|
222
|
+
base_requirements.add("setuptools")
|
|
223
|
+
base_requirements.add("hypothesis")
|
|
224
|
+
|
|
225
|
+
requested_extras = extract_requested_extras(
|
|
226
|
+
cfg_items.install_commands, cfg_items.matrix, primary_meta.extras.keys()
|
|
227
|
+
)
|
|
228
|
+
for ex in requested_extras:
|
|
229
|
+
base_requirements.update(primary_meta.extras.get(ex, set()))
|
|
230
|
+
|
|
231
|
+
for install_cmd in cfg_items.install_commands:
|
|
232
|
+
for cmd_part in split_shell_command(install_cmd):
|
|
233
|
+
try:
|
|
234
|
+
tokens = shlex.split(cmd_part)
|
|
235
|
+
except Exception:
|
|
236
|
+
logger.exception("Failed to split command %s", cmd_part)
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
skip_next = False
|
|
240
|
+
for i, tok in enumerate(tokens):
|
|
241
|
+
if skip_next:
|
|
242
|
+
skip_next = False
|
|
243
|
+
continue
|
|
244
|
+
if tok in {"-r", "--requirement"} and i + 1 < len(tokens):
|
|
245
|
+
rel = tokens[i + 1]
|
|
246
|
+
skip_next = True
|
|
247
|
+
requirements_from_file = resolve_requirements_file(commit, rel, set())
|
|
248
|
+
base_requirements.update(requirements_from_file)
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
skip_next = False
|
|
252
|
+
for tok in tokens:
|
|
253
|
+
if skip_next:
|
|
254
|
+
skip_next = False
|
|
255
|
+
continue
|
|
256
|
+
if tok in {"-r", "--requirement"}:
|
|
257
|
+
skip_next = True
|
|
258
|
+
continue
|
|
259
|
+
if tok.startswith("-"):
|
|
260
|
+
continue
|
|
261
|
+
normalized = normalize_requirement(tok)
|
|
262
|
+
base_requirements.update(normalized)
|
|
263
|
+
|
|
264
|
+
for vals in cfg_items.matrix.values():
|
|
265
|
+
for v in vals:
|
|
266
|
+
s = str(v).strip()
|
|
267
|
+
if s and not s.startswith("-"):
|
|
268
|
+
normalized = normalize_requirement(s)
|
|
269
|
+
base_requirements.update(normalized)
|
|
270
|
+
|
|
271
|
+
# E) Build and read wheel metadata
|
|
272
|
+
project_dir = tmpfile_pth / primary_root
|
|
273
|
+
pkg_name, pkg_version, wheel_requires, wheel_requires_python = uv_build_and_read_metadata(project_dir)
|
|
274
|
+
|
|
275
|
+
if not primary_meta.name and pkg_name:
|
|
276
|
+
primary_meta.name = pkg_name
|
|
277
|
+
if not primary_meta.version and pkg_version:
|
|
278
|
+
primary_meta.version = pkg_version
|
|
279
|
+
if wheel_requires_python and not primary_meta.requires_python:
|
|
280
|
+
primary_meta.requires_python = wheel_requires_python
|
|
281
|
+
|
|
282
|
+
own_import = None
|
|
283
|
+
if primary_meta.name:
|
|
284
|
+
own_import = primary_meta.name.replace("-", "_")
|
|
285
|
+
|
|
286
|
+
# F) Candidate runtime requirements (unresolved)
|
|
287
|
+
runtime_candidates: set[str] = set(wheel_requires)
|
|
288
|
+
if not runtime_candidates:
|
|
289
|
+
runtime_inferred = infer_runtime_from_imports(project_dir, own_import_name=own_import)
|
|
290
|
+
build_names = {re.split(r"[~<>=!; ]", breq, maxsplit=1)[0] for breq in primary_meta.build_requires}
|
|
291
|
+
promote = {x for x in runtime_inferred if x in build_names}
|
|
292
|
+
runtime_candidates.update(runtime_inferred)
|
|
293
|
+
runtime_candidates.update(promote)
|
|
294
|
+
|
|
295
|
+
runtime_candidates.update(base_requirements)
|
|
296
|
+
|
|
297
|
+
cleaned_unresolved = filter_requirements_for_pypi(
|
|
298
|
+
runtime_candidates,
|
|
299
|
+
project_dir=project_dir,
|
|
300
|
+
own_import_name=own_import,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if not cleaned_unresolved and runtime_candidates:
|
|
304
|
+
cleaned_unresolved = sorted({
|
|
305
|
+
r for r in runtime_candidates if extract_pkg_name(r) in ALLOWLIST_COMMON_PYPI
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
found_flag = False
|
|
309
|
+
|
|
310
|
+
for use_cleaned_pinned in (False, True):
|
|
311
|
+
for py_tuple in candidate_python_versions:
|
|
312
|
+
if found_flag:
|
|
313
|
+
break
|
|
314
|
+
candidate_version = ".".join(map(str, py_tuple))
|
|
315
|
+
logger.debug("Trying Python %s", candidate_version)
|
|
316
|
+
|
|
317
|
+
if not ensure_python_version_available(candidate_version):
|
|
318
|
+
logger.debug("Python %s not available, trying next", candidate_version)
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
candidate_venv_path = Path(tmpdir) / f"venv_{candidate_version.replace('.', '_')}"
|
|
322
|
+
venv_cp = run_uv(["venv", str(candidate_venv_path), "--python", candidate_version])
|
|
323
|
+
|
|
324
|
+
if venv_cp.returncode != 0:
|
|
325
|
+
logger.debug(
|
|
326
|
+
"Failed to create venv with Python %s: %s", candidate_version, venv_cp.stderr.decode()
|
|
327
|
+
)
|
|
328
|
+
continue
|
|
329
|
+
|
|
330
|
+
python_exe = candidate_venv_path / "bin" / "python"
|
|
331
|
+
if not python_exe.exists():
|
|
332
|
+
python_exe = candidate_venv_path / "Scripts" / "python.exe"
|
|
333
|
+
|
|
334
|
+
if not python_exe.exists():
|
|
335
|
+
logger.debug("Venv created but Python executable not found for version %s", candidate_version)
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
def _compile_or_pass_through(
|
|
339
|
+
reqs: list[str], *, strict_cutoff: bool, py_ver: str
|
|
340
|
+
) -> tuple[list[str], str]:
|
|
341
|
+
from .blocklist import (
|
|
342
|
+
add_to_blocklist,
|
|
343
|
+
extract_failing_package,
|
|
344
|
+
remove_package_from_requirements,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
current_reqs = list(reqs)
|
|
348
|
+
max_compile_retries = 3
|
|
349
|
+
compile_retry_count = 0
|
|
350
|
+
|
|
351
|
+
while compile_retry_count <= max_compile_retries:
|
|
352
|
+
try:
|
|
353
|
+
resolved = uv_compile(
|
|
354
|
+
current_reqs,
|
|
355
|
+
python_version=py_ver,
|
|
356
|
+
cutoff_rfc3339=cutoff if strict_cutoff else None,
|
|
357
|
+
)
|
|
358
|
+
strat = (
|
|
359
|
+
f"{'cutoff=strict' if strict_cutoff else 'cutoff=none'}, extras=on, python={py_ver}"
|
|
360
|
+
)
|
|
361
|
+
if compile_retry_count > 0:
|
|
362
|
+
strat = f"{strat} (compile-healed: {compile_retry_count} pkgs)"
|
|
363
|
+
return resolved, strat
|
|
364
|
+
except Exception as e:
|
|
365
|
+
error_msg = str(e)
|
|
366
|
+
|
|
367
|
+
if compile_retry_count < max_compile_retries and (
|
|
368
|
+
"was not found in the package registry" in error_msg
|
|
369
|
+
or "Because there are no versions of" in error_msg
|
|
370
|
+
):
|
|
371
|
+
failing_pkg = extract_failing_package(error_msg)
|
|
372
|
+
if failing_pkg:
|
|
373
|
+
if add_to_blocklist(failing_pkg):
|
|
374
|
+
logger.info(
|
|
375
|
+
"Compile self-healing: Blocking '%s' (retry %d/%d)",
|
|
376
|
+
failing_pkg,
|
|
377
|
+
compile_retry_count + 1,
|
|
378
|
+
max_compile_retries,
|
|
379
|
+
)
|
|
380
|
+
current_reqs, was_removed = remove_package_from_requirements(
|
|
381
|
+
current_reqs, failing_pkg
|
|
382
|
+
)
|
|
383
|
+
if was_removed:
|
|
384
|
+
compile_retry_count += 1
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
return list(current_reqs), f"unresolved(pass-through): {e.__class__.__name__}"
|
|
388
|
+
|
|
389
|
+
return list(current_reqs), "unresolved(max-retries-exceeded)"
|
|
390
|
+
|
|
391
|
+
if use_cleaned_pinned:
|
|
392
|
+
cleaned_unresolved = clean_pinned(cleaned_unresolved)
|
|
393
|
+
candidate_resolved, candidate_strategy = _compile_or_pass_through(
|
|
394
|
+
cleaned_unresolved, strict_cutoff=True, py_ver=candidate_version
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if candidate_resolved == cleaned_unresolved and candidate_resolved:
|
|
398
|
+
relaxed = [x for x in cleaned_unresolved if not re.search(r"\[.*\]$", x)]
|
|
399
|
+
if relaxed and relaxed != cleaned_unresolved:
|
|
400
|
+
resolved2, strat2 = _compile_or_pass_through(
|
|
401
|
+
relaxed, strict_cutoff=False, py_ver=candidate_version
|
|
402
|
+
)
|
|
403
|
+
if resolved2 != relaxed or "cutoff=none" in strat2:
|
|
404
|
+
candidate_resolved, candidate_strategy = resolved2, strat2
|
|
405
|
+
|
|
406
|
+
if not candidate_resolved and cleaned_unresolved:
|
|
407
|
+
candidate_resolved = list(cleaned_unresolved)
|
|
408
|
+
|
|
409
|
+
# H) Validate via dry-run with self-healing retry
|
|
410
|
+
from .blocklist import (
|
|
411
|
+
add_to_blocklist,
|
|
412
|
+
extract_failing_package,
|
|
413
|
+
remove_package_from_requirements,
|
|
414
|
+
should_retry_without_package,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
candidate_can_install, candidate_dry_run_log = uv_dry_run_install(
|
|
418
|
+
candidate_resolved, python_version=candidate_version, venv_path=candidate_venv_path
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
max_retries = 3
|
|
422
|
+
retry_count = 0
|
|
423
|
+
current_deps = list(candidate_resolved)
|
|
424
|
+
|
|
425
|
+
while (
|
|
426
|
+
not candidate_can_install
|
|
427
|
+
and retry_count < max_retries
|
|
428
|
+
and should_retry_without_package(candidate_dry_run_log)
|
|
429
|
+
):
|
|
430
|
+
failing_pkg = extract_failing_package(candidate_dry_run_log)
|
|
431
|
+
if not failing_pkg:
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
if add_to_blocklist(failing_pkg):
|
|
435
|
+
logger.info(
|
|
436
|
+
"Self-healing: Blocking '%s' and retrying (attempt %d/%d)",
|
|
437
|
+
failing_pkg,
|
|
438
|
+
retry_count + 1,
|
|
439
|
+
max_retries,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
current_deps, was_removed = remove_package_from_requirements(current_deps, failing_pkg)
|
|
443
|
+
if not was_removed:
|
|
444
|
+
break
|
|
445
|
+
|
|
446
|
+
candidate_can_install, candidate_dry_run_log = uv_dry_run_install(
|
|
447
|
+
current_deps, python_version=candidate_version, venv_path=candidate_venv_path
|
|
448
|
+
)
|
|
449
|
+
retry_count += 1
|
|
450
|
+
|
|
451
|
+
if retry_count > 0:
|
|
452
|
+
candidate_resolved = current_deps
|
|
453
|
+
if retry_count > 0 and candidate_can_install:
|
|
454
|
+
candidate_strategy = f"{candidate_strategy} (self-healed: {retry_count} pkgs removed)"
|
|
455
|
+
|
|
456
|
+
python_version = candidate_version
|
|
457
|
+
resolved_dependencies = candidate_resolved
|
|
458
|
+
resolution_strategy = candidate_strategy
|
|
459
|
+
can_install = candidate_can_install
|
|
460
|
+
dry_run_log = candidate_dry_run_log
|
|
461
|
+
|
|
462
|
+
if can_install:
|
|
463
|
+
ok_real, real_log = uv_install_real(candidate_resolved, python_executable=python_exe.as_posix())
|
|
464
|
+
if ok_real:
|
|
465
|
+
found_flag = True
|
|
466
|
+
logger.debug("Success with Python %s (preflight install ok)!", candidate_version)
|
|
467
|
+
break
|
|
468
|
+
else:
|
|
469
|
+
logger.debug(
|
|
470
|
+
"Dry-run ok but real install failed on Python %s; trying older version.\n%s",
|
|
471
|
+
candidate_version,
|
|
472
|
+
real_log[-800:],
|
|
473
|
+
)
|
|
474
|
+
can_install = False
|
|
475
|
+
dry_run_log = real_log
|
|
476
|
+
|
|
477
|
+
log_lower = dry_run_log.lower()
|
|
478
|
+
is_abi_error = (
|
|
479
|
+
"python abi tag" in log_lower
|
|
480
|
+
or "cp3" in dry_run_log
|
|
481
|
+
or ("no wheels" in log_lower and "python" in log_lower)
|
|
482
|
+
or "cannot install on python version" in log_lower
|
|
483
|
+
or "only versions" in log_lower
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
if is_abi_error:
|
|
487
|
+
logger.debug("ABI incompatibility with Python %s, trying older version", candidate_version)
|
|
488
|
+
continue
|
|
489
|
+
else:
|
|
490
|
+
logger.debug("Non-ABI error with Python %s, stopping attempts", candidate_version)
|
|
491
|
+
break
|
|
492
|
+
|
|
493
|
+
if not python_version:
|
|
494
|
+
logger.debug("No Python version succeeded")
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
# I) Final identity
|
|
498
|
+
excluded_missing_on_pypi = {}
|
|
499
|
+
excluded_exists_incompatible = {}
|
|
500
|
+
excluded_other = {}
|
|
501
|
+
|
|
502
|
+
commit_info = {
|
|
503
|
+
"sha": sha,
|
|
504
|
+
"repo_name": repo_name,
|
|
505
|
+
"package_name": primary_meta.name,
|
|
506
|
+
"package_version": primary_meta.version,
|
|
507
|
+
"python_version": python_version,
|
|
508
|
+
"build_command": list(cfg_items.build_commands),
|
|
509
|
+
"install_command": list(cfg_items.install_commands),
|
|
510
|
+
"final_dependencies": list(dict.fromkeys(resolved_dependencies)),
|
|
511
|
+
"can_install": can_install,
|
|
512
|
+
"dry_run_log": dry_run_log,
|
|
513
|
+
"primary_root": primary_root,
|
|
514
|
+
"resolution_strategy": resolution_strategy,
|
|
515
|
+
"excluded_missing_on_pypi": excluded_missing_on_pypi,
|
|
516
|
+
"excluded_exists_incompatible": excluded_exists_incompatible,
|
|
517
|
+
"excluded_other": excluded_other,
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return commit_info
|
|
521
|
+
finally:
|
|
522
|
+
_cleanup_checkout()
|