fuzzprep 0.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.
- fuzzprep/__init__.py +3 -0
- fuzzprep/__main__.py +4 -0
- fuzzprep/agents/harness_builder/SKILL.md +156 -0
- fuzzprep/agents/library_builder/SKILL.md +147 -0
- fuzzprep/agents/scripts/check_build.sh +87 -0
- fuzzprep/agents/scripts/check_build_in_container.sh +73 -0
- fuzzprep/agents/scripts/check_dockerfile_from_scratch.sh +33 -0
- fuzzprep/cli.py +1109 -0
- fuzzprep/core/__init__.py +0 -0
- fuzzprep/core/agent_stream.py +304 -0
- fuzzprep/core/files.py +35 -0
- fuzzprep/core/paths.py +25 -0
- fuzzprep/core/reporting.py +218 -0
- fuzzprep/core/repos.py +217 -0
- fuzzprep/core/resources.py +41 -0
- fuzzprep/core/subprocesses.py +197 -0
- fuzzprep/feature_extractor/__init__.py +0 -0
- fuzzprep/feature_extractor/benchmark_yaml.py +92 -0
- fuzzprep/feature_extractor/extraction.py +184 -0
- fuzzprep/feature_extractor/models.py +96 -0
- fuzzprep/feature_extractor/native/.clang-format +1 -0
- fuzzprep/feature_extractor/native/CMakeLists.txt +90 -0
- fuzzprep/feature_extractor/native/include/feature_extractor.hpp +136 -0
- fuzzprep/feature_extractor/native/src/extraction_action.cpp +294 -0
- fuzzprep/feature_extractor/native/src/json_writer.cpp +154 -0
- fuzzprep/feature_extractor/native/src/macro_callbacks.cpp +75 -0
- fuzzprep/feature_extractor/native/src/main.cpp +130 -0
- fuzzprep/feature_extractor/native_build.py +99 -0
- fuzzprep/library_builder/__init__.py +0 -0
- fuzzprep/library_builder/agents.py +472 -0
- fuzzprep/library_builder/analysis.py +145 -0
- fuzzprep/library_builder/build_parameters.py +158 -0
- fuzzprep/library_builder/dependency_resolution.py +139 -0
- fuzzprep/library_builder/environments/__init__.py +0 -0
- fuzzprep/library_builder/environments/base.py +89 -0
- fuzzprep/library_builder/environments/gate.py +96 -0
- fuzzprep/library_builder/environments/local.py +205 -0
- fuzzprep/library_builder/environments/oss_fuzz.py +485 -0
- fuzzprep/library_builder/environments/verification.py +125 -0
- fuzzprep/library_builder/exploration.py +217 -0
- fuzzprep/library_builder/generation.py +325 -0
- fuzzprep/library_builder/harness_explorer.py +257 -0
- fuzzprep/library_builder/models.py +169 -0
- fuzzprep/library_builder/package_names.json +33 -0
- fuzzprep/library_builder/package_names.py +40 -0
- fuzzprep/library_builder/scripts.py +389 -0
- fuzzprep/library_builder/stats.py +102 -0
- fuzzprep/library_builder/symbol_patterns.json +65 -0
- fuzzprep/library_builder/timeouts.py +14 -0
- fuzzprep/library_builder/workspace.py +250 -0
- fuzzprep-0.1.0.dist-info/METADATA +255 -0
- fuzzprep-0.1.0.dist-info/RECORD +56 -0
- fuzzprep-0.1.0.dist-info/WHEEL +4 -0
- fuzzprep-0.1.0.dist-info/entry_points.txt +3 -0
- fuzzprep-0.1.0.dist-info/licenses/LICENSE +202 -0
- fuzzprep-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +52 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fuzzprep.core.files import write_executable
|
|
10
|
+
from fuzzprep.core.subprocesses import Runner, run_command
|
|
11
|
+
from fuzzprep.library_builder.environments.base import Environment
|
|
12
|
+
from fuzzprep.library_builder.models import (
|
|
13
|
+
HarnessExplorationResult,
|
|
14
|
+
Language,
|
|
15
|
+
LinkConfiguration,
|
|
16
|
+
)
|
|
17
|
+
from fuzzprep.library_builder.scripts import (
|
|
18
|
+
HARNESS_SOURCE_DIR,
|
|
19
|
+
build_harness_script,
|
|
20
|
+
build_harnesses_script,
|
|
21
|
+
write_default_fuzzer,
|
|
22
|
+
)
|
|
23
|
+
from fuzzprep.library_builder.timeouts import HARNESS_PROBE_TIMEOUT_SECONDS
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
_MAX_ATTEMPTS = 5
|
|
28
|
+
|
|
29
|
+
# Always linked rather than discovered: glibc's pthread_* entry points are weak symbols, so
|
|
30
|
+
# omitting -lpthread silently links no-op stubs instead of erroring, and _resolve_flags never
|
|
31
|
+
# sees an undefined symbol to react to.
|
|
32
|
+
_DEFAULT_LINK_FLAGS = ["-lpthread"]
|
|
33
|
+
|
|
34
|
+
_PATTERNS_FILE = Path(__file__).parent / "symbol_patterns.json"
|
|
35
|
+
|
|
36
|
+
# Each flag maps to a compiled regex that ORs all of its patterns together.
|
|
37
|
+
# Patterns use anchors (^/$) to avoid false substring matches.
|
|
38
|
+
_LIB_PATTERNS: dict[str, re.Pattern[str]] = {
|
|
39
|
+
flag: re.compile("|".join(patterns))
|
|
40
|
+
for flag, patterns in json.loads(_PATTERNS_FILE.read_text()).items()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# C++ ABI symbols that appear when a C library pulls in a C++ static archive.
|
|
44
|
+
# Matches operator new/delete, exception-handling (__cxa_), and personality routine.
|
|
45
|
+
_CXX_ABI_RE = re.compile(r"operator (?:new|delete)\b|__cxa_|__gxx_personality")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def explore_harness_compilation( # noqa: PLR0913 -- public API; every param is a distinct required input
|
|
49
|
+
install_dir: Path,
|
|
50
|
+
workdir: Path,
|
|
51
|
+
language: Language,
|
|
52
|
+
*,
|
|
53
|
+
extra_include_paths: list[str] | None = None,
|
|
54
|
+
extra_library_paths: list[str] | None = None,
|
|
55
|
+
environment: Environment = Environment.LOCAL,
|
|
56
|
+
run: Runner | None = None,
|
|
57
|
+
) -> HarnessExplorationResult:
|
|
58
|
+
"""Test harness compilation against install artifacts to discover transitive deps.
|
|
59
|
+
|
|
60
|
+
Links with --whole-archive to force every library symbol in, so every undefined
|
|
61
|
+
transitive dependency surfaces. Retries up to _MAX_ATTEMPTS times, accumulating the -l
|
|
62
|
+
flags resolved from each attempt's linker errors on top of _DEFAULT_LINK_FLAGS. Always
|
|
63
|
+
returns a result; callers read .succeeded.
|
|
64
|
+
|
|
65
|
+
extra_include_paths/extra_library_paths are fixed inputs (typically from a prior agent's
|
|
66
|
+
AgentReport) threaded unchanged into the returned result.
|
|
67
|
+
|
|
68
|
+
environment.harness_probe_command decides how each attempt enters the build. run defaults
|
|
69
|
+
to a host subprocess; a caller probing inside a container passes a run primitive that
|
|
70
|
+
wraps the command in `docker run`.
|
|
71
|
+
"""
|
|
72
|
+
extra_include_paths = extra_include_paths or []
|
|
73
|
+
extra_library_paths = extra_library_paths or []
|
|
74
|
+
lib_dir = install_dir / "lib"
|
|
75
|
+
include_dir = install_dir / "include"
|
|
76
|
+
|
|
77
|
+
static_libs = sorted(lib_dir.glob("*.a"))
|
|
78
|
+
logger.debug(
|
|
79
|
+
"Discovered static libraries [%s] from dir %s",
|
|
80
|
+
" ".join(str(p) for p in static_libs),
|
|
81
|
+
lib_dir,
|
|
82
|
+
)
|
|
83
|
+
if not static_libs:
|
|
84
|
+
return HarnessExplorationResult(
|
|
85
|
+
succeeded=False,
|
|
86
|
+
command=[],
|
|
87
|
+
static_libs=[],
|
|
88
|
+
include_dir=include_dir,
|
|
89
|
+
transitive_link_flags=[],
|
|
90
|
+
stdout="",
|
|
91
|
+
stderr="no *.a files found in install/lib",
|
|
92
|
+
exit_code=-1,
|
|
93
|
+
extra_include_paths=extra_include_paths,
|
|
94
|
+
extra_library_paths=extra_library_paths,
|
|
95
|
+
environment=environment,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
use_cpp = language == Language.CPP
|
|
99
|
+
harness_src_dir = workdir / HARNESS_SOURCE_DIR
|
|
100
|
+
harness_src_dir.mkdir(exist_ok=True)
|
|
101
|
+
# Idempotent: workspace materialization already wrote this stub. Discovery upgrades this
|
|
102
|
+
# same file's extension in place on a C++ finding rather than probing with a throwaway
|
|
103
|
+
# copy, so the discovered language cannot desync from what generation ships.
|
|
104
|
+
harness_path = write_default_fuzzer(harness_src_dir, language)
|
|
105
|
+
|
|
106
|
+
script_path = workdir / "compile_harness.sh"
|
|
107
|
+
batch_script_path = workdir / "compile_harnesses.sh"
|
|
108
|
+
runner = run if run is not None else run_command
|
|
109
|
+
extra_flags: list[str] = list(_DEFAULT_LINK_FLAGS)
|
|
110
|
+
seen_flags: set[str] = set(_DEFAULT_LINK_FLAGS)
|
|
111
|
+
last_stdout = ""
|
|
112
|
+
last_stderr = ""
|
|
113
|
+
last_exit = -1
|
|
114
|
+
diagnostic = ""
|
|
115
|
+
|
|
116
|
+
for _ in range(_MAX_ATTEMPTS):
|
|
117
|
+
write_executable(
|
|
118
|
+
script_path,
|
|
119
|
+
build_harness_script(
|
|
120
|
+
LinkConfiguration(
|
|
121
|
+
static_libs=static_libs,
|
|
122
|
+
transitive_link_flags=extra_flags,
|
|
123
|
+
extra_library_paths=extra_library_paths,
|
|
124
|
+
extra_include_paths=extra_include_paths,
|
|
125
|
+
),
|
|
126
|
+
whole_archive=True,
|
|
127
|
+
harness_cflags=os.environ.get("CFLAGS"),
|
|
128
|
+
harness_cxxflags=os.environ.get("CXXFLAGS"),
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
write_executable(batch_script_path, build_harnesses_script())
|
|
132
|
+
command = environment.harness_probe_command
|
|
133
|
+
result = runner(command, workdir, HARNESS_PROBE_TIMEOUT_SECONDS)
|
|
134
|
+
last_stdout = result.stdout
|
|
135
|
+
last_stderr = result.stderr
|
|
136
|
+
last_exit = result.exit_code
|
|
137
|
+
diagnostic = result.output
|
|
138
|
+
|
|
139
|
+
if result.exit_code == 0:
|
|
140
|
+
return HarnessExplorationResult(
|
|
141
|
+
succeeded=True,
|
|
142
|
+
command=command,
|
|
143
|
+
static_libs=static_libs,
|
|
144
|
+
include_dir=include_dir,
|
|
145
|
+
transitive_link_flags=extra_flags,
|
|
146
|
+
stdout=last_stdout,
|
|
147
|
+
stderr=last_stderr,
|
|
148
|
+
exit_code=last_exit,
|
|
149
|
+
script_path=script_path,
|
|
150
|
+
extra_include_paths=extra_include_paths,
|
|
151
|
+
extra_library_paths=extra_library_paths,
|
|
152
|
+
environment=environment,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
upgraded_to_cxx = False
|
|
156
|
+
if not use_cpp and _requires_cxx(diagnostic):
|
|
157
|
+
logger.debug("Upgrading harness build to use CXX instead of CC")
|
|
158
|
+
harness_path.unlink(missing_ok=True)
|
|
159
|
+
harness_path = write_default_fuzzer(harness_src_dir, Language.CPP)
|
|
160
|
+
use_cpp = True
|
|
161
|
+
upgraded_to_cxx = True
|
|
162
|
+
|
|
163
|
+
new_flags = _resolve_flags(diagnostic, seen_flags)
|
|
164
|
+
if not new_flags and not upgraded_to_cxx:
|
|
165
|
+
break
|
|
166
|
+
extra_flags.extend(new_flags)
|
|
167
|
+
seen_flags.update(new_flags)
|
|
168
|
+
|
|
169
|
+
return HarnessExplorationResult(
|
|
170
|
+
succeeded=False,
|
|
171
|
+
command=[],
|
|
172
|
+
static_libs=static_libs,
|
|
173
|
+
include_dir=include_dir,
|
|
174
|
+
transitive_link_flags=extra_flags,
|
|
175
|
+
stdout=last_stdout,
|
|
176
|
+
stderr=last_stderr,
|
|
177
|
+
exit_code=last_exit,
|
|
178
|
+
missing_system_libs=extract_missing_system_libs(diagnostic),
|
|
179
|
+
extra_include_paths=extra_include_paths,
|
|
180
|
+
extra_library_paths=extra_library_paths,
|
|
181
|
+
environment=environment,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _resolve_flags(stderr: str, already_seen: set[str]) -> list[str]:
|
|
186
|
+
symbols = _extract_undefined_symbols(stderr)
|
|
187
|
+
resolved: dict[str, str] = {}
|
|
188
|
+
for sym in symbols:
|
|
189
|
+
flag = _symbol_to_flag(sym)
|
|
190
|
+
if flag and flag not in already_seen and flag not in resolved:
|
|
191
|
+
resolved[flag] = flag
|
|
192
|
+
return list(resolved.values())
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _extract_undefined_symbols(stderr: str) -> list[str]:
|
|
196
|
+
symbols: list[str] = []
|
|
197
|
+
# Linux ld: undefined reference to `symbol'
|
|
198
|
+
for m in re.finditer(r"undefined reference to `([^']+)'", stderr):
|
|
199
|
+
symbols.append(m.group(1))
|
|
200
|
+
# macOS ld (compact): Undefined symbol: _symbol
|
|
201
|
+
for m in re.finditer(r'[Uu]ndefined symbol[: ]+[_"]?([A-Za-z_][A-Za-z0-9_]*)', stderr):
|
|
202
|
+
symbols.append(m.group(1))
|
|
203
|
+
# macOS ld (verbose): " "_Symbol", referenced from:"
|
|
204
|
+
for m in re.finditer(r'"_([A-Za-z_][A-Za-z0-9_]*)",\s+referenced from:', stderr):
|
|
205
|
+
symbols.append(m.group(1))
|
|
206
|
+
return symbols
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _requires_cxx(stderr: str) -> bool:
|
|
210
|
+
if _CXX_ABI_RE.search(stderr):
|
|
211
|
+
return True
|
|
212
|
+
return any(
|
|
213
|
+
symbol.startswith("_Z") or "::" in symbol for symbol in _extract_undefined_symbols(stderr)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def extract_missing_system_libs(stderr: str) -> list[str]:
|
|
218
|
+
libs: list[str] = []
|
|
219
|
+
# macOS ld: library 'zstd' not found
|
|
220
|
+
for m in re.finditer(r"ld: library '([^']+)' not found", stderr):
|
|
221
|
+
libs.append(m.group(1))
|
|
222
|
+
# Linux ld: cannot find -lzstd
|
|
223
|
+
for m in re.finditer(r"ld: cannot find -l([^\s:]+)", stderr):
|
|
224
|
+
libs.append(m.group(1))
|
|
225
|
+
return list(dict.fromkeys(libs)) # deduplicate, preserve order
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def lib_names_from_link_flags(flags: list[str]) -> list[str]:
|
|
229
|
+
"""Strip the "-l" prefix from transitive_link_flags entries.
|
|
230
|
+
|
|
231
|
+
Every entry is "-l<name>", since every key in symbol_patterns.json is. The bare names
|
|
232
|
+
are what package_names.translate() takes.
|
|
233
|
+
"""
|
|
234
|
+
return [flag.removeprefix("-l") for flag in flags]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _symbol_to_flag(symbol: str) -> str | None:
|
|
238
|
+
for flag, pattern in _LIB_PATTERNS.items():
|
|
239
|
+
if pattern.search(symbol):
|
|
240
|
+
return flag
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def validate_harness_artifacts(workdir: Path) -> list[str]:
|
|
245
|
+
"""Check that a harness binary really exists on the host, in every environment.
|
|
246
|
+
|
|
247
|
+
An agent's exit code is not evidence that it linked anything, so what it claims to have
|
|
248
|
+
produced is checked directly. One host path serves both environments because the gate puts
|
|
249
|
+
the binaries there: a local build writes <workdir>/out itself, and
|
|
250
|
+
check_build_in_container.sh bind-mounts that same directory over the base image's
|
|
251
|
+
$OUT=/out, which is outside the /src mount and would otherwise be discarded with the
|
|
252
|
+
container.
|
|
253
|
+
"""
|
|
254
|
+
out_dir = workdir / "out"
|
|
255
|
+
if not out_dir.exists() or not any(out_dir.iterdir()):
|
|
256
|
+
return [f"no compiled harness binary found in {out_dir}"]
|
|
257
|
+
return []
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum, StrEnum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fuzzprep.core.subprocesses import MergedOutput
|
|
8
|
+
from fuzzprep.library_builder.environments.base import Environment
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BuildSystem(Enum):
|
|
12
|
+
CMAKE = "cmake"
|
|
13
|
+
MESON = "meson"
|
|
14
|
+
AUTOTOOLS = "autotools"
|
|
15
|
+
MAKEFILE = "makefile"
|
|
16
|
+
UNKNOWN = "unknown"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AutotoolsSetup(Enum):
|
|
20
|
+
CONFIGURE = "configure" # configure script already present
|
|
21
|
+
AUTOGEN = "autogen" # autogen.sh present, must run before configure
|
|
22
|
+
BOOTSTRAP = "bootstrap" # bootstrap present (gnulib convention), same role as autogen.sh
|
|
23
|
+
AUTORECONF = "autoreconf" # only configure.ac / configure.in, need autoreconf -fiv
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Language(Enum):
|
|
27
|
+
C = "c"
|
|
28
|
+
CPP = "c++"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class AnalysisResult:
|
|
33
|
+
"""What static analysis concluded about a repository — immutable pipeline input.
|
|
34
|
+
|
|
35
|
+
Frozen because every later phase reads it and none owns it. Anything a later phase
|
|
36
|
+
discovers travels onward as an explicit argument, not as a mutation of this record.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
project_name: str
|
|
40
|
+
source_path: Path
|
|
41
|
+
build_system: BuildSystem
|
|
42
|
+
language: Language
|
|
43
|
+
clone_url: str
|
|
44
|
+
repo_ref: str | None
|
|
45
|
+
warnings: list[str] = field(default_factory=list)
|
|
46
|
+
autotools_setup: AutotoolsSetup | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class AgentReport:
|
|
51
|
+
"""Parsed contents of an agent invocation's agent_report.json, or "nothing reported"."""
|
|
52
|
+
|
|
53
|
+
summary: str | None = None
|
|
54
|
+
missing_libs: list[str] = field(default_factory=list)
|
|
55
|
+
missing_apt_packages: list[str] = field(default_factory=list)
|
|
56
|
+
extra_include_paths: list[str] = field(default_factory=list)
|
|
57
|
+
extra_library_paths: list[str] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AgentStopReason(StrEnum):
|
|
61
|
+
"""Why a repair agent stopped without fixing the build.
|
|
62
|
+
|
|
63
|
+
None of these is a verdict on the build: one needs a person, one needs time to pass, and
|
|
64
|
+
one needs only a retry. Reported on the result, so "the build did not pass" stays one
|
|
65
|
+
control path -- but a consumer counting repair failures must exclude them, because in all
|
|
66
|
+
three cases the agent never finished judging the build.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
ACTION_REQUIRED = "action_required"
|
|
70
|
+
BUDGET_LIMITED = "budget_limited"
|
|
71
|
+
SERVICE_UNAVAILABLE = "service_unavailable"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(kw_only=True)
|
|
75
|
+
class AgentOutcome:
|
|
76
|
+
"""What a repair-agent attempt contributes to a stage's result.
|
|
77
|
+
|
|
78
|
+
Both stage results carry this same block, so it is declared once here and inherited.
|
|
79
|
+
Keyword-only, so the two results can still declare their own required fields.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
llm_used: bool = False
|
|
83
|
+
# The validated script to publish, set once a stage (or an agent's repair) proved it
|
|
84
|
+
# works and its paths are portable enough to copy verbatim.
|
|
85
|
+
script_path: Path | None = None
|
|
86
|
+
agent_stop_reason: AgentStopReason | None = None
|
|
87
|
+
# Non-empty only when FuzzPrep rejected a repair the agent reported as done, because
|
|
88
|
+
# the artifacts it claims to have produced are not there. That is a different thing to
|
|
89
|
+
# tell the user than an agent that failed and said so.
|
|
90
|
+
validation_errors: list[str] = field(default_factory=list)
|
|
91
|
+
cost_usd: float | None = None
|
|
92
|
+
input_tokens: int | None = None
|
|
93
|
+
output_tokens: int | None = None
|
|
94
|
+
transcript_path: Path | None = None
|
|
95
|
+
agent_summary: str | None = None
|
|
96
|
+
missing_apt_packages: list[str] = field(default_factory=list)
|
|
97
|
+
extra_include_paths: list[str] = field(default_factory=list)
|
|
98
|
+
extra_library_paths: list[str] = field(default_factory=list)
|
|
99
|
+
environment: Environment = Environment.LOCAL
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class LinkConfiguration:
|
|
104
|
+
"""Everything a harness link line needs: the library's own archives, plus what they
|
|
105
|
+
transitively pull in and where to find it.
|
|
106
|
+
|
|
107
|
+
Separate from HarnessExplorationResult so the retry loop, which rewrites
|
|
108
|
+
compile_harness.sh with one more resolved flag per attempt, proposes a link line rather
|
|
109
|
+
than appearing to report an outcome.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
static_libs: list[Path] = field(default_factory=list)
|
|
113
|
+
transitive_link_flags: list[str] = field(default_factory=list)
|
|
114
|
+
extra_library_paths: list[str] = field(default_factory=list)
|
|
115
|
+
extra_include_paths: list[str] = field(default_factory=list)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class BuildExplorationResult(MergedOutput, AgentOutcome):
|
|
120
|
+
build_system: BuildSystem
|
|
121
|
+
succeeded: bool
|
|
122
|
+
command: list[str]
|
|
123
|
+
stdout: str
|
|
124
|
+
stderr: str
|
|
125
|
+
exit_code: int
|
|
126
|
+
duration_seconds: float
|
|
127
|
+
# The validated install tree. Set even when the source lives outside the workspace and
|
|
128
|
+
# the build script therefore cannot be copied verbatim.
|
|
129
|
+
install_dir: Path | None = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class HarnessExplorationResult(MergedOutput, AgentOutcome):
|
|
134
|
+
succeeded: bool
|
|
135
|
+
command: list[str]
|
|
136
|
+
static_libs: list[Path]
|
|
137
|
+
include_dir: Path
|
|
138
|
+
transitive_link_flags: list[str]
|
|
139
|
+
stdout: str
|
|
140
|
+
stderr: str
|
|
141
|
+
exit_code: int
|
|
142
|
+
missing_system_libs: list[str] = field(default_factory=list)
|
|
143
|
+
duration_seconds: float = 0.0
|
|
144
|
+
# Whether the gate that produced this result reused the workspace's install/ rather than
|
|
145
|
+
# rebuilding the library from nothing. Stamped by environments/gate.py, so a repair agent
|
|
146
|
+
# is told to run the same gate the pipeline ran instead of recomputing the decision and
|
|
147
|
+
# paying for a cold rebuild the pipeline had decided to skip. See
|
|
148
|
+
# verification.gate_keeps_artifacts.
|
|
149
|
+
gate_keeps_artifacts: bool = False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class BuildPaths:
|
|
154
|
+
source_dir: str
|
|
155
|
+
build_dir: str
|
|
156
|
+
install_dir: str
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class HarnessPaths:
|
|
161
|
+
install_dir: Path
|
|
162
|
+
workdir: Path
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class GenerationResult:
|
|
167
|
+
project_name: str
|
|
168
|
+
output_path: Path
|
|
169
|
+
files: list[Path]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"system_libs": ["m", "pthread", "dl", "rt", "resolv", "c", "gcc_s", "stdc++"],
|
|
4
|
+
"apt_packages": {
|
|
5
|
+
"z": "zlib1g-dev",
|
|
6
|
+
"zstd": "libzstd-dev",
|
|
7
|
+
"lz4": "liblz4-dev",
|
|
8
|
+
"lzma": "liblzma-dev",
|
|
9
|
+
"bz2": "libbz2-dev",
|
|
10
|
+
"ssl": "libssl-dev",
|
|
11
|
+
"crypto": "libssl-dev",
|
|
12
|
+
"iconv": "libc6-dev",
|
|
13
|
+
"xml2": "libxml2-dev",
|
|
14
|
+
"sqlite3": "libsqlite3-dev",
|
|
15
|
+
"pcre2-8": "libpcre2-dev",
|
|
16
|
+
"ffi": "libffi-dev",
|
|
17
|
+
"curl": "libcurl4-openssl-dev",
|
|
18
|
+
"event": "libevent-dev",
|
|
19
|
+
"uv": "libuv1-dev",
|
|
20
|
+
"png": "libpng-dev",
|
|
21
|
+
"jpeg": "libjpeg-dev",
|
|
22
|
+
"webp": "libwebp-dev",
|
|
23
|
+
"freetype": "libfreetype6-dev",
|
|
24
|
+
"sodium": "libsodium-dev",
|
|
25
|
+
"gcrypt": "libgcrypt20-dev",
|
|
26
|
+
"ssh2": "libssh2-1-dev",
|
|
27
|
+
"seccomp": "libseccomp-dev",
|
|
28
|
+
"gmp": "libgmp-dev",
|
|
29
|
+
"archive": "libarchive-dev",
|
|
30
|
+
"magic": "libmagic-dev",
|
|
31
|
+
"git2": "libgit2-dev"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_DATA_FILE = Path(__file__).parent / "package_names.json"
|
|
8
|
+
_DATA = json.loads(_DATA_FILE.read_text())
|
|
9
|
+
_SYSTEM_LIBS: frozenset[str] = frozenset(_DATA["system_libs"])
|
|
10
|
+
_APT_PACKAGES: dict[str, str] = _DATA["apt_packages"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class PackageTranslation:
|
|
15
|
+
apt_packages: list[str]
|
|
16
|
+
unknown_libs: list[str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def translate(lib_names: list[str]) -> PackageTranslation:
|
|
20
|
+
"""Translate raw lib names (e.g. ["zstd"]) to installable apt package names.
|
|
21
|
+
|
|
22
|
+
System libs (m, pthread, dl, rt, resolv, c, gcc_s, stdc++) are silently dropped.
|
|
23
|
+
Libs with no mapping go into unknown_libs. Deduplication preserves order.
|
|
24
|
+
"""
|
|
25
|
+
apt: list[str] = []
|
|
26
|
+
unknown: list[str] = []
|
|
27
|
+
|
|
28
|
+
for lib in lib_names:
|
|
29
|
+
if lib in _SYSTEM_LIBS:
|
|
30
|
+
continue
|
|
31
|
+
package = _APT_PACKAGES.get(lib)
|
|
32
|
+
if package is None:
|
|
33
|
+
unknown.append(lib)
|
|
34
|
+
continue
|
|
35
|
+
apt.append(package)
|
|
36
|
+
|
|
37
|
+
return PackageTranslation(
|
|
38
|
+
apt_packages=list(dict.fromkeys(apt)),
|
|
39
|
+
unknown_libs=list(dict.fromkeys(unknown)),
|
|
40
|
+
)
|