d810-cobra 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 w00tzenheimer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ # Build inputs that live OUTSIDE the importable package.
2
+ #
3
+ # setuptools builds the sdist from packages + package-data, and package-data
4
+ # only reaches files inside a package. src/include and src/cpp are neither, so
5
+ # without these lines the sdist would omit the header and the C++ shim -- and
6
+ # `pip install d810-cobra` from source would fail at compile with a missing
7
+ # cobra_shim.h. The sdist IS published (deploy.yml build_sdist), so this is not
8
+ # hypothetical.
9
+ include LICENSE
10
+ include README.md
11
+ recursive-include src/include *.h
12
+ recursive-include src/cpp *.cpp
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: d810-cobra
3
+ Version: 0.1.0
4
+ Summary: CoBRA MBA-solver backend for d810 (mba-solve)
5
+ Author: w00tzenheimer
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/w00tzenheimer/d810-CoBRA
8
+ Project-URL: Issues, https://github.com/w00tzenheimer/d810-CoBRA/issues
9
+ Keywords: deobfuscation,mba,ida-pro,hex-rays,cobra
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Security
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: d810-ng>=0.6.6
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=8.0; extra == "test"
21
+ Dynamic: license-file
22
+
23
+ # d810-cobra
24
+
25
+ [![ci](https://github.com/w00tzenheimer/d810-CoBRA/actions/workflows/ci.yml/badge.svg)](https://github.com/w00tzenheimer/d810-CoBRA/actions/workflows/ci.yml)
26
+ [![python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](pyproject.toml)
27
+ [![license](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
28
+ [![d810-ng](https://img.shields.io/badge/d810--ng-backend-8A2BE2.svg)](https://github.com/w00tzenheimer/d810-ng)
29
+
30
+ CoBRA MBA-solver backend for [d810](https://github.com/w00tzenheimer/d810-ng) —
31
+ the `mba-solve` pass.
32
+
33
+ ```console
34
+ pip install d810-cobra
35
+ ```
36
+
37
+ That is the whole installation. d810 discovers this package automatically; no
38
+ configuration, no `COBRA_ROOT`, no CMake, no C++23 toolchain on the user's
39
+ machine.
40
+
41
+ ## Why it is a separate package
42
+
43
+ d810's 203 `mba-simplify` transforms are pattern-matched identities. On
44
+ coefficient-based linear MBA they fire zero times — measured, not assumed.
45
+ [CoBRA](https://github.com/trailofbits/CoBRA) is a signature-driven solver that
46
+ closes exactly that gap.
47
+
48
+ Shipping it inside d810 meant every d810 wheel carried a C++23 build of abseil,
49
+ highway and cobra-core, and CoBRA's version was pinned to a d810 commit. Split
50
+ out, d810's wheel stays pure and the two version independently.
51
+
52
+ ## How d810 finds it
53
+
54
+ One entry point, in the unversioned `d810.backends` group:
55
+
56
+ ```toml
57
+ [project.entry-points."d810.backends"]
58
+ cobra = "d810_cobra:MANIFEST"
59
+ ```
60
+
61
+ ```python
62
+ MANIFEST = {
63
+ "name": "cobra",
64
+ "api_version": 1,
65
+ "provides": "d810_cobra.solve",
66
+ "rules": ("d810_cobra.rules.cobra_solve",),
67
+ "implements": {"mba-solve": "CobraSolveRule"},
68
+ }
69
+ ```
70
+
71
+ `MANIFEST` is a plain dict — deliberately not d810's `BackendManifest`. The
72
+ package depends on d810 at runtime, but the *manifest* must not: importing
73
+ `BackendManifest` would turn "this d810 predates the plugin protocol" from a
74
+ clean "backend not discovered" into an ImportError during d810 startup.
75
+
76
+ Its `provides` is a *string*, resolved lazily, so a version-incompatible d810
77
+ rejects this backend after reading three fields — without importing `solve.py`
78
+ and therefore without loading the compiled extension.
79
+
80
+ `rules` and `implements` are what make the pass actually *run*, and each closes
81
+ a failure that is silent without it:
82
+
83
+ - **`rules`** — d810 registers its own optimizer rules by scanning
84
+ `d810.optimizers.__path__`. That scan is path-scoped and cannot reach a rule
85
+ living inside this package. Without declaring it, the backend reports
86
+ `available` while `CobraSolveRule` never registers — indistinguishable from a
87
+ pass that ran and matched nothing. d810 imports these only after the backend
88
+ probes usable, so a missing binding yields no rule rather than a rule that
89
+ raises on every call.
90
+ - **`implements`** — d810 derives a pass's `allowed_rule_names` from it at
91
+ registration time, long before rules are imported; a rule outside that
92
+ allowlist is skipped at dispatch. Declaring it here is what let d810 stop
93
+ hardcoding `"CobraSolveRule"` in its own source. The key is d810's pass id
94
+ (`d810.core.pass_ids.PassId.MBA_SOLVE`), written as a plain string so that
95
+ declaring a manifest still requires no d810 import — `PassId` is a
96
+ `StrEnum`, so the two compare and hash identically.
97
+
98
+ d810 itself is a hard dependency (`solve.py` uses `d810.core.getLogger`,
99
+ `table.py` uses `d810.core.cache`, `convert.py`/`detect.py` use
100
+ `d810.hexrays.*`). Some of those are d810 internals rather than a published
101
+ API, so a d810 refactor can break this package without either side bumping a
102
+ major version.
103
+
104
+ d810 ships no MBA solver of its own — `cobra` is not one of its builtin
105
+ backends — so this package supplies the `mba-solve` implementation rather than
106
+ overriding one:
107
+
108
+ ```
109
+ cobra available d810-cobra 0.1.0
110
+ ```
111
+
112
+ Check it with `d810cli backends`. Without this package installed, d810's
113
+ `mba-solve` pass resolves no implementation and contributes no stages.
114
+
115
+ ## Building from source
116
+
117
+ ```console
118
+ git clone --recursive https://github.com/w00tzenheimer/d810-CoBRA
119
+ cd d810-CoBRA
120
+ python tools/build_cobra.py # abseil + highway + cobra-core (CMake + Ninja)
121
+ pip install -e .
122
+ ```
123
+
124
+ `tools/build_cobra.py` needs CMake and Ninja, and a C++23 compiler. On Windows
125
+ it pins MSVC (`cl`) explicitly: `-G Ninja` with no compiler pinned picks
126
+ whatever is first in `PATH`, and a MinGW build produces `libabsl_*.a` archives
127
+ that an MSVC-built `.pyd` can neither `/WHOLEARCHIVE:` nor safely link against.
128
+
129
+ The build **fails loudly** rather than producing a package without the binding.
130
+ A wheel that installs cleanly and simplifies nothing is the failure mode this
131
+ package exists to make impossible.
132
+
133
+ ## Layout
134
+
135
+ | path | what |
136
+ |---|---|
137
+ | `src/d810_cobra/expr.py` | parse/evaluate/accept — pure data, no IDA |
138
+ | `src/d810_cobra/probe.py` | locate `cobra-cli`, or report a structured skip |
139
+ | `src/d810_cobra/solve.py` | the backend entry point d810 resolves |
140
+ | `src/d810_cobra/_cobra.pyx` | Cython binding over `cobra_shim.cpp` |
141
+ | `src/d810_cobra/rules/` | the `mba-solve` peephole rule (needs d810 + Hex-Rays) |
142
+ | `src/include/cobra_shim.h` | C ABI the binding compiles against |
143
+ | `src/cpp/cobra_shim.cpp` | C++ shim over cobra-core |
144
+ | `third_party/cobra` | pinned CoBRA submodule |
145
+
146
+ Headers and the C++ shim sit outside the package, mirroring d810's own
147
+ `src/include`. They are build inputs, so the wheel ships only what is
148
+ importable; `MANIFEST.in` is what carries them into the sdist. Note that
149
+ setuptools auto-includes a declared Extension *source* but never an
150
+ `include_dirs` header — drop `MANIFEST.in` and the sdist still contains
151
+ `cobra_shim.cpp` while silently losing `cobra_shim.h`.
152
+
153
+ Everything that touches `ida_hexrays` lives under `rules/`, so the solver core
154
+ stays unit-testable without IDA.
155
+
156
+ ## License
157
+
158
+ MIT. CoBRA itself is vendored as a submodule under its own license.
@@ -0,0 +1,136 @@
1
+ # d810-cobra
2
+
3
+ [![ci](https://github.com/w00tzenheimer/d810-CoBRA/actions/workflows/ci.yml/badge.svg)](https://github.com/w00tzenheimer/d810-CoBRA/actions/workflows/ci.yml)
4
+ [![python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](pyproject.toml)
5
+ [![license](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
6
+ [![d810-ng](https://img.shields.io/badge/d810--ng-backend-8A2BE2.svg)](https://github.com/w00tzenheimer/d810-ng)
7
+
8
+ CoBRA MBA-solver backend for [d810](https://github.com/w00tzenheimer/d810-ng) —
9
+ the `mba-solve` pass.
10
+
11
+ ```console
12
+ pip install d810-cobra
13
+ ```
14
+
15
+ That is the whole installation. d810 discovers this package automatically; no
16
+ configuration, no `COBRA_ROOT`, no CMake, no C++23 toolchain on the user's
17
+ machine.
18
+
19
+ ## Why it is a separate package
20
+
21
+ d810's 203 `mba-simplify` transforms are pattern-matched identities. On
22
+ coefficient-based linear MBA they fire zero times — measured, not assumed.
23
+ [CoBRA](https://github.com/trailofbits/CoBRA) is a signature-driven solver that
24
+ closes exactly that gap.
25
+
26
+ Shipping it inside d810 meant every d810 wheel carried a C++23 build of abseil,
27
+ highway and cobra-core, and CoBRA's version was pinned to a d810 commit. Split
28
+ out, d810's wheel stays pure and the two version independently.
29
+
30
+ ## How d810 finds it
31
+
32
+ One entry point, in the unversioned `d810.backends` group:
33
+
34
+ ```toml
35
+ [project.entry-points."d810.backends"]
36
+ cobra = "d810_cobra:MANIFEST"
37
+ ```
38
+
39
+ ```python
40
+ MANIFEST = {
41
+ "name": "cobra",
42
+ "api_version": 1,
43
+ "provides": "d810_cobra.solve",
44
+ "rules": ("d810_cobra.rules.cobra_solve",),
45
+ "implements": {"mba-solve": "CobraSolveRule"},
46
+ }
47
+ ```
48
+
49
+ `MANIFEST` is a plain dict — deliberately not d810's `BackendManifest`. The
50
+ package depends on d810 at runtime, but the *manifest* must not: importing
51
+ `BackendManifest` would turn "this d810 predates the plugin protocol" from a
52
+ clean "backend not discovered" into an ImportError during d810 startup.
53
+
54
+ Its `provides` is a *string*, resolved lazily, so a version-incompatible d810
55
+ rejects this backend after reading three fields — without importing `solve.py`
56
+ and therefore without loading the compiled extension.
57
+
58
+ `rules` and `implements` are what make the pass actually *run*, and each closes
59
+ a failure that is silent without it:
60
+
61
+ - **`rules`** — d810 registers its own optimizer rules by scanning
62
+ `d810.optimizers.__path__`. That scan is path-scoped and cannot reach a rule
63
+ living inside this package. Without declaring it, the backend reports
64
+ `available` while `CobraSolveRule` never registers — indistinguishable from a
65
+ pass that ran and matched nothing. d810 imports these only after the backend
66
+ probes usable, so a missing binding yields no rule rather than a rule that
67
+ raises on every call.
68
+ - **`implements`** — d810 derives a pass's `allowed_rule_names` from it at
69
+ registration time, long before rules are imported; a rule outside that
70
+ allowlist is skipped at dispatch. Declaring it here is what let d810 stop
71
+ hardcoding `"CobraSolveRule"` in its own source. The key is d810's pass id
72
+ (`d810.core.pass_ids.PassId.MBA_SOLVE`), written as a plain string so that
73
+ declaring a manifest still requires no d810 import — `PassId` is a
74
+ `StrEnum`, so the two compare and hash identically.
75
+
76
+ d810 itself is a hard dependency (`solve.py` uses `d810.core.getLogger`,
77
+ `table.py` uses `d810.core.cache`, `convert.py`/`detect.py` use
78
+ `d810.hexrays.*`). Some of those are d810 internals rather than a published
79
+ API, so a d810 refactor can break this package without either side bumping a
80
+ major version.
81
+
82
+ d810 ships no MBA solver of its own — `cobra` is not one of its builtin
83
+ backends — so this package supplies the `mba-solve` implementation rather than
84
+ overriding one:
85
+
86
+ ```
87
+ cobra available d810-cobra 0.1.0
88
+ ```
89
+
90
+ Check it with `d810cli backends`. Without this package installed, d810's
91
+ `mba-solve` pass resolves no implementation and contributes no stages.
92
+
93
+ ## Building from source
94
+
95
+ ```console
96
+ git clone --recursive https://github.com/w00tzenheimer/d810-CoBRA
97
+ cd d810-CoBRA
98
+ python tools/build_cobra.py # abseil + highway + cobra-core (CMake + Ninja)
99
+ pip install -e .
100
+ ```
101
+
102
+ `tools/build_cobra.py` needs CMake and Ninja, and a C++23 compiler. On Windows
103
+ it pins MSVC (`cl`) explicitly: `-G Ninja` with no compiler pinned picks
104
+ whatever is first in `PATH`, and a MinGW build produces `libabsl_*.a` archives
105
+ that an MSVC-built `.pyd` can neither `/WHOLEARCHIVE:` nor safely link against.
106
+
107
+ The build **fails loudly** rather than producing a package without the binding.
108
+ A wheel that installs cleanly and simplifies nothing is the failure mode this
109
+ package exists to make impossible.
110
+
111
+ ## Layout
112
+
113
+ | path | what |
114
+ |---|---|
115
+ | `src/d810_cobra/expr.py` | parse/evaluate/accept — pure data, no IDA |
116
+ | `src/d810_cobra/probe.py` | locate `cobra-cli`, or report a structured skip |
117
+ | `src/d810_cobra/solve.py` | the backend entry point d810 resolves |
118
+ | `src/d810_cobra/_cobra.pyx` | Cython binding over `cobra_shim.cpp` |
119
+ | `src/d810_cobra/rules/` | the `mba-solve` peephole rule (needs d810 + Hex-Rays) |
120
+ | `src/include/cobra_shim.h` | C ABI the binding compiles against |
121
+ | `src/cpp/cobra_shim.cpp` | C++ shim over cobra-core |
122
+ | `third_party/cobra` | pinned CoBRA submodule |
123
+
124
+ Headers and the C++ shim sit outside the package, mirroring d810's own
125
+ `src/include`. They are build inputs, so the wheel ships only what is
126
+ importable; `MANIFEST.in` is what carries them into the sdist. Note that
127
+ setuptools auto-includes a declared Extension *source* but never an
128
+ `include_dirs` header — drop `MANIFEST.in` and the sdist still contains
129
+ `cobra_shim.cpp` while silently losing `cobra_shim.h`.
130
+
131
+ Everything that touches `ida_hexrays` lives under `rules/`, so the solver core
132
+ stays unit-testable without IDA.
133
+
134
+ ## License
135
+
136
+ MIT. CoBRA itself is vendored as a submodule under its own license.
@@ -0,0 +1,65 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.1", "wheel", "Cython>=3.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ # Must match the PyPI Trusted Publisher registration exactly.
7
+ name = "d810-cobra"
8
+ version = "0.1.0"
9
+ description = "CoBRA MBA-solver backend for d810 (mba-solve)"
10
+ readme = "README.md"
11
+ requires-python = ">= 3.10"
12
+ license = "MIT"
13
+ authors = [{ name = "w00tzenheimer" }]
14
+ keywords = ["deobfuscation", "mba", "ida-pro", "hex-rays", "cobra"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Security",
21
+ ]
22
+
23
+ # d810 IS a hard runtime dependency, including for the entry-point target:
24
+ # solve.py imports d810.core.getLogger, table.py needs d810.core.cache.CacheImpl
25
+ # and d810.core.typing, convert.py/detect.py need d810.hexrays.*. Only `expr`
26
+ # and `probe` are genuinely standalone.
27
+ #
28
+ # Not circular: d810 never imports this package, it only discovers it through
29
+ # the entry point.
30
+ #
31
+ # Caveat worth knowing: several of these are d810 INTERNALS rather than a
32
+ # published API (d810.core.cache, d810.core.typing), so a d810 refactor can
33
+ # break this package without a major version bump on either side.
34
+ dependencies = ["d810-ng>=0.6.6"]
35
+
36
+ [project.optional-dependencies]
37
+ test = ["pytest>=8.0"]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/w00tzenheimer/d810-CoBRA"
41
+ Issues = "https://github.com/w00tzenheimer/d810-CoBRA/issues"
42
+
43
+ # This is the whole integration surface. d810's BackendRegistry scans this
44
+ # group before its builtins, so an installed copy of this package supersedes
45
+ # anything d810 ships under the same name. The group is deliberately
46
+ # unversioned -- the compatibility contract lives in the manifest's
47
+ # api_version, not in the entry-point name.
48
+ [project.entry-points."d810.backends"]
49
+ cobra = "d810_cobra:MANIFEST"
50
+
51
+ [tool.setuptools]
52
+ package-dir = { "" = "src" }
53
+
54
+ [tool.setuptools.packages.find]
55
+ where = ["src"]
56
+
57
+ [tool.setuptools.package-data]
58
+ # cobra_shim.h is no longer here: it moved to src/include, matching d810's own
59
+ # layout. It reaches the sdist through MANIFEST.in instead -- package-data only
60
+ # covers files inside an importable package, and a header is a build input, not
61
+ # something a wheel consumer imports.
62
+ d810_cobra = ["*.pyx", "*.pxd"]
63
+
64
+ [tool.pytest.ini_options]
65
+ testpaths = ["tests/unit"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,174 @@
1
+ """Build the CoBRA solver binding.
2
+
3
+ Unlike d810, where this extension was optional and the wheel had to remain
4
+ installable without it, the binding is the entire point of this package. So a
5
+ build that cannot produce it FAILS rather than quietly yielding an inert
6
+ install -- the failure mode we spent a long time chasing in d810 was exactly a
7
+ wheel that installed cleanly and simplified nothing.
8
+
9
+ The C++ side is built by ``tools/build_cobra.py`` (CMake + Ninja), which
10
+ produces the layout consumed here.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import pathlib
17
+ import platform
18
+
19
+ from setuptools import setup
20
+
21
+ OSTYPE = platform.system()
22
+ HERE = pathlib.Path(__file__).parent.resolve()
23
+
24
+ #: Layout mirrors d810 itself, which keeps shared C headers in ``src/include``
25
+ #: rather than inside the importable package. Both ``_cobra.pyx`` and
26
+ #: ``cobra_shim.cpp`` include the header by bare name, so this must be on the
27
+ #: include path for either to compile.
28
+ #:
29
+ #: Keeping them out of ``src/d810_cobra`` means the wheel ships only what is
30
+ #: importable: the header and the C++ shim are build inputs, and a consumer who
31
+ #: pip-installs a built wheel has no use for either.
32
+ SRC = HERE / "src"
33
+ INCLUDE_DIR = SRC / "include"
34
+
35
+
36
+ def _first_existing(root: pathlib.Path, candidates: tuple[str, ...]):
37
+ for rel in candidates:
38
+ candidate = root / rel
39
+ if candidate.exists():
40
+ return candidate
41
+ return None
42
+
43
+
44
+ def get_cobra_ext_modules():
45
+ try:
46
+ from Cython.Build import cythonize
47
+ from setuptools import Extension
48
+ except ImportError: # pragma: no cover - build-time only
49
+ raise ImportError("Cython is required to build the CoBRA binding")
50
+
51
+ # Default to the vendored submodule so `pip install -e .` works in a
52
+ # checkout with no environment set up. CI overrides this for the flat
53
+ # artifact layout.
54
+ cobra_root = os.environ.get("COBRA_ROOT") or str(HERE / "third_party" / "cobra")
55
+ root = pathlib.Path(cobra_root)
56
+ if not root.is_dir():
57
+ raise RuntimeError(
58
+ f"COBRA_ROOT={root} does not exist. Either init the submodule\n"
59
+ " git submodule update --init --recursive\n"
60
+ "and run python tools/build_cobra.py , or point COBRA_ROOT at a\n"
61
+ "prebuilt cobra-core bundle."
62
+ )
63
+
64
+ # TWO accepted layouts. The flat one exists so a released/CI static-lib
65
+ # bundle can be consumed directly -- requiring callers to reshape a
66
+ # download into a fake build tree is pure friction, and reshaping inside CI
67
+ # just relocates it.
68
+ #
69
+ # FLAT (release / CI artifact): <root>/lib/*.{a,lib} <root>/include/
70
+ # BUILD TREE (local checkout): <root>/build/lib/core/
71
+ # <root>/build-deps/install/{lib*,include}/
72
+ flat_lib = root / "lib"
73
+ flat_inc = root / "include"
74
+ is_flat = flat_lib.is_dir() and any(flat_lib.glob("*cobra-core*"))
75
+
76
+ if is_flat:
77
+ include_dirs = [str(INCLUDE_DIR), str(flat_inc)]
78
+ library_dirs = [str(flat_lib)]
79
+ else:
80
+ # Pick ONE dependency prefix and one core build; globbing several and
81
+ # merging them silently mixes incompatible trees.
82
+ deps_prefix = _first_existing(
83
+ root, ("build-deps/install", "build-deps-nollvm/install")
84
+ )
85
+ core_dir = _first_existing(root, ("build/lib/core", "build-nollvm/lib/core"))
86
+ if deps_prefix is None or core_dir is None:
87
+ raise RuntimeError(
88
+ f"COBRA_ROOT={root} does not look built. Accepted layouts:\n"
89
+ f" flat: {root}/lib/*cobra-core* + {root}/include/\n"
90
+ f" build tree: {root}/build/lib/core + {root}/build-deps/install\n"
91
+ "Run: python tools/build_cobra.py"
92
+ )
93
+ include_dirs = [
94
+ str(INCLUDE_DIR),
95
+ str(root / "include"),
96
+ str(deps_prefix / "include"),
97
+ ]
98
+ # lib vs lib64: manylinux is RHEL-based, where CMAKE_INSTALL_LIBDIR
99
+ # defaults to lib64, so never hardcode "lib".
100
+ library_dirs = [str(core_dir)] + [
101
+ str(p) for p in sorted(deps_prefix.glob("lib*")) if p.is_dir()
102
+ ]
103
+
104
+ # CoBRA requires C++23 (Result.h uses std::expected).
105
+ std_args = ["/std:c++latest"] if OSTYPE == "Windows" else ["-std=c++23"]
106
+
107
+ # abseil scatters constants and singletons (container_internal::kSooControl,
108
+ # MixingHashState::kSeed, Mutex, Now, ...) across archive members that
109
+ # nothing else references, so a normal -l link never pulls them in. Python
110
+ # extensions link with -undefined dynamic_lookup, so this does NOT fail the
111
+ # link -- it fails later at dlopen with "symbol not found in flat
112
+ # namespace", which is much harder to diagnose. Force every abseil archive
113
+ # in and let the linker dead-strip the remainder.
114
+ #
115
+ # MSVC emits absl_base.lib; Unix toolchains emit libabsl_base.a. Globbing
116
+ # only the Unix shape silently yields NOTHING on Windows and the force-load
117
+ # never happens -- again a load-time failure, not a link-time one. An empty
118
+ # match is therefore fatal.
119
+ _absl_pattern = "absl_*.lib" if OSTYPE == "Windows" else "libabsl_*.a"
120
+ absl_archives = [
121
+ str(p) for d in library_dirs for p in sorted(pathlib.Path(d).glob(_absl_pattern))
122
+ ]
123
+ if not absl_archives:
124
+ raise RuntimeError(
125
+ f"no abseil archives matched {_absl_pattern!r} under {library_dirs}. "
126
+ "Without force-loading them the extension links but fails at import "
127
+ "with missing absl symbols; refusing to build a broken binding."
128
+ )
129
+ if OSTYPE == "Darwin":
130
+ link_args = [f"-Wl,-force_load,{a}" for a in absl_archives]
131
+ link_args.append("-Wl,-dead_strip")
132
+ elif OSTYPE == "Linux":
133
+ link_args = [
134
+ "-Wl,--whole-archive",
135
+ *absl_archives,
136
+ "-Wl,--no-whole-archive",
137
+ "-Wl,--gc-sections",
138
+ ]
139
+ else:
140
+ link_args = [f"/WHOLEARCHIVE:{a}" for a in absl_archives]
141
+
142
+ return cythonize(
143
+ Extension(
144
+ "d810_cobra._cobra",
145
+ [
146
+ "src/d810_cobra/_cobra.pyx",
147
+ "src/cpp/cobra_shim.cpp",
148
+ ],
149
+ language="c++",
150
+ include_dirs=include_dirs,
151
+ library_dirs=library_dirs,
152
+ libraries=["cobra-core"],
153
+ extra_compile_args=std_args,
154
+ extra_link_args=link_args,
155
+ ),
156
+ compiler_directives={"language_level": "3", "binding": True},
157
+ )
158
+
159
+
160
+ #: Building an sdist compiles nothing -- it packs sources -- but setuptools
161
+ #: evaluates ``ext_modules`` on EVERY PEP 517 hook, including
162
+ #: ``get_requires_for_build_sdist``, which runs before anything is packed. So
163
+ #: ``python -m build --sdist`` demanded a fully built cobra-core just to produce
164
+ #: a tarball, and deploy.yml's build_sdist job (which has no CoBRA build step by
165
+ #: design) died in 8s with the "does not look built" error.
166
+ #:
167
+ #: Deliberately opt-IN, rather than "return [] whenever CoBRA is missing": the
168
+ #: latter would let a *wheel* build silently produce a package with no binding.
169
+ #: That is precisely the failure this package refuses -- a wheel that installs
170
+ #: cleanly and simplifies nothing. Skipping the extension must require someone
171
+ #: to say so explicitly, and only for the one artifact that cannot contain it.
172
+ SDIST_ONLY = os.environ.get("D810_COBRA_SDIST_ONLY") == "1"
173
+
174
+ setup(ext_modules=[] if SDIST_ONLY else get_cobra_ext_modules())