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.
Files changed (56) hide show
  1. fuzzprep/__init__.py +3 -0
  2. fuzzprep/__main__.py +4 -0
  3. fuzzprep/agents/harness_builder/SKILL.md +156 -0
  4. fuzzprep/agents/library_builder/SKILL.md +147 -0
  5. fuzzprep/agents/scripts/check_build.sh +87 -0
  6. fuzzprep/agents/scripts/check_build_in_container.sh +73 -0
  7. fuzzprep/agents/scripts/check_dockerfile_from_scratch.sh +33 -0
  8. fuzzprep/cli.py +1109 -0
  9. fuzzprep/core/__init__.py +0 -0
  10. fuzzprep/core/agent_stream.py +304 -0
  11. fuzzprep/core/files.py +35 -0
  12. fuzzprep/core/paths.py +25 -0
  13. fuzzprep/core/reporting.py +218 -0
  14. fuzzprep/core/repos.py +217 -0
  15. fuzzprep/core/resources.py +41 -0
  16. fuzzprep/core/subprocesses.py +197 -0
  17. fuzzprep/feature_extractor/__init__.py +0 -0
  18. fuzzprep/feature_extractor/benchmark_yaml.py +92 -0
  19. fuzzprep/feature_extractor/extraction.py +184 -0
  20. fuzzprep/feature_extractor/models.py +96 -0
  21. fuzzprep/feature_extractor/native/.clang-format +1 -0
  22. fuzzprep/feature_extractor/native/CMakeLists.txt +90 -0
  23. fuzzprep/feature_extractor/native/include/feature_extractor.hpp +136 -0
  24. fuzzprep/feature_extractor/native/src/extraction_action.cpp +294 -0
  25. fuzzprep/feature_extractor/native/src/json_writer.cpp +154 -0
  26. fuzzprep/feature_extractor/native/src/macro_callbacks.cpp +75 -0
  27. fuzzprep/feature_extractor/native/src/main.cpp +130 -0
  28. fuzzprep/feature_extractor/native_build.py +99 -0
  29. fuzzprep/library_builder/__init__.py +0 -0
  30. fuzzprep/library_builder/agents.py +472 -0
  31. fuzzprep/library_builder/analysis.py +145 -0
  32. fuzzprep/library_builder/build_parameters.py +158 -0
  33. fuzzprep/library_builder/dependency_resolution.py +139 -0
  34. fuzzprep/library_builder/environments/__init__.py +0 -0
  35. fuzzprep/library_builder/environments/base.py +89 -0
  36. fuzzprep/library_builder/environments/gate.py +96 -0
  37. fuzzprep/library_builder/environments/local.py +205 -0
  38. fuzzprep/library_builder/environments/oss_fuzz.py +485 -0
  39. fuzzprep/library_builder/environments/verification.py +125 -0
  40. fuzzprep/library_builder/exploration.py +217 -0
  41. fuzzprep/library_builder/generation.py +325 -0
  42. fuzzprep/library_builder/harness_explorer.py +257 -0
  43. fuzzprep/library_builder/models.py +169 -0
  44. fuzzprep/library_builder/package_names.json +33 -0
  45. fuzzprep/library_builder/package_names.py +40 -0
  46. fuzzprep/library_builder/scripts.py +389 -0
  47. fuzzprep/library_builder/stats.py +102 -0
  48. fuzzprep/library_builder/symbol_patterns.json +65 -0
  49. fuzzprep/library_builder/timeouts.py +14 -0
  50. fuzzprep/library_builder/workspace.py +250 -0
  51. fuzzprep-0.1.0.dist-info/METADATA +255 -0
  52. fuzzprep-0.1.0.dist-info/RECORD +56 -0
  53. fuzzprep-0.1.0.dist-info/WHEEL +4 -0
  54. fuzzprep-0.1.0.dist-info/entry_points.txt +3 -0
  55. fuzzprep-0.1.0.dist-info/licenses/LICENSE +202 -0
  56. 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
+ )