embtrace-check 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.
- embtrace_check/__init__.py +3 -0
- embtrace_check/analyzer/__init__.py +1 -0
- embtrace_check/analyzer/models.py +148 -0
- embtrace_check/analyzer/normalize.py +118 -0
- embtrace_check/analyzer/parsers/__init__.py +40 -0
- embtrace_check/analyzer/parsers/autotools.py +101 -0
- embtrace_check/analyzer/parsers/cargo.py +102 -0
- embtrace_check/analyzer/parsers/cmake.py +242 -0
- embtrace_check/analyzer/parsers/configure.py +139 -0
- embtrace_check/analyzer/parsers/gomod.py +54 -0
- embtrace_check/analyzer/parsers/gradle.py +97 -0
- embtrace_check/analyzer/parsers/makefile.py +86 -0
- embtrace_check/analyzer/parsers/maven.py +81 -0
- embtrace_check/analyzer/parsers/meson.py +125 -0
- embtrace_check/analyzer/parsers/npm.py +32 -0
- embtrace_check/analyzer/parsers/python.py +252 -0
- embtrace_check/analyzer/pipeline/__init__.py +162 -0
- embtrace_check/analyzer/pipeline/base.py +55 -0
- embtrace_check/analyzer/pipeline/merge.py +107 -0
- embtrace_check/analyzer/pipeline/tier1_cli.py +749 -0
- embtrace_check/analyzer/pipeline/tier2_structured.py +717 -0
- embtrace_check/analyzer/pipeline/tier4_regex.py +231 -0
- embtrace_check/analyzer/scanner.py +215 -0
- embtrace_check/cli.py +198 -0
- embtrace_check/collector.py +100 -0
- embtrace_check/core/__init__.py +1 -0
- embtrace_check/core/exceptions.py +228 -0
- embtrace_check/core/log.py +72 -0
- embtrace_check/payload.py +99 -0
- embtrace_check/sbom/__init__.py +1 -0
- embtrace_check/sbom/scanner.py +984 -0
- embtrace_check/upload.py +86 -0
- embtrace_check-0.1.0.dist-info/METADATA +123 -0
- embtrace_check-0.1.0.dist-info/RECORD +37 -0
- embtrace_check-0.1.0.dist-info/WHEEL +4 -0
- embtrace_check-0.1.0.dist-info/entry_points.txt +2 -0
- embtrace_check-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Vendored subset of the embtrace analyzer (deterministic tiers only)."""
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Pydantic models for AI project analyzer.
|
|
2
|
+
|
|
3
|
+
Covers LLM analysis output, reconciliation results, and the decisions.yaml
|
|
4
|
+
persistence format.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# LLM output models
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
class BuildFileDependency(BaseModel):
|
|
16
|
+
"""A dependency detected by LLM analysis of build files."""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
version: str = ""
|
|
20
|
+
ecosystem: str = ""
|
|
21
|
+
source_file: str = ""
|
|
22
|
+
source_line: int = 0
|
|
23
|
+
context: str = "" # LLM context, e.g. "inside if(USE_LIBRESSL)"
|
|
24
|
+
confidence: float = 1.0
|
|
25
|
+
tier: int = 0 # 0=LLM, 1-5=Pipeline tier
|
|
26
|
+
detection_method: str = "" # e.g. "cargo-metadata", "regex-cmake", "treesitter-meson"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BuildFileArtifact(BaseModel):
|
|
30
|
+
"""An artifact (binary, library, etc.) detected in build files."""
|
|
31
|
+
|
|
32
|
+
path: str
|
|
33
|
+
artifact_type: str = "binary" # binary | library | config | script
|
|
34
|
+
source_file: str = ""
|
|
35
|
+
source_line: int = 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BuildFileInternalDep(BaseModel):
|
|
39
|
+
"""An internal project dependency detected in build files."""
|
|
40
|
+
|
|
41
|
+
project: str
|
|
42
|
+
source_file: str = ""
|
|
43
|
+
source_line: int = 0
|
|
44
|
+
context: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LLMAnalysisResult(BaseModel):
|
|
48
|
+
"""Result of analyzing a single build file with the LLM."""
|
|
49
|
+
|
|
50
|
+
file_path: str
|
|
51
|
+
dependencies: list[BuildFileDependency] = []
|
|
52
|
+
artifacts: list[BuildFileArtifact] = []
|
|
53
|
+
internal_deps: list[BuildFileInternalDep] = []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Reconciliation result models
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
class ConfirmedEntry(BaseModel):
|
|
61
|
+
"""Both lockfile scanner and build-file analysis agree on this dependency."""
|
|
62
|
+
|
|
63
|
+
name: str
|
|
64
|
+
version: str
|
|
65
|
+
ecosystem: str = ""
|
|
66
|
+
source_file: str = ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class LockfileOnlyEntry(BaseModel):
|
|
70
|
+
"""Dependency found only in the lockfile scanner output."""
|
|
71
|
+
|
|
72
|
+
name: str
|
|
73
|
+
version: str
|
|
74
|
+
ecosystem: str = ""
|
|
75
|
+
accept: bool | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BuildFileOnlyEntry(BaseModel):
|
|
79
|
+
"""Dependency found only in LLM build-file analysis."""
|
|
80
|
+
|
|
81
|
+
name: str
|
|
82
|
+
version: str = ""
|
|
83
|
+
ecosystem: str = ""
|
|
84
|
+
source_file: str = ""
|
|
85
|
+
confidence: float = 1.0
|
|
86
|
+
context: str = ""
|
|
87
|
+
accept: bool | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ConflictEntry(BaseModel):
|
|
91
|
+
"""Version mismatch between lockfile and build-file analysis."""
|
|
92
|
+
|
|
93
|
+
name: str
|
|
94
|
+
lockfile_version: str
|
|
95
|
+
build_file_version: str
|
|
96
|
+
ecosystem: str = ""
|
|
97
|
+
source_file: str = ""
|
|
98
|
+
accept: bool | None = None
|
|
99
|
+
use_version: str = ""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ArtifactEntry(BaseModel):
|
|
103
|
+
"""A produced artifact detected in build files."""
|
|
104
|
+
|
|
105
|
+
path: str
|
|
106
|
+
artifact_type: str = "binary"
|
|
107
|
+
source_file: str = ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class InternalDepEntry(BaseModel):
|
|
111
|
+
"""An internal dependency detected in build files."""
|
|
112
|
+
|
|
113
|
+
project: str
|
|
114
|
+
source_file: str = ""
|
|
115
|
+
context: str = ""
|
|
116
|
+
accept: bool | None = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# decisions.yaml root model
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
class DecisionsFile(BaseModel):
|
|
124
|
+
"""Root model for .embtrace/proposal/decisions.yaml."""
|
|
125
|
+
|
|
126
|
+
scan_date: str
|
|
127
|
+
model: str = ""
|
|
128
|
+
confirmed: list[ConfirmedEntry] = []
|
|
129
|
+
lockfile_only: list[LockfileOnlyEntry] = []
|
|
130
|
+
build_file_only: list[BuildFileOnlyEntry] = []
|
|
131
|
+
conflicts: list[ConflictEntry] = []
|
|
132
|
+
artifacts: list[ArtifactEntry] = []
|
|
133
|
+
internal_dependencies: list[InternalDepEntry] = []
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Reconciliation container
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
class ReconciliationResult(BaseModel):
|
|
141
|
+
"""Container for reconciliation output — input to proposal generation."""
|
|
142
|
+
|
|
143
|
+
confirmed: list[ConfirmedEntry] = []
|
|
144
|
+
lockfile_only: list[LockfileOnlyEntry] = []
|
|
145
|
+
build_file_only: list[BuildFileOnlyEntry] = []
|
|
146
|
+
conflicts: list[ConflictEntry] = []
|
|
147
|
+
artifacts: list[ArtifactEntry] = []
|
|
148
|
+
internal_dependencies: list[InternalDepEntry] = []
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Shared dependency name normalization.
|
|
2
|
+
|
|
3
|
+
Consolidated from reconciler._normalize_name() and eval.normalize_dep_name()
|
|
4
|
+
to provide a single source of truth for name matching across the analyzer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
# Version suffix pattern: _0_3, _1_0, _2, _3_0, _2_11, etc.
|
|
12
|
+
# Applied AFTER separator normalization (- → _), so dots become _.
|
|
13
|
+
_VERSION_SUFFIX = re.compile(r"(?:_\d+)+$")
|
|
14
|
+
|
|
15
|
+
# Component suffixes commonly appended to pkg-config names
|
|
16
|
+
_COMPONENT_SUFFIXES = (
|
|
17
|
+
"_client", "_server", "_base", "_core", "_common",
|
|
18
|
+
"_egl", "_cursor", "_protocols", "_generic",
|
|
19
|
+
"_video", "_app", "_riff", "_pbutils", "_audio",
|
|
20
|
+
"_simple", "_allocators",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# CMake Find-module names often differ from pkg-config / canonical names.
|
|
24
|
+
# Map normalized aliases → canonical normalized name.
|
|
25
|
+
_ALIASES: dict[str, str] = {
|
|
26
|
+
# pkg-config → canonical
|
|
27
|
+
"cares": "c_ares",
|
|
28
|
+
"pulse": "pulseaudio",
|
|
29
|
+
"pulse_simple": "pulseaudio",
|
|
30
|
+
"avcodec": "ffmpeg",
|
|
31
|
+
"avformat": "ffmpeg",
|
|
32
|
+
"avutil": "ffmpeg",
|
|
33
|
+
"swscale": "ffmpeg",
|
|
34
|
+
"swresample": "ffmpeg",
|
|
35
|
+
"avdevice": "ffmpeg",
|
|
36
|
+
"avfilter": "ffmpeg",
|
|
37
|
+
"postproc": "ffmpeg",
|
|
38
|
+
"gtk2": "gtk",
|
|
39
|
+
"gtk3": "gtk",
|
|
40
|
+
"gtk4": "gtk",
|
|
41
|
+
"gtk_3": "gtk",
|
|
42
|
+
"gstreamer": "gstreamer",
|
|
43
|
+
"gstreamer_base": "gstreamer",
|
|
44
|
+
"gstreamer_video": "gstreamer",
|
|
45
|
+
"gstreamer_app": "gstreamer",
|
|
46
|
+
"gstreamer_riff": "gstreamer",
|
|
47
|
+
"gstreamer_pbutils": "gstreamer",
|
|
48
|
+
# CMake module variants
|
|
49
|
+
"dbus1": "dbus",
|
|
50
|
+
"dbus_1": "dbus",
|
|
51
|
+
"sdl2": "sdl",
|
|
52
|
+
"sdl3": "sdl",
|
|
53
|
+
"qt6widgets": "qt",
|
|
54
|
+
"qt5widgets": "qt",
|
|
55
|
+
"qt6core": "qt",
|
|
56
|
+
"qt5core": "qt",
|
|
57
|
+
"qt6gui": "qt",
|
|
58
|
+
"qt5gui": "qt",
|
|
59
|
+
"qt6": "qt",
|
|
60
|
+
"qt5": "qt",
|
|
61
|
+
"gtest": "googletest",
|
|
62
|
+
"gmock": "googletest",
|
|
63
|
+
"pythoninterp": "python",
|
|
64
|
+
"pythonlibs": "python",
|
|
65
|
+
"python3": "python",
|
|
66
|
+
"cudnn": "cudnn",
|
|
67
|
+
"cudatoolkit": "cuda",
|
|
68
|
+
"threads": "pthread",
|
|
69
|
+
"pthreads": "pthread",
|
|
70
|
+
# -l flag names → package names
|
|
71
|
+
"ssl": "openssl",
|
|
72
|
+
"crypto": "openssl",
|
|
73
|
+
"z": "zlib",
|
|
74
|
+
# OpenCV-specific
|
|
75
|
+
"onnxruntime": "onnx_runtime",
|
|
76
|
+
# Wayland components
|
|
77
|
+
"wayland_scanner": "wayland",
|
|
78
|
+
# glib variants (with and without version suffix)
|
|
79
|
+
"glib_2": "glib",
|
|
80
|
+
"gobject_2": "glib",
|
|
81
|
+
"gobject": "glib",
|
|
82
|
+
"gmodule": "glib",
|
|
83
|
+
"gmodule_2": "glib",
|
|
84
|
+
"gio_2": "glib",
|
|
85
|
+
"gio": "glib",
|
|
86
|
+
# mesa / graphics
|
|
87
|
+
"glslangvalidator": "glslang",
|
|
88
|
+
"sensors": "lmsensors",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def normalize_dep_name(name: str) -> str:
|
|
93
|
+
"""Normalize a dependency name for matching.
|
|
94
|
+
|
|
95
|
+
Lowercases, strips common prefixes (lib, python-, py-),
|
|
96
|
+
strips version suffixes and component suffixes from pkg-config names,
|
|
97
|
+
normalizes separators (-, ., +, space → _), and applies alias mappings.
|
|
98
|
+
"""
|
|
99
|
+
n = name.lower().strip()
|
|
100
|
+
# Strip common prefixes
|
|
101
|
+
for prefix in ("lib", "python-", "py-"):
|
|
102
|
+
if n.startswith(prefix) and len(n) > len(prefix) + 1:
|
|
103
|
+
n = n[len(prefix):]
|
|
104
|
+
break
|
|
105
|
+
# Remove + (gtk+ → gtk)
|
|
106
|
+
n = n.replace("+", "")
|
|
107
|
+
# Normalize separators
|
|
108
|
+
n = n.replace("-", "_").replace(".", "_").replace(" ", "_")
|
|
109
|
+
# Strip version suffixes (e.g. pipewire_0_3 → pipewire)
|
|
110
|
+
n = _VERSION_SUFFIX.sub("", n)
|
|
111
|
+
# Strip component suffixes (e.g. wayland_client → wayland)
|
|
112
|
+
for suffix in _COMPONENT_SUFFIXES:
|
|
113
|
+
if n.endswith(suffix) and len(n) > len(suffix):
|
|
114
|
+
n = n[: -len(suffix)]
|
|
115
|
+
break
|
|
116
|
+
# Apply alias mappings
|
|
117
|
+
n = _ALIASES.get(n, n)
|
|
118
|
+
return n
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Deterministic build-file parsers.
|
|
2
|
+
|
|
3
|
+
Each parser module exposes a ``parse(content: str) -> list[str]`` function
|
|
4
|
+
that extracts dependency names from a specific build-file format.
|
|
5
|
+
|
|
6
|
+
The ``PARSERS`` dict maps file_type → parse function, matching the types
|
|
7
|
+
used by ``scanner.BUILD_FILE_PATTERNS``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
|
|
14
|
+
from embtrace_check.analyzer.parsers.autotools import parse as parse_autotools
|
|
15
|
+
from embtrace_check.analyzer.parsers.cargo import parse as parse_cargo
|
|
16
|
+
from embtrace_check.analyzer.parsers.cmake import parse as parse_cmake
|
|
17
|
+
from embtrace_check.analyzer.parsers.configure import parse as parse_configure
|
|
18
|
+
from embtrace_check.analyzer.parsers.gomod import parse as parse_go
|
|
19
|
+
from embtrace_check.analyzer.parsers.gradle import parse as parse_gradle
|
|
20
|
+
from embtrace_check.analyzer.parsers.makefile import parse as parse_make
|
|
21
|
+
from embtrace_check.analyzer.parsers.maven import parse as parse_maven
|
|
22
|
+
from embtrace_check.analyzer.parsers.meson import parse as parse_meson
|
|
23
|
+
from embtrace_check.analyzer.parsers.npm import parse as parse_npm
|
|
24
|
+
from embtrace_check.analyzer.parsers.python import parse as parse_python
|
|
25
|
+
|
|
26
|
+
# Maps file_type (from scanner.BUILD_FILE_PATTERNS) → parser function
|
|
27
|
+
PARSERS: dict[str, Callable[[str], list[str]]] = {
|
|
28
|
+
"cmake": parse_cmake,
|
|
29
|
+
"meson": parse_meson,
|
|
30
|
+
"cargo": parse_cargo,
|
|
31
|
+
"go": parse_go,
|
|
32
|
+
"python": parse_python,
|
|
33
|
+
"npm": parse_npm,
|
|
34
|
+
"gradle": parse_gradle,
|
|
35
|
+
"maven": parse_maven,
|
|
36
|
+
"autotools": parse_autotools,
|
|
37
|
+
"configure": parse_configure,
|
|
38
|
+
"make": parse_make,
|
|
39
|
+
"conan": parse_cmake, # conanfile.py/txt handled separately; fallback to cmake
|
|
40
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Deterministic Autotools parser (configure.ac / configure.in).
|
|
2
|
+
|
|
3
|
+
Extracts dependency names from GNU Autotools build files:
|
|
4
|
+
- AC_CHECK_LIB([name], [func])
|
|
5
|
+
- AC_CHECK_HEADER([header.h])
|
|
6
|
+
- PKG_CHECK_MODULES([VAR], [name >= version])
|
|
7
|
+
- AC_SEARCH_LIBS([func], [lib1 lib2 ...])
|
|
8
|
+
- AX_CHECK_OPENSSL / AX_CHECK_ZLIB etc.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
# AC_CHECK_LIB([crypto], [SHA1_Init], ...)
|
|
16
|
+
# AC_CHECK_LIB(curl, curl_global_init, ...)
|
|
17
|
+
_AC_CHECK_LIB = re.compile(
|
|
18
|
+
r"AC_CHECK_LIB\s*\(\s*\[?(\w+)\]?",
|
|
19
|
+
re.IGNORECASE,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# PKG_CHECK_MODULES([DLT], ['automotive-dlt >= 2.11'])
|
|
23
|
+
# PKG_CHECK_MODULES(DLT, automotive-dlt >= 2.11)
|
|
24
|
+
_PKG_CHECK_MODULES = re.compile(
|
|
25
|
+
r"PKG_CHECK_MODULES\s*\(\s*\[?\w+\]?\s*,\s*\[?'?\"?([a-zA-Z0-9_][a-zA-Z0-9_.+-]*)",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# AC_SEARCH_LIBS([clock_gettime], [rt posix4])
|
|
29
|
+
# Also handles unbracketed: AC_SEARCH_LIBS(fmod, m)
|
|
30
|
+
_AC_SEARCH_LIBS = re.compile(
|
|
31
|
+
r"AC_SEARCH_LIBS\s*\(\s*\[?\w+\]?\s*,\s*\[?([^\],\)]+)",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# AC_CHECK_HEADER([header.h]) — not always a dependency, but useful
|
|
35
|
+
_AC_CHECK_HEADER = re.compile(
|
|
36
|
+
r"AC_CHECK_HEADER\s*\(\s*\[?([a-zA-Z0-9_/.+-]+\.h)\]?",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# AX_CHECK_OPENSSL, AX_CHECK_ZLIB, etc.
|
|
40
|
+
_AX_CHECK = re.compile(
|
|
41
|
+
r"AX_CHECK_(\w+)",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Header → library name mapping (common cases)
|
|
45
|
+
_HEADER_TO_LIB: dict[str, str] = {
|
|
46
|
+
"openssl/ssl.h": "openssl",
|
|
47
|
+
"openssl/crypto.h": "openssl",
|
|
48
|
+
"curl/curl.h": "libcurl",
|
|
49
|
+
"zlib.h": "zlib",
|
|
50
|
+
"expat.h": "libexpat",
|
|
51
|
+
"pcre2.h": "libpcre2",
|
|
52
|
+
"pcre.h": "libpcre",
|
|
53
|
+
"iconv.h": "libiconv",
|
|
54
|
+
"libintl.h": "libintl",
|
|
55
|
+
"pthread.h": "pthread",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _strip_comments(content: str) -> str:
|
|
60
|
+
"""Remove autotools comments (dnl ... and # ...)."""
|
|
61
|
+
content = re.sub(r"dnl\s+[^\n]*", "", content)
|
|
62
|
+
content = re.sub(r"#[^\n]*", "", content)
|
|
63
|
+
return content
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse(content: str) -> list[str]:
|
|
67
|
+
"""Extract dependency names from configure.ac content."""
|
|
68
|
+
content = _strip_comments(content)
|
|
69
|
+
deps: set[str] = set()
|
|
70
|
+
|
|
71
|
+
# AC_CHECK_LIB
|
|
72
|
+
for m in _AC_CHECK_LIB.finditer(content):
|
|
73
|
+
name = m.group(1)
|
|
74
|
+
# Prefix with lib if it's a short name (convention)
|
|
75
|
+
if len(name) > 1:
|
|
76
|
+
deps.add(name)
|
|
77
|
+
|
|
78
|
+
# PKG_CHECK_MODULES
|
|
79
|
+
for m in _PKG_CHECK_MODULES.finditer(content):
|
|
80
|
+
deps.add(m.group(1))
|
|
81
|
+
|
|
82
|
+
# AC_SEARCH_LIBS — extract each library name
|
|
83
|
+
for m in _AC_SEARCH_LIBS.finditer(content):
|
|
84
|
+
libs = m.group(1).split()
|
|
85
|
+
for lib in libs:
|
|
86
|
+
lib = lib.strip()
|
|
87
|
+
if lib:
|
|
88
|
+
deps.add(lib)
|
|
89
|
+
|
|
90
|
+
# AX_CHECK_*
|
|
91
|
+
for m in _AX_CHECK.finditer(content):
|
|
92
|
+
name = m.group(1).lower()
|
|
93
|
+
deps.add(name)
|
|
94
|
+
|
|
95
|
+
# AC_CHECK_HEADER — map known headers to library names
|
|
96
|
+
for m in _AC_CHECK_HEADER.finditer(content):
|
|
97
|
+
header = m.group(1)
|
|
98
|
+
if header in _HEADER_TO_LIB:
|
|
99
|
+
deps.add(_HEADER_TO_LIB[header])
|
|
100
|
+
|
|
101
|
+
return sorted(deps)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Deterministic Cargo.toml parser.
|
|
2
|
+
|
|
3
|
+
Extracts dependency names from Rust Cargo.toml by parsing:
|
|
4
|
+
- [dependencies]
|
|
5
|
+
- [dev-dependencies]
|
|
6
|
+
- [build-dependencies]
|
|
7
|
+
- [target.'cfg(...)'.dependencies]
|
|
8
|
+
|
|
9
|
+
Filters out path-only dependencies (workspace members).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
# Section headers
|
|
17
|
+
_SECTION = re.compile(r"^\[([^\]]+)\]", re.MULTILINE)
|
|
18
|
+
|
|
19
|
+
# Simple dependency: name = "version"
|
|
20
|
+
_SIMPLE_DEP = re.compile(r"^(\w[\w-]*)\s*=\s*\"([^\"]+)\"", re.MULTILINE)
|
|
21
|
+
|
|
22
|
+
# Table dependency: name = { version = "...", ... }
|
|
23
|
+
# Also matches: name = { path = "..." } (internal — filter later)
|
|
24
|
+
_TABLE_DEP = re.compile(
|
|
25
|
+
r"^(\w[\w-]*)\s*=\s*\{([^}]+)\}",
|
|
26
|
+
re.MULTILINE,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Workspace members in [workspace]
|
|
30
|
+
_WORKSPACE_MEMBERS = re.compile(
|
|
31
|
+
r"members\s*=\s*\[([^\]]+)\]",
|
|
32
|
+
re.DOTALL,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_dep_section(section: str) -> bool:
|
|
37
|
+
"""Check if a TOML section header is a dependency section."""
|
|
38
|
+
s = section.lower().strip()
|
|
39
|
+
return s.endswith("dependencies") and not s.startswith("workspace")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_PACKAGE_FIELD = re.compile(r'package\s*=\s*"([^"]+)"')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _extract_package_name(table_content: str) -> str | None:
|
|
46
|
+
"""Extract the actual package name from a ``package = "..."`` field."""
|
|
47
|
+
m = _PACKAGE_FIELD.search(table_content)
|
|
48
|
+
return m.group(1) if m else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _has_path_only(table_content: str) -> bool:
|
|
52
|
+
"""Check if a table-style dependency is path-only (internal)."""
|
|
53
|
+
has_path = "path" in table_content
|
|
54
|
+
has_version = "version" in table_content
|
|
55
|
+
has_git = "git" in table_content
|
|
56
|
+
has_workspace = re.search(r"workspace\s*=\s*true", table_content) is not None
|
|
57
|
+
# workspace = true without external source = workspace/internal dep
|
|
58
|
+
if has_workspace and not has_git:
|
|
59
|
+
return True
|
|
60
|
+
# path-only without version = workspace/internal dep
|
|
61
|
+
return has_path and not has_version and not has_git
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse(content: str) -> list[str]:
|
|
65
|
+
"""Extract dependency names from Cargo.toml content."""
|
|
66
|
+
deps: set[str] = set()
|
|
67
|
+
|
|
68
|
+
# Find all sections and their content ranges
|
|
69
|
+
sections: list[tuple[str, int, int]] = []
|
|
70
|
+
for m in _SECTION.finditer(content):
|
|
71
|
+
sections.append((m.group(1), m.end(), 0)) # end will be filled below
|
|
72
|
+
|
|
73
|
+
# Fill section end positions
|
|
74
|
+
filled: list[tuple[str, int, int]] = []
|
|
75
|
+
for i, (name, start, _) in enumerate(sections):
|
|
76
|
+
end = (
|
|
77
|
+
sections[i + 1][1] - len(sections[i + 1][0]) - 2
|
|
78
|
+
if i + 1 < len(sections) else len(content)
|
|
79
|
+
)
|
|
80
|
+
filled.append((name, start, end))
|
|
81
|
+
|
|
82
|
+
# Process each dependency section
|
|
83
|
+
for section_name, start, end in filled:
|
|
84
|
+
if not _is_dep_section(section_name):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
section_content = content[start:end]
|
|
88
|
+
|
|
89
|
+
# Simple deps: name = "version"
|
|
90
|
+
for m in _SIMPLE_DEP.finditer(section_content):
|
|
91
|
+
deps.add(m.group(1))
|
|
92
|
+
|
|
93
|
+
# Table deps: name = { version = "...", ... }
|
|
94
|
+
for m in _TABLE_DEP.finditer(section_content):
|
|
95
|
+
name = m.group(1)
|
|
96
|
+
table = m.group(2)
|
|
97
|
+
if not _has_path_only(table):
|
|
98
|
+
# Use actual package name if renamed (package = "real_name")
|
|
99
|
+
pkg = _extract_package_name(table)
|
|
100
|
+
deps.add(pkg if pkg else name)
|
|
101
|
+
|
|
102
|
+
return sorted(deps)
|