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
fuzzprep/cli.py
ADDED
|
@@ -0,0 +1,1109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from fuzzprep.core.reporting import (
|
|
12
|
+
Phase,
|
|
13
|
+
PhaseReporter,
|
|
14
|
+
build_diagnostic,
|
|
15
|
+
format_diagnostic,
|
|
16
|
+
format_startup_failure,
|
|
17
|
+
summarize_message,
|
|
18
|
+
)
|
|
19
|
+
from fuzzprep.core.subprocesses import streaming_context
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from fuzzprep.core.reporting import FailureDiagnostic
|
|
23
|
+
from fuzzprep.core.repos import RepoSource
|
|
24
|
+
from fuzzprep.library_builder.build_parameters import BuildParameters
|
|
25
|
+
from fuzzprep.library_builder.dependency_resolution import DependencyState
|
|
26
|
+
from fuzzprep.library_builder.environments.base import Environment, EnvironmentExecutor
|
|
27
|
+
from fuzzprep.library_builder.generation import GenerationInputs
|
|
28
|
+
from fuzzprep.library_builder.models import (
|
|
29
|
+
AnalysisResult,
|
|
30
|
+
BuildExplorationResult,
|
|
31
|
+
BuildSystem,
|
|
32
|
+
HarnessExplorationResult,
|
|
33
|
+
)
|
|
34
|
+
from fuzzprep.library_builder.stats import RunStatus
|
|
35
|
+
|
|
36
|
+
_DEFAULT_AGENT = "claude"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _configure_generate_parser(p: argparse.ArgumentParser) -> None:
|
|
40
|
+
p.add_argument(
|
|
41
|
+
"repo_url",
|
|
42
|
+
metavar="REPO_URL",
|
|
43
|
+
help="Repository URL or local path to analyze.",
|
|
44
|
+
)
|
|
45
|
+
p.add_argument(
|
|
46
|
+
"--agent",
|
|
47
|
+
choices=["codex", "claude"],
|
|
48
|
+
default=_DEFAULT_AGENT,
|
|
49
|
+
metavar="codex|claude",
|
|
50
|
+
help=(
|
|
51
|
+
f"Agent backend used to repair a failed build. Default: {_DEFAULT_AGENT}. "
|
|
52
|
+
"Overridden by --no-agents."
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
p.add_argument(
|
|
56
|
+
"--output",
|
|
57
|
+
metavar="DIR",
|
|
58
|
+
help="Parent directory for the generated project. Output is written to DIR/PROJECT_NAME.",
|
|
59
|
+
)
|
|
60
|
+
p.add_argument(
|
|
61
|
+
"--project-name",
|
|
62
|
+
metavar="NAME",
|
|
63
|
+
help="Override the project name inferred from REPO_URL.",
|
|
64
|
+
)
|
|
65
|
+
p.add_argument(
|
|
66
|
+
"--repo-ref",
|
|
67
|
+
metavar="REF",
|
|
68
|
+
help="Branch, tag, or commit to check out. Not valid for a local path.",
|
|
69
|
+
)
|
|
70
|
+
p.add_argument(
|
|
71
|
+
"--environment",
|
|
72
|
+
choices=["local", "oss-fuzz"],
|
|
73
|
+
default="local",
|
|
74
|
+
metavar="local|oss-fuzz",
|
|
75
|
+
help="Target environment to build and validate each stage in. Default: local.",
|
|
76
|
+
)
|
|
77
|
+
p.add_argument(
|
|
78
|
+
"--base-image",
|
|
79
|
+
metavar="IMAGE",
|
|
80
|
+
help="Base image for the generated Dockerfile. Default: the OSS-Fuzz base builder.",
|
|
81
|
+
)
|
|
82
|
+
p.add_argument("--cc", metavar="COMPILER", help="C compiler for local build preparation.")
|
|
83
|
+
p.add_argument("--cxx", metavar="COMPILER", help="C++ compiler for local build preparation.")
|
|
84
|
+
p.add_argument(
|
|
85
|
+
"--library-cflags",
|
|
86
|
+
metavar="FLAGS",
|
|
87
|
+
help="C flags for the library build; defaults to CFLAGS. Use --library-cflags=FLAGS.",
|
|
88
|
+
)
|
|
89
|
+
p.add_argument(
|
|
90
|
+
"--library-cxxflags",
|
|
91
|
+
metavar="FLAGS",
|
|
92
|
+
help="C++ flags for the library build; defaults to CXXFLAGS. Use --library-cxxflags=FLAGS.",
|
|
93
|
+
)
|
|
94
|
+
p.add_argument(
|
|
95
|
+
"--library-configure-arg",
|
|
96
|
+
dest="library_configure_args",
|
|
97
|
+
action="append",
|
|
98
|
+
metavar="ARG",
|
|
99
|
+
help=(
|
|
100
|
+
"Configure option for the library build, baked into build_library.sh: a cmake "
|
|
101
|
+
"-DVAR=VALUE, a meson -Doption=value, an autotools --enable-foo, or a make "
|
|
102
|
+
"VAR=value. Repeat for more than one."
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
p.add_argument(
|
|
106
|
+
"--harness-cflags",
|
|
107
|
+
metavar="FLAGS",
|
|
108
|
+
help="Default C flags in the generated local harness compiler; use --harness-cflags=FLAGS.",
|
|
109
|
+
)
|
|
110
|
+
p.add_argument(
|
|
111
|
+
"--harness-cxxflags",
|
|
112
|
+
metavar="FLAGS",
|
|
113
|
+
help=(
|
|
114
|
+
"Default C++ flags in the generated local harness compiler; "
|
|
115
|
+
"use --harness-cxxflags=FLAGS."
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
p.add_argument(
|
|
119
|
+
"--no-agents",
|
|
120
|
+
action="store_true",
|
|
121
|
+
help="Disable agent repair; a failed build then simply fails the run.",
|
|
122
|
+
)
|
|
123
|
+
p.add_argument(
|
|
124
|
+
"--bypass-scratch-validation",
|
|
125
|
+
action="store_true",
|
|
126
|
+
help=(
|
|
127
|
+
"Turn off default behavior that builds the library in a fresh "
|
|
128
|
+
"environment to confirm that all agent-provided changes are reproducible "
|
|
129
|
+
"and OSS-Fuzz builds work from scratch."
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
p.add_argument(
|
|
133
|
+
"--quiet",
|
|
134
|
+
action="store_true",
|
|
135
|
+
help=(
|
|
136
|
+
"Suppress per-line raw subprocess output while a phase is running. "
|
|
137
|
+
"Phase banners and failure diagnostics are always shown regardless."
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _configure_extract_features_parser(p: argparse.ArgumentParser) -> None:
|
|
143
|
+
p.add_argument(
|
|
144
|
+
"build_path",
|
|
145
|
+
metavar="BUILD_PATH",
|
|
146
|
+
help="Directory containing compile_commands.json to extract features from.",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _configure_generate_yaml_parser(p: argparse.ArgumentParser) -> None:
|
|
151
|
+
p.add_argument(
|
|
152
|
+
"build_path",
|
|
153
|
+
metavar="BUILD_PATH",
|
|
154
|
+
help="Directory containing features.json from a prior extract-features run.",
|
|
155
|
+
)
|
|
156
|
+
p.add_argument(
|
|
157
|
+
"headers",
|
|
158
|
+
metavar="HEADER_NAMES",
|
|
159
|
+
nargs="*",
|
|
160
|
+
help="List of header file names to include in analysis",
|
|
161
|
+
)
|
|
162
|
+
p.add_argument(
|
|
163
|
+
"--target-name",
|
|
164
|
+
metavar="NAME",
|
|
165
|
+
help="Override the default 'default_fuzzer' benchmark target name.",
|
|
166
|
+
)
|
|
167
|
+
p.add_argument(
|
|
168
|
+
"--target-path",
|
|
169
|
+
metavar="PATH",
|
|
170
|
+
help="Override the default /src/harness_source/<target-name>.{c,cc} target path.",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
175
|
+
parser = argparse.ArgumentParser(
|
|
176
|
+
prog="fuzzprep",
|
|
177
|
+
description="Prepare C/C++ libraries for OSS-Fuzz project generation.",
|
|
178
|
+
)
|
|
179
|
+
parser.add_argument(
|
|
180
|
+
"--log-level",
|
|
181
|
+
choices=["debug", "info", "warning", "error", "critical"],
|
|
182
|
+
default=None,
|
|
183
|
+
metavar="LEVEL",
|
|
184
|
+
help="Set logging verbosity (debug|info|warning|error|critical). Default: no output.",
|
|
185
|
+
)
|
|
186
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
187
|
+
generate = subparsers.add_parser(
|
|
188
|
+
"generate",
|
|
189
|
+
help="Generate an oss-fuzz project for a C/C++ library.",
|
|
190
|
+
description="Generate an oss-fuzz project directory for a C/C++ library repository.",
|
|
191
|
+
)
|
|
192
|
+
_configure_generate_parser(generate)
|
|
193
|
+
extract_features = subparsers.add_parser(
|
|
194
|
+
"extract-features",
|
|
195
|
+
help="Extract a library's API surface into a JSON feature artifact.",
|
|
196
|
+
description="Extract functions, typedefs, macros, enums, and records from a "
|
|
197
|
+
"compile_commands.json into features.json.",
|
|
198
|
+
)
|
|
199
|
+
_configure_extract_features_parser(extract_features)
|
|
200
|
+
generate_yaml = subparsers.add_parser(
|
|
201
|
+
"generate-yaml",
|
|
202
|
+
help="Convert an extracted feature artifact into an oss-fuzz-gen benchmark YAML.",
|
|
203
|
+
description="Convert features.json into a curated, oss-fuzz-gen-compatible "
|
|
204
|
+
"benchmark YAML file.",
|
|
205
|
+
)
|
|
206
|
+
_configure_generate_yaml_parser(generate_yaml)
|
|
207
|
+
return parser
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def main(argv: list[str] | None = None) -> int:
|
|
211
|
+
parser = build_parser()
|
|
212
|
+
args = parser.parse_args(argv)
|
|
213
|
+
level = getattr(logging, args.log_level.upper()) if args.log_level else logging.CRITICAL + 1
|
|
214
|
+
# force=True because basicConfig silently no-ops when the root logger already has
|
|
215
|
+
# handlers, e.g. a prior call in the same process or a test harness's own setup.
|
|
216
|
+
logging.basicConfig(level=level, force=True)
|
|
217
|
+
if args.command is None:
|
|
218
|
+
parser.print_help()
|
|
219
|
+
return 0
|
|
220
|
+
if args.command == "generate":
|
|
221
|
+
return _cmd_generate(args)
|
|
222
|
+
if args.command == "extract-features":
|
|
223
|
+
return _cmd_extract_features(args)
|
|
224
|
+
if args.command == "generate-yaml":
|
|
225
|
+
return _cmd_generate_benchmark(args)
|
|
226
|
+
else:
|
|
227
|
+
raise ValueError("Unknown command")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def build_library( # noqa: PLR0913 -- public API; all params are distinct required/optional inputs
|
|
231
|
+
analysis: AnalysisResult,
|
|
232
|
+
workspace: Path,
|
|
233
|
+
executor: EnvironmentExecutor,
|
|
234
|
+
*,
|
|
235
|
+
agent: str | None = None,
|
|
236
|
+
timeout: int | None = None,
|
|
237
|
+
quiet: bool = False,
|
|
238
|
+
logs_dir: Path | None = None,
|
|
239
|
+
parameters: BuildParameters | None = None,
|
|
240
|
+
) -> BuildExplorationResult:
|
|
241
|
+
"""Run the executor's library build, then fall back to an LLM agent if it failed.
|
|
242
|
+
|
|
243
|
+
result.llm_used is True when the agent path was taken; result.agent_stop_reason says why
|
|
244
|
+
the agent stopped, if it did. The deterministic build and the agent repair get their own
|
|
245
|
+
phase banners. logs_dir, when given, is where the deterministic build's raw output goes.
|
|
246
|
+
"""
|
|
247
|
+
from fuzzprep.library_builder.build_parameters import BuildParameters
|
|
248
|
+
from fuzzprep.library_builder.timeouts import DEFAULT_BUILD_TIMEOUT_SECONDS
|
|
249
|
+
|
|
250
|
+
parameters = parameters or BuildParameters.defaults()
|
|
251
|
+
timeout = timeout if timeout is not None else DEFAULT_BUILD_TIMEOUT_SECONDS
|
|
252
|
+
log_name = f"{Phase.DETERMINISTIC_LIBRARY_BUILD.value}.log"
|
|
253
|
+
deterministic_log_path = logs_dir / log_name if logs_dir else None
|
|
254
|
+
with PhaseReporter(Phase.DETERMINISTIC_LIBRARY_BUILD) as reporter:
|
|
255
|
+
with (
|
|
256
|
+
parameters.library_environment(),
|
|
257
|
+
streaming_context(quiet=quiet, log_path=deterministic_log_path),
|
|
258
|
+
):
|
|
259
|
+
result = executor.run_library_build(
|
|
260
|
+
analysis, workspace, timeout=timeout, parameters=parameters
|
|
261
|
+
)
|
|
262
|
+
reporter.succeed() if result.succeeded else reporter.fail()
|
|
263
|
+
|
|
264
|
+
if result.succeeded:
|
|
265
|
+
return result
|
|
266
|
+
if agent is None:
|
|
267
|
+
print("Library build failed and agent repair is disabled (--no-agents).")
|
|
268
|
+
return result
|
|
269
|
+
|
|
270
|
+
from fuzzprep.library_builder.agents import invoke_library_builder_agent
|
|
271
|
+
|
|
272
|
+
with PhaseReporter(Phase.AGENT_LIBRARY_REPAIR) as agent_reporter:
|
|
273
|
+
result = invoke_library_builder_agent(
|
|
274
|
+
analysis,
|
|
275
|
+
result,
|
|
276
|
+
workspace,
|
|
277
|
+
tool=agent,
|
|
278
|
+
environment=result.environment,
|
|
279
|
+
)
|
|
280
|
+
agent_reporter.succeed() if result.succeeded else agent_reporter.fail()
|
|
281
|
+
return result
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def build_harness( # noqa: PLR0913 -- public API; all params are distinct required/optional inputs
|
|
285
|
+
analysis: AnalysisResult,
|
|
286
|
+
install_dir: Path,
|
|
287
|
+
workspace: Path,
|
|
288
|
+
library_result: BuildExplorationResult,
|
|
289
|
+
executor: EnvironmentExecutor,
|
|
290
|
+
*,
|
|
291
|
+
agent: str | None = None,
|
|
292
|
+
quiet: bool = False,
|
|
293
|
+
logs_dir: Path | None = None,
|
|
294
|
+
parameters: BuildParameters | None = None,
|
|
295
|
+
) -> HarnessExplorationResult:
|
|
296
|
+
"""Probe harness compilation, then fall back to an LLM agent if it failed.
|
|
297
|
+
|
|
298
|
+
library_result's extra_include_paths/extra_library_paths, if the library-build agent
|
|
299
|
+
reported any, are threaded into the probe. The deterministic probe and the agent repair
|
|
300
|
+
get their own phase banners. logs_dir, when given, is where the probe's raw output goes.
|
|
301
|
+
"""
|
|
302
|
+
from fuzzprep.library_builder.build_parameters import BuildParameters
|
|
303
|
+
|
|
304
|
+
parameters = parameters or BuildParameters.defaults()
|
|
305
|
+
log_name = f"{Phase.HARNESS_COMPILE_PROBE.value}.log"
|
|
306
|
+
deterministic_log_path = logs_dir / log_name if logs_dir else None
|
|
307
|
+
with PhaseReporter(Phase.HARNESS_COMPILE_PROBE) as reporter:
|
|
308
|
+
with (
|
|
309
|
+
parameters.harness_environment(),
|
|
310
|
+
streaming_context(quiet=quiet, log_path=deterministic_log_path),
|
|
311
|
+
):
|
|
312
|
+
result = executor.run_harness_compile(
|
|
313
|
+
install_dir,
|
|
314
|
+
workspace,
|
|
315
|
+
analysis.language,
|
|
316
|
+
extra_include_paths=library_result.extra_include_paths,
|
|
317
|
+
extra_library_paths=library_result.extra_library_paths,
|
|
318
|
+
parameters=parameters,
|
|
319
|
+
library_llm_used=library_result.llm_used,
|
|
320
|
+
)
|
|
321
|
+
reporter.succeed() if result.succeeded else reporter.fail()
|
|
322
|
+
|
|
323
|
+
if result.succeeded or agent is None:
|
|
324
|
+
return result
|
|
325
|
+
|
|
326
|
+
from fuzzprep.library_builder.agents import invoke_harness_builder_agent
|
|
327
|
+
from fuzzprep.library_builder.models import HarnessPaths
|
|
328
|
+
|
|
329
|
+
with PhaseReporter(Phase.AGENT_HARNESS_REPAIR) as agent_reporter:
|
|
330
|
+
result = invoke_harness_builder_agent(
|
|
331
|
+
analysis,
|
|
332
|
+
result,
|
|
333
|
+
HarnessPaths(install_dir=install_dir, workdir=workspace),
|
|
334
|
+
tool=agent,
|
|
335
|
+
environment=result.environment,
|
|
336
|
+
)
|
|
337
|
+
agent_reporter.succeed() if result.succeeded else agent_reporter.fail()
|
|
338
|
+
return result
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _ingest_source(args: argparse.Namespace, state_dir: Path) -> RepoSource | str:
|
|
342
|
+
"""Clone or resolve the repository, returning its source path or an error message
|
|
343
|
+
on failure (the caller wraps this in the INGESTION phase's diagnostic)."""
|
|
344
|
+
from fuzzprep.core.repos import (
|
|
345
|
+
CloneFailedError,
|
|
346
|
+
InvalidProjectNameError,
|
|
347
|
+
LocalRepoRefError,
|
|
348
|
+
NoCloneableOriginError,
|
|
349
|
+
RepositoryNotFoundError,
|
|
350
|
+
ingest_local,
|
|
351
|
+
ingest_url,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
if _is_url(args.repo_url):
|
|
356
|
+
return ingest_url(
|
|
357
|
+
args.repo_url,
|
|
358
|
+
project_name=args.project_name,
|
|
359
|
+
repo_ref=args.repo_ref,
|
|
360
|
+
state_dir=state_dir,
|
|
361
|
+
)
|
|
362
|
+
return ingest_local(
|
|
363
|
+
Path(args.repo_url),
|
|
364
|
+
project_name=args.project_name,
|
|
365
|
+
repo_ref=args.repo_ref,
|
|
366
|
+
state_dir=state_dir,
|
|
367
|
+
)
|
|
368
|
+
except RepositoryNotFoundError as exc:
|
|
369
|
+
return f"Repository not found: {exc}"
|
|
370
|
+
except (CloneFailedError, InvalidProjectNameError, LocalRepoRefError) as exc:
|
|
371
|
+
return str(exc)
|
|
372
|
+
except NoCloneableOriginError:
|
|
373
|
+
return (
|
|
374
|
+
"No cloneable git origin found. Provide a URL instead of a local path,"
|
|
375
|
+
" or add a remote origin."
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _resolve_output_path(args: argparse.Namespace, analysis: AnalysisResult) -> Path | None:
|
|
380
|
+
"""The output directory for this run, or None if the user declined to overwrite it.
|
|
381
|
+
|
|
382
|
+
Resolves and confirms only. Nothing is deleted until generation is ready to write, so a
|
|
383
|
+
run that fails partway leaves the previous, working output intact.
|
|
384
|
+
"""
|
|
385
|
+
output_path = Path(args.output) if args.output else Path.cwd() / "output"
|
|
386
|
+
output_path = output_path / analysis.project_name
|
|
387
|
+
if not output_path.exists():
|
|
388
|
+
return output_path
|
|
389
|
+
if sys.stdin.isatty():
|
|
390
|
+
if input(f"Output directory {output_path} already exists. Overwrite? (y/n) ") != "y":
|
|
391
|
+
return None
|
|
392
|
+
else:
|
|
393
|
+
print(f"Output directory {output_path} already exists, overwriting ...")
|
|
394
|
+
return output_path
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _run_library_phase( # noqa: PLR0913 -- private helper; all params are distinct required inputs
|
|
398
|
+
analysis: AnalysisResult,
|
|
399
|
+
workspace: Path,
|
|
400
|
+
agent: str | None,
|
|
401
|
+
state: DependencyState,
|
|
402
|
+
state_file: Path,
|
|
403
|
+
executor: EnvironmentExecutor,
|
|
404
|
+
*,
|
|
405
|
+
quiet: bool,
|
|
406
|
+
logs_dir: Path | None,
|
|
407
|
+
parameters: BuildParameters,
|
|
408
|
+
) -> BuildExplorationResult:
|
|
409
|
+
"""Build the library, persisting any packages the library-build agent reported missing."""
|
|
410
|
+
from fuzzprep.library_builder import dependency_resolution
|
|
411
|
+
from fuzzprep.library_builder.dependency_resolution import DependencySource
|
|
412
|
+
|
|
413
|
+
result = build_library(
|
|
414
|
+
analysis,
|
|
415
|
+
workspace,
|
|
416
|
+
executor,
|
|
417
|
+
agent=agent,
|
|
418
|
+
quiet=quiet,
|
|
419
|
+
logs_dir=logs_dir,
|
|
420
|
+
parameters=parameters,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
if result.missing_apt_packages:
|
|
424
|
+
dependencies = dependency_resolution.from_agent_report(
|
|
425
|
+
[],
|
|
426
|
+
result.missing_apt_packages,
|
|
427
|
+
source=DependencySource.LIBRARY_AGENT,
|
|
428
|
+
)
|
|
429
|
+
dependency_resolution.merge(state, dependencies)
|
|
430
|
+
dependency_resolution.save_state(state_file, state)
|
|
431
|
+
|
|
432
|
+
return result
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _harness_failure_diagnostic(
|
|
436
|
+
harness_result: HarnessExplorationResult,
|
|
437
|
+
apt_hint_list: list[str],
|
|
438
|
+
log_path: Path | None,
|
|
439
|
+
) -> FailureDiagnostic:
|
|
440
|
+
"""Build the diagnostic for a failed harness compilation, which stops the run: the probe
|
|
441
|
+
is the only evidence that compile_harness.sh's link line works."""
|
|
442
|
+
phase = Phase.AGENT_HARNESS_REPAIR if harness_result.llm_used else Phase.HARNESS_COMPILE_PROBE
|
|
443
|
+
origin = "agent" if harness_result.llm_used else "deterministic"
|
|
444
|
+
step = _agent_step(harness_result) if harness_result.llm_used else "harness link probe"
|
|
445
|
+
if apt_hint_list:
|
|
446
|
+
libs = ", ".join(harness_result.missing_system_libs)
|
|
447
|
+
message = (
|
|
448
|
+
f"Missing system libraries: {libs}\n"
|
|
449
|
+
f" apt: {' '.join(apt_hint_list)}\n"
|
|
450
|
+
"Install these packages and re-run for a complete harness build."
|
|
451
|
+
)
|
|
452
|
+
elif harness_result.validation_errors:
|
|
453
|
+
message = _rejected_repair_message(harness_result)
|
|
454
|
+
elif harness_result.llm_used and harness_result.agent_summary:
|
|
455
|
+
message = harness_result.agent_summary
|
|
456
|
+
else:
|
|
457
|
+
message = summarize_message(harness_result.stderr or harness_result.stdout)
|
|
458
|
+
return build_diagnostic(
|
|
459
|
+
phase,
|
|
460
|
+
step=step,
|
|
461
|
+
message=message,
|
|
462
|
+
origin=origin,
|
|
463
|
+
log_path=log_path,
|
|
464
|
+
exit_code=harness_result.exit_code,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _rejected_repair_message(result: BuildExplorationResult | HarnessExplorationResult) -> str:
|
|
469
|
+
"""The message for a repair the agent reported as done and FuzzPrep rejected.
|
|
470
|
+
|
|
471
|
+
The agent's summary describes the failure it set out to fix, not why the run stopped, so
|
|
472
|
+
it appears as context under the artifact check that rejected it.
|
|
473
|
+
"""
|
|
474
|
+
detail = "\n".join(f" {error.strip()}" for error in result.validation_errors)
|
|
475
|
+
message = (
|
|
476
|
+
f"The agent reported success, but the artifacts it claims to have built "
|
|
477
|
+
f"are not there:\n{detail}"
|
|
478
|
+
)
|
|
479
|
+
if result.agent_summary:
|
|
480
|
+
message += f"\n Agent's own summary: {result.agent_summary}"
|
|
481
|
+
return message
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _agent_step(result: BuildExplorationResult | HarnessExplorationResult) -> str:
|
|
485
|
+
"""How to describe an agent attempt in a diagnostic, including why it stopped."""
|
|
486
|
+
from fuzzprep.library_builder.models import AgentStopReason
|
|
487
|
+
|
|
488
|
+
if result.agent_stop_reason is AgentStopReason.SERVICE_UNAVAILABLE:
|
|
489
|
+
return "LLM repair attempt (API unavailable — the build was never diagnosed; retry)"
|
|
490
|
+
if result.agent_stop_reason is AgentStopReason.BUDGET_LIMITED:
|
|
491
|
+
return "LLM repair attempt (usage/rate limit reached)"
|
|
492
|
+
if result.agent_stop_reason is AgentStopReason.ACTION_REQUIRED:
|
|
493
|
+
return "LLM repair attempt (action required)"
|
|
494
|
+
return "LLM repair attempt"
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _run_harness_phase( # noqa: PLR0913 -- private helper; all params are distinct required inputs
|
|
498
|
+
analysis: AnalysisResult,
|
|
499
|
+
install_dir: Path,
|
|
500
|
+
workspace: Path,
|
|
501
|
+
library_result: BuildExplorationResult,
|
|
502
|
+
agent: str | None,
|
|
503
|
+
state: DependencyState,
|
|
504
|
+
state_file: Path,
|
|
505
|
+
executor: EnvironmentExecutor,
|
|
506
|
+
*,
|
|
507
|
+
environment: Environment,
|
|
508
|
+
quiet: bool,
|
|
509
|
+
debug: bool,
|
|
510
|
+
logs_dir: Path | None,
|
|
511
|
+
parameters: BuildParameters,
|
|
512
|
+
) -> HarnessExplorationResult:
|
|
513
|
+
"""Probe harness compilation, persist any newly-discovered packages, and report status."""
|
|
514
|
+
from fuzzprep.library_builder import dependency_resolution
|
|
515
|
+
from fuzzprep.library_builder import workspace as workspace_layout
|
|
516
|
+
from fuzzprep.library_builder.dependency_resolution import DependencySource
|
|
517
|
+
from fuzzprep.library_builder.environments.base import Environment
|
|
518
|
+
|
|
519
|
+
harness_result = build_harness(
|
|
520
|
+
analysis,
|
|
521
|
+
install_dir,
|
|
522
|
+
workspace,
|
|
523
|
+
library_result,
|
|
524
|
+
executor,
|
|
525
|
+
agent=agent,
|
|
526
|
+
quiet=quiet,
|
|
527
|
+
logs_dir=logs_dir,
|
|
528
|
+
parameters=parameters,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# Covers both the libs the linker reported missing and the ones it resolved silently
|
|
532
|
+
# because the exploration host already had them.
|
|
533
|
+
linker_deps = dependency_resolution.from_deterministic_probe(
|
|
534
|
+
harness_result.missing_system_libs, harness_result.transitive_link_flags
|
|
535
|
+
)
|
|
536
|
+
# The harness-build agent names apt packages itself, bypassing the translation table.
|
|
537
|
+
harness_agent_deps = dependency_resolution.from_agent_report(
|
|
538
|
+
[],
|
|
539
|
+
harness_result.missing_apt_packages,
|
|
540
|
+
source=DependencySource.HARNESS_AGENT,
|
|
541
|
+
)
|
|
542
|
+
if linker_deps or harness_agent_deps:
|
|
543
|
+
dependency_resolution.merge(state, linker_deps + harness_agent_deps)
|
|
544
|
+
dependency_resolution.save_state(state_file, state)
|
|
545
|
+
|
|
546
|
+
unknown_names = [
|
|
547
|
+
dep.name for dep in linker_deps if dep.name is not None and dep.apt_package is None
|
|
548
|
+
]
|
|
549
|
+
if unknown_names:
|
|
550
|
+
print(
|
|
551
|
+
f"Warning: no known apt package mapping for: {', '.join(unknown_names)}. "
|
|
552
|
+
"Install these manually before building elsewhere.",
|
|
553
|
+
file=sys.stderr,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
if environment is Environment.OSS_FUZZ:
|
|
557
|
+
# The Dockerfile was written before this phase's discoveries existed, and generation
|
|
558
|
+
# copies it rather than re-rendering it, so merge them in now.
|
|
559
|
+
workspace_layout.inject_apt_packages(workspace, state.apt_packages)
|
|
560
|
+
|
|
561
|
+
if harness_result.succeeded:
|
|
562
|
+
print("Successfully produced harness compilation!")
|
|
563
|
+
return harness_result
|
|
564
|
+
|
|
565
|
+
apt_hint_list = list(
|
|
566
|
+
dict.fromkeys(
|
|
567
|
+
[dep.apt_package for dep in linker_deps if dep.apt_package is not None]
|
|
568
|
+
+ harness_result.missing_apt_packages
|
|
569
|
+
)
|
|
570
|
+
)
|
|
571
|
+
log_path = (
|
|
572
|
+
harness_result.transcript_path
|
|
573
|
+
if harness_result.llm_used
|
|
574
|
+
else (logs_dir / f"{Phase.HARNESS_COMPILE_PROBE.value}.log" if logs_dir else None)
|
|
575
|
+
)
|
|
576
|
+
diagnostic = _harness_failure_diagnostic(harness_result, apt_hint_list, log_path)
|
|
577
|
+
print(
|
|
578
|
+
format_diagnostic(diagnostic, debug=debug, raw_output=harness_result.stdout),
|
|
579
|
+
file=sys.stderr,
|
|
580
|
+
)
|
|
581
|
+
print(
|
|
582
|
+
f"No output directory was generated. The library build artifacts are still in "
|
|
583
|
+
f"{workspace / 'install'} if you want to debug the link line there.",
|
|
584
|
+
file=sys.stderr,
|
|
585
|
+
)
|
|
586
|
+
return harness_result
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _verify_shipped_dockerfile(workspace: Path, project_name: str, *, quiet: bool) -> bool:
|
|
590
|
+
"""Build the workspace Dockerfile with nothing mounted and run OSS-Fuzz's `compile`.
|
|
591
|
+
|
|
592
|
+
The last step before generation for an oss-fuzz target. Everything before it ran with the
|
|
593
|
+
workspace mounted, which is what makes the artifacts reachable — and also what lets a
|
|
594
|
+
broken clone or apt layer in the Dockerfile pass unnoticed.
|
|
595
|
+
"""
|
|
596
|
+
from fuzzprep.library_builder.environments import verification
|
|
597
|
+
|
|
598
|
+
with PhaseReporter(Phase.DOCKERFILE_VERIFICATION) as reporter:
|
|
599
|
+
with streaming_context(quiet=quiet):
|
|
600
|
+
result = verification.run_from_scratch_docker_verification(
|
|
601
|
+
workspace, project_name=project_name
|
|
602
|
+
)
|
|
603
|
+
reporter.succeed() if result.passed else reporter.fail()
|
|
604
|
+
return result.passed
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _generate_outputs(workspace: Path, output_path: Path, inputs: GenerationInputs) -> int:
|
|
608
|
+
"""Write the output directory, reporting what landed where."""
|
|
609
|
+
from fuzzprep.library_builder.generation import MissingInstallTreeError, generate
|
|
610
|
+
|
|
611
|
+
if output_path.exists():
|
|
612
|
+
# Deferred until now, not done when the path was confirmed, so a run that fails
|
|
613
|
+
# partway leaves the previous output in place.
|
|
614
|
+
shutil.rmtree(output_path)
|
|
615
|
+
try:
|
|
616
|
+
result = generate(workspace, output_path, inputs)
|
|
617
|
+
except MissingInstallTreeError as exc:
|
|
618
|
+
# The half-written directory goes: publishing is all-or-nothing, and a partial one
|
|
619
|
+
# reads as a usable project to anything that only checks that it exists.
|
|
620
|
+
shutil.rmtree(output_path, ignore_errors=True)
|
|
621
|
+
print(f"Output generation failed: {exc}", file=sys.stderr)
|
|
622
|
+
return 1
|
|
623
|
+
|
|
624
|
+
print(f"Environment: {inputs.environment.value}")
|
|
625
|
+
print(f"Output: {result.output_path}")
|
|
626
|
+
verification_command = _command_str(inputs.harness.command or inputs.build.command)
|
|
627
|
+
if verification_command is not None:
|
|
628
|
+
print(f"Verified with: {verification_command}")
|
|
629
|
+
if (output_path / "compile_commands.json").exists():
|
|
630
|
+
print(f"Compile commands: {output_path / 'compile_commands.json'}")
|
|
631
|
+
return 0
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _command_str(command: list[str]) -> str | None:
|
|
635
|
+
"""Render a verification command's argv, so the report records the literal command that
|
|
636
|
+
confirmed success or failure."""
|
|
637
|
+
return " ".join(command) if command else None
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _write_run_stats( # noqa: PLR0913 -- private helper; every param is a distinct record field
|
|
641
|
+
stats_path: Path,
|
|
642
|
+
workdir: Path,
|
|
643
|
+
start_time: float,
|
|
644
|
+
build_result: BuildExplorationResult,
|
|
645
|
+
harness_result: HarnessExplorationResult | None,
|
|
646
|
+
status: RunStatus,
|
|
647
|
+
environment: Environment,
|
|
648
|
+
parameters: BuildParameters,
|
|
649
|
+
*,
|
|
650
|
+
bypass_scratch_validation: bool = False,
|
|
651
|
+
) -> None:
|
|
652
|
+
"""Build and persist stats.json for this run, successful or not.
|
|
653
|
+
|
|
654
|
+
The compile-commands path is resolved from workdir rather than read off a result, so it
|
|
655
|
+
records the capture the gate's build actually left behind -- including on the lane where a
|
|
656
|
+
repair agent produced that build.
|
|
657
|
+
"""
|
|
658
|
+
from fuzzprep.library_builder.stats import (
|
|
659
|
+
RunStats,
|
|
660
|
+
agent_phase_stats,
|
|
661
|
+
not_invoked_agent_stats,
|
|
662
|
+
write_run_stats,
|
|
663
|
+
)
|
|
664
|
+
from fuzzprep.library_builder.workspace import find_compile_commands
|
|
665
|
+
|
|
666
|
+
stats_path.parent.mkdir(parents=True, exist_ok=True)
|
|
667
|
+
command = (harness_result.command if harness_result else None) or build_result.command
|
|
668
|
+
compile_commands = find_compile_commands(workdir)
|
|
669
|
+
write_run_stats(
|
|
670
|
+
stats_path,
|
|
671
|
+
RunStats(
|
|
672
|
+
total_duration_seconds=time.monotonic() - start_time,
|
|
673
|
+
library_build_agent=agent_phase_stats(build_result),
|
|
674
|
+
harness_build_agent=(
|
|
675
|
+
agent_phase_stats(harness_result)
|
|
676
|
+
if harness_result is not None
|
|
677
|
+
else not_invoked_agent_stats()
|
|
678
|
+
),
|
|
679
|
+
status=status,
|
|
680
|
+
environment=environment,
|
|
681
|
+
compile_commands_path=str(compile_commands) if compile_commands else None,
|
|
682
|
+
verification_command=_command_str(command),
|
|
683
|
+
build_parameters=parameters.to_dict(),
|
|
684
|
+
scratch_validation_bypassed=bypass_scratch_validation,
|
|
685
|
+
),
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _finish_generate_run( # noqa: PLR0913 -- private helper; every param is a distinct record field
|
|
690
|
+
rc: int,
|
|
691
|
+
stats_path: Path,
|
|
692
|
+
workdir: Path,
|
|
693
|
+
output_path: Path,
|
|
694
|
+
start_time: float,
|
|
695
|
+
build_result: BuildExplorationResult,
|
|
696
|
+
harness_result: HarnessExplorationResult,
|
|
697
|
+
environment: Environment,
|
|
698
|
+
parameters: BuildParameters,
|
|
699
|
+
*,
|
|
700
|
+
bypass_scratch_validation: bool = False,
|
|
701
|
+
) -> int:
|
|
702
|
+
"""Record the run's outcome and report it, returning the exit code to hand back.
|
|
703
|
+
|
|
704
|
+
The output copy of stats.json is only made when generation succeeded: a failed one removes
|
|
705
|
+
the directory it was writing, so there is nothing left to put it in.
|
|
706
|
+
"""
|
|
707
|
+
from fuzzprep.library_builder.stats import RunStatus
|
|
708
|
+
|
|
709
|
+
status = RunStatus.SUCCESS if rc == 0 else RunStatus.FAILED_OUTPUT_GENERATION
|
|
710
|
+
_write_run_stats(
|
|
711
|
+
stats_path,
|
|
712
|
+
workdir,
|
|
713
|
+
start_time,
|
|
714
|
+
build_result,
|
|
715
|
+
harness_result,
|
|
716
|
+
status,
|
|
717
|
+
environment,
|
|
718
|
+
parameters,
|
|
719
|
+
bypass_scratch_validation=bypass_scratch_validation,
|
|
720
|
+
)
|
|
721
|
+
if rc == 0:
|
|
722
|
+
shutil.copy2(stats_path, output_path / "stats.json")
|
|
723
|
+
_print_run_summary(status)
|
|
724
|
+
return rc
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _select_executor(
|
|
728
|
+
environment: Environment, base_image: str | None, *, bypass_scratch_validation: bool = False
|
|
729
|
+
) -> EnvironmentExecutor:
|
|
730
|
+
from fuzzprep.library_builder.environments.base import Environment
|
|
731
|
+
from fuzzprep.library_builder.environments.local import LocalExecutor
|
|
732
|
+
from fuzzprep.library_builder.environments.oss_fuzz import OssFuzzExecutor
|
|
733
|
+
|
|
734
|
+
if environment is Environment.OSS_FUZZ:
|
|
735
|
+
return OssFuzzExecutor(
|
|
736
|
+
base_image=base_image, bypass_scratch_validation=bypass_scratch_validation
|
|
737
|
+
)
|
|
738
|
+
return LocalExecutor(base_image=base_image, bypass_scratch_validation=bypass_scratch_validation)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _check_environment_availability(
|
|
742
|
+
executor: EnvironmentExecutor, environment: Environment, parameters: BuildParameters
|
|
743
|
+
) -> int | None:
|
|
744
|
+
"""Return an exit code if the environment is unavailable, else None to proceed."""
|
|
745
|
+
from fuzzprep.library_builder.environments.base import EnvironmentUnavailableError
|
|
746
|
+
|
|
747
|
+
try:
|
|
748
|
+
executor.check_availability(parameters)
|
|
749
|
+
except EnvironmentUnavailableError as exc:
|
|
750
|
+
print(
|
|
751
|
+
format_startup_failure(f"Environment '{environment.value}' is unavailable: {exc}"),
|
|
752
|
+
file=sys.stderr,
|
|
753
|
+
)
|
|
754
|
+
return 1
|
|
755
|
+
return None
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _report_library_build_failure(
|
|
759
|
+
result: BuildExplorationResult, *, debug: bool, log_path: Path | None
|
|
760
|
+
) -> None:
|
|
761
|
+
"""Print the diagnostic for a failed library build — deterministic or post-agent.
|
|
762
|
+
|
|
763
|
+
One printer for one control path: a failed library build always stops the run, whether no
|
|
764
|
+
agent was armed, the repair did not hold, or the agent stopped for a person to act.
|
|
765
|
+
"""
|
|
766
|
+
phase = Phase.AGENT_LIBRARY_REPAIR if result.llm_used else Phase.DETERMINISTIC_LIBRARY_BUILD
|
|
767
|
+
origin = "agent" if result.llm_used else "deterministic"
|
|
768
|
+
step = _agent_step(result) if result.llm_used else "deterministic build command"
|
|
769
|
+
if result.validation_errors:
|
|
770
|
+
message = _rejected_repair_message(result)
|
|
771
|
+
elif result.llm_used and result.agent_summary:
|
|
772
|
+
message = result.agent_summary
|
|
773
|
+
else:
|
|
774
|
+
message = summarize_message(result.stdout)
|
|
775
|
+
diagnostic = build_diagnostic(
|
|
776
|
+
phase,
|
|
777
|
+
step=step,
|
|
778
|
+
message=message,
|
|
779
|
+
origin=origin,
|
|
780
|
+
log_path=log_path,
|
|
781
|
+
exit_code=result.exit_code,
|
|
782
|
+
)
|
|
783
|
+
print(format_diagnostic(diagnostic, debug=debug, raw_output=result.stdout), file=sys.stderr)
|
|
784
|
+
if result.command:
|
|
785
|
+
print(f"Reproduce with: {_command_str(result.command)}", file=sys.stderr)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _report_library_build_success() -> None:
|
|
789
|
+
print("Successfully produced library build!")
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _report_compile_commands(workdir: Path, build_system: BuildSystem) -> None:
|
|
793
|
+
"""Report the capture, after the gate rather than after the library phase.
|
|
794
|
+
|
|
795
|
+
Reported here because the gate rebuilds from nothing and is the last thing to write the
|
|
796
|
+
file. Reported at the end of the library phase instead, it could only ever describe a build
|
|
797
|
+
the gate was about to redo -- and on the repair lane it had nothing to describe at all.
|
|
798
|
+
"""
|
|
799
|
+
from fuzzprep.library_builder.exploration import compile_commands_absent_reason
|
|
800
|
+
from fuzzprep.library_builder.workspace import find_compile_commands
|
|
801
|
+
|
|
802
|
+
captured = find_compile_commands(workdir)
|
|
803
|
+
if captured is not None:
|
|
804
|
+
print(f"Compile commands: {captured}")
|
|
805
|
+
else:
|
|
806
|
+
print(f"Compile commands: not captured ({compile_commands_absent_reason(build_system)})")
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _print_run_summary(status: RunStatus) -> None:
|
|
810
|
+
"""Print a final, unambiguous overall-outcome line.
|
|
811
|
+
|
|
812
|
+
Without it, the last thing printed is the output-generation banner, which only reports
|
|
813
|
+
that writing the files succeeded.
|
|
814
|
+
"""
|
|
815
|
+
from fuzzprep.library_builder.stats import RunStatus
|
|
816
|
+
|
|
817
|
+
if status is RunStatus.SUCCESS:
|
|
818
|
+
print("Overall: SUCCESS")
|
|
819
|
+
return
|
|
820
|
+
reason = {
|
|
821
|
+
RunStatus.FAILED_LIBRARY_BUILD: "library build failed",
|
|
822
|
+
RunStatus.FAILED_HARNESS_BUILD: "harness compile probe failed",
|
|
823
|
+
RunStatus.FAILED_DOCKERFILE_VERIFICATION: (
|
|
824
|
+
"the generated Dockerfile did not build from scratch"
|
|
825
|
+
),
|
|
826
|
+
RunStatus.FAILED_OUTPUT_GENERATION: "the output directory could not be published",
|
|
827
|
+
}[status]
|
|
828
|
+
print(f"Overall: FAILED ({reason} — see diagnostic above)", file=sys.stderr)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _print_run_configuration(
|
|
832
|
+
environment: Environment, agent: str | None, parameters: BuildParameters
|
|
833
|
+
) -> None:
|
|
834
|
+
"""Announce what this run will do before it starts doing it.
|
|
835
|
+
|
|
836
|
+
The agent backend is named because a default run may call a paid network service, and the
|
|
837
|
+
configure options because they change what gets built and are otherwise only visible
|
|
838
|
+
inside the generated script.
|
|
839
|
+
"""
|
|
840
|
+
print(f"Environment: {environment.value}")
|
|
841
|
+
print(f"Agent: {agent or 'none (--no-agents)'}")
|
|
842
|
+
if parameters.library_configure_args:
|
|
843
|
+
print(f"Configure: {' '.join(parameters.library_configure_args)}")
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _print_analysis(analysis: AnalysisResult) -> None:
|
|
847
|
+
print(f"Build system: {analysis.build_system.value}")
|
|
848
|
+
print(f"Language: {analysis.language.value}")
|
|
849
|
+
for warning in analysis.warnings:
|
|
850
|
+
print(f"Warning: {warning}", file=sys.stderr)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _cmd_generate(args: argparse.Namespace) -> int:
|
|
854
|
+
from fuzzprep.core.paths import (
|
|
855
|
+
default_state_dir,
|
|
856
|
+
project_dir,
|
|
857
|
+
project_logs_dir,
|
|
858
|
+
project_state_file,
|
|
859
|
+
)
|
|
860
|
+
from fuzzprep.library_builder import dependency_resolution
|
|
861
|
+
from fuzzprep.library_builder.analysis import UnsupportedRepositoryError, analyze
|
|
862
|
+
from fuzzprep.library_builder.build_parameters import BuildParameters
|
|
863
|
+
from fuzzprep.library_builder.environments.base import Environment
|
|
864
|
+
from fuzzprep.library_builder.generation import GenerationInputs
|
|
865
|
+
from fuzzprep.library_builder.stats import RunStatus
|
|
866
|
+
|
|
867
|
+
start_time = time.monotonic()
|
|
868
|
+
state_dir = default_state_dir()
|
|
869
|
+
environment = Environment(args.environment)
|
|
870
|
+
bypass_scratch_validation = args.bypass_scratch_validation
|
|
871
|
+
executor = _select_executor(
|
|
872
|
+
environment, args.base_image, bypass_scratch_validation=bypass_scratch_validation
|
|
873
|
+
)
|
|
874
|
+
quiet = args.quiet
|
|
875
|
+
debug = args.log_level == "debug"
|
|
876
|
+
parameters = BuildParameters.from_args(args)
|
|
877
|
+
agent = None if args.no_agents else args.agent
|
|
878
|
+
|
|
879
|
+
_print_run_configuration(environment, agent, parameters)
|
|
880
|
+
availability_rc = _check_environment_availability(executor, environment, parameters)
|
|
881
|
+
if availability_rc is not None:
|
|
882
|
+
return availability_rc
|
|
883
|
+
|
|
884
|
+
with PhaseReporter(Phase.INGESTION) as reporter:
|
|
885
|
+
source = _ingest_source(args, state_dir)
|
|
886
|
+
if isinstance(source, str):
|
|
887
|
+
reporter.fail()
|
|
888
|
+
print(
|
|
889
|
+
format_diagnostic(
|
|
890
|
+
build_diagnostic(
|
|
891
|
+
Phase.INGESTION,
|
|
892
|
+
step="repository resolution",
|
|
893
|
+
message=source,
|
|
894
|
+
origin="deterministic",
|
|
895
|
+
),
|
|
896
|
+
debug=debug,
|
|
897
|
+
),
|
|
898
|
+
file=sys.stderr,
|
|
899
|
+
)
|
|
900
|
+
return 1
|
|
901
|
+
reporter.succeed()
|
|
902
|
+
|
|
903
|
+
with PhaseReporter(Phase.STATIC_ANALYSIS) as reporter:
|
|
904
|
+
try:
|
|
905
|
+
analysis = analyze(source)
|
|
906
|
+
except UnsupportedRepositoryError:
|
|
907
|
+
reporter.fail()
|
|
908
|
+
print(
|
|
909
|
+
format_diagnostic(
|
|
910
|
+
build_diagnostic(
|
|
911
|
+
Phase.STATIC_ANALYSIS,
|
|
912
|
+
step="build-system detection",
|
|
913
|
+
message="No C/C++ build signals found in this repository.",
|
|
914
|
+
origin="deterministic",
|
|
915
|
+
),
|
|
916
|
+
debug=debug,
|
|
917
|
+
),
|
|
918
|
+
file=sys.stderr,
|
|
919
|
+
)
|
|
920
|
+
return 1
|
|
921
|
+
reporter.succeed()
|
|
922
|
+
_print_analysis(analysis)
|
|
923
|
+
|
|
924
|
+
output_path = _resolve_output_path(args, analysis)
|
|
925
|
+
if output_path is None:
|
|
926
|
+
print("Chose not to overwrite the existing output directory; nothing was written.")
|
|
927
|
+
return 1
|
|
928
|
+
|
|
929
|
+
workspace = project_dir(state_dir, analysis.project_name)
|
|
930
|
+
logs_dir = project_logs_dir(state_dir, analysis.project_name)
|
|
931
|
+
stats_path = workspace / "stats.json"
|
|
932
|
+
state_file = project_state_file(state_dir, analysis.project_name)
|
|
933
|
+
state = dependency_resolution.load_state(state_file)
|
|
934
|
+
|
|
935
|
+
build_result = _run_library_phase(
|
|
936
|
+
analysis,
|
|
937
|
+
workspace,
|
|
938
|
+
agent,
|
|
939
|
+
state,
|
|
940
|
+
state_file,
|
|
941
|
+
executor,
|
|
942
|
+
quiet=quiet,
|
|
943
|
+
logs_dir=logs_dir,
|
|
944
|
+
parameters=parameters,
|
|
945
|
+
)
|
|
946
|
+
if not build_result.succeeded:
|
|
947
|
+
_report_library_build_failure(
|
|
948
|
+
build_result,
|
|
949
|
+
debug=debug,
|
|
950
|
+
log_path=(
|
|
951
|
+
build_result.transcript_path
|
|
952
|
+
if build_result.llm_used
|
|
953
|
+
else logs_dir / f"{Phase.DETERMINISTIC_LIBRARY_BUILD.value}.log"
|
|
954
|
+
),
|
|
955
|
+
)
|
|
956
|
+
_write_run_stats(
|
|
957
|
+
stats_path,
|
|
958
|
+
workspace,
|
|
959
|
+
start_time,
|
|
960
|
+
build_result,
|
|
961
|
+
None,
|
|
962
|
+
RunStatus.FAILED_LIBRARY_BUILD,
|
|
963
|
+
environment,
|
|
964
|
+
parameters,
|
|
965
|
+
bypass_scratch_validation=bypass_scratch_validation,
|
|
966
|
+
)
|
|
967
|
+
_print_run_summary(RunStatus.FAILED_LIBRARY_BUILD)
|
|
968
|
+
return 1
|
|
969
|
+
_report_library_build_success()
|
|
970
|
+
|
|
971
|
+
harness_result = _run_harness_phase(
|
|
972
|
+
analysis,
|
|
973
|
+
workspace / "install",
|
|
974
|
+
workspace,
|
|
975
|
+
build_result,
|
|
976
|
+
agent,
|
|
977
|
+
state,
|
|
978
|
+
state_file,
|
|
979
|
+
executor,
|
|
980
|
+
environment=environment,
|
|
981
|
+
quiet=quiet,
|
|
982
|
+
debug=debug,
|
|
983
|
+
logs_dir=logs_dir,
|
|
984
|
+
parameters=parameters,
|
|
985
|
+
)
|
|
986
|
+
if not harness_result.succeeded:
|
|
987
|
+
_write_run_stats(
|
|
988
|
+
stats_path,
|
|
989
|
+
workspace,
|
|
990
|
+
start_time,
|
|
991
|
+
build_result,
|
|
992
|
+
harness_result,
|
|
993
|
+
RunStatus.FAILED_HARNESS_BUILD,
|
|
994
|
+
environment,
|
|
995
|
+
parameters,
|
|
996
|
+
bypass_scratch_validation=bypass_scratch_validation,
|
|
997
|
+
)
|
|
998
|
+
_print_run_summary(RunStatus.FAILED_HARNESS_BUILD)
|
|
999
|
+
return 1
|
|
1000
|
+
_report_compile_commands(workspace, analysis.build_system)
|
|
1001
|
+
|
|
1002
|
+
if (
|
|
1003
|
+
environment is Environment.OSS_FUZZ
|
|
1004
|
+
and not bypass_scratch_validation
|
|
1005
|
+
and not _verify_shipped_dockerfile(workspace, analysis.project_name, quiet=quiet)
|
|
1006
|
+
):
|
|
1007
|
+
print(
|
|
1008
|
+
"The generated Dockerfile did not build and compile from scratch, so the "
|
|
1009
|
+
"oss-fuzz project would not work where it matters. Nothing was generated.",
|
|
1010
|
+
file=sys.stderr,
|
|
1011
|
+
)
|
|
1012
|
+
_write_run_stats(
|
|
1013
|
+
stats_path,
|
|
1014
|
+
workspace,
|
|
1015
|
+
start_time,
|
|
1016
|
+
build_result,
|
|
1017
|
+
harness_result,
|
|
1018
|
+
RunStatus.FAILED_DOCKERFILE_VERIFICATION,
|
|
1019
|
+
environment,
|
|
1020
|
+
parameters,
|
|
1021
|
+
bypass_scratch_validation=bypass_scratch_validation,
|
|
1022
|
+
)
|
|
1023
|
+
_print_run_summary(RunStatus.FAILED_DOCKERFILE_VERIFICATION)
|
|
1024
|
+
return 1
|
|
1025
|
+
|
|
1026
|
+
with PhaseReporter(Phase.OUTPUT_GENERATION) as reporter:
|
|
1027
|
+
with parameters.harness_environment():
|
|
1028
|
+
rc = _generate_outputs(
|
|
1029
|
+
workspace,
|
|
1030
|
+
output_path,
|
|
1031
|
+
GenerationInputs(
|
|
1032
|
+
analysis=analysis,
|
|
1033
|
+
build=build_result,
|
|
1034
|
+
harness=harness_result,
|
|
1035
|
+
system_packages=state.apt_packages,
|
|
1036
|
+
environment=environment,
|
|
1037
|
+
agent_backend=(
|
|
1038
|
+
agent if (build_result.llm_used or harness_result.llm_used) else None
|
|
1039
|
+
),
|
|
1040
|
+
scratch_validation_bypassed=bypass_scratch_validation,
|
|
1041
|
+
),
|
|
1042
|
+
)
|
|
1043
|
+
reporter.succeed() if rc == 0 else reporter.fail()
|
|
1044
|
+
|
|
1045
|
+
return _finish_generate_run(
|
|
1046
|
+
rc,
|
|
1047
|
+
stats_path,
|
|
1048
|
+
workspace,
|
|
1049
|
+
output_path,
|
|
1050
|
+
start_time,
|
|
1051
|
+
build_result,
|
|
1052
|
+
harness_result,
|
|
1053
|
+
environment,
|
|
1054
|
+
parameters,
|
|
1055
|
+
bypass_scratch_validation=bypass_scratch_validation,
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def _cmd_extract_features(args: argparse.Namespace) -> int:
|
|
1060
|
+
from fuzzprep.feature_extractor.extraction import (
|
|
1061
|
+
FeatureArtifactError,
|
|
1062
|
+
MissingCompileCommandsError,
|
|
1063
|
+
extract_features,
|
|
1064
|
+
)
|
|
1065
|
+
from fuzzprep.feature_extractor.native_build import NativeBuildError
|
|
1066
|
+
|
|
1067
|
+
build_path = Path(args.build_path)
|
|
1068
|
+
try:
|
|
1069
|
+
result = extract_features(build_path)
|
|
1070
|
+
except (MissingCompileCommandsError, NativeBuildError, FeatureArtifactError) as exc:
|
|
1071
|
+
print(str(exc), file=sys.stderr)
|
|
1072
|
+
return 1
|
|
1073
|
+
|
|
1074
|
+
print(
|
|
1075
|
+
f"Extracted {len(result.functions)} functions, {len(result.typedefs)} typedefs, "
|
|
1076
|
+
f"{len(result.macros)} macros, {len(result.enums)} enums, "
|
|
1077
|
+
f"{len(result.records)} records -> {build_path / 'features.json'}"
|
|
1078
|
+
)
|
|
1079
|
+
return 0
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _cmd_generate_benchmark(args: argparse.Namespace) -> int:
|
|
1083
|
+
from fuzzprep.feature_extractor.benchmark_yaml import generate_benchmark
|
|
1084
|
+
from fuzzprep.feature_extractor.extraction import (
|
|
1085
|
+
FeatureArtifactError,
|
|
1086
|
+
MissingFeatureArtifactError,
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
build_path = Path(args.build_path)
|
|
1090
|
+
try:
|
|
1091
|
+
benchmark = generate_benchmark(
|
|
1092
|
+
build_path,
|
|
1093
|
+
headers=args.headers,
|
|
1094
|
+
target_name=args.target_name,
|
|
1095
|
+
target_path=args.target_path,
|
|
1096
|
+
)
|
|
1097
|
+
except (MissingFeatureArtifactError, FeatureArtifactError) as exc:
|
|
1098
|
+
print(str(exc), file=sys.stderr)
|
|
1099
|
+
return 1
|
|
1100
|
+
|
|
1101
|
+
print(
|
|
1102
|
+
f"Generated benchmark for {len(benchmark.functions)} public functions -> "
|
|
1103
|
+
f"{build_path / f'{benchmark.project}.yaml'}"
|
|
1104
|
+
)
|
|
1105
|
+
return 0
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _is_url(value: str) -> bool:
|
|
1109
|
+
return value.startswith(("https://", "http://", "git://", "ssh://", "git@"))
|