libmdr 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.
libmdr-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mos9527, Amr Satrio and other contributors
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.
libmdr-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: libmdr
3
+ Version: 0.1.0
4
+ Summary: Python bindings for the mdr-shared C ABI
5
+ License-Expression: MIT
6
+ Project-URL: Source, https://github.com/mos9527/libmdr-py
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/plain
9
+ License-File: LICENSE
10
+ Provides-Extra: dev
11
+ Provides-Extra: bt
12
+ Requires-Dist: libmdr-bt; extra == "bt"
13
+ Provides-Extra: cli
14
+ Requires-Dist: libmdr-cli; extra == "cli"
15
+ Dynamic: license-file
16
+
17
+ ctypes bindings for libmdr (mdr-shared).
libmdr-0.1.0/README.md ADDED
@@ -0,0 +1 @@
1
+ # libmdr
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "libmdr"
7
+ version = "0.1.0"
8
+ description = "Python bindings for the mdr-shared C ABI"
9
+ readme = { text = "ctypes bindings for libmdr (mdr-shared).", content-type = "text/plain" }
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+
14
+ [project.optional-dependencies]
15
+ dev = []
16
+ bt = ["libmdr-bt"]
17
+ cli = ["libmdr-cli"]
18
+
19
+ [project.urls]
20
+ Source = "https://github.com/mos9527/libmdr-py"
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
24
+
25
+ [tool.setuptools.package-data]
26
+ libmdr = ["_native/*"]
27
+
28
+ # Wheels are tagged py3-none-<platform> (see setup.py): the native library is
29
+ # loaded via ctypes, so it is built once per platform and cibuildwheel reuses
30
+ # that wheel to run the tests on every other selected interpreter.
31
+ [tool.cibuildwheel]
32
+ skip = ["*-musllinux_*", "cp31[1-9]*-manylinux_armv7l"] # armv7l runs under QEMU: test once
33
+ build-verbosity = 1
34
+ test-command = "python -c \"import libmdr, libmdr._dll as d; print(d.load())\""
35
+ # Ubuntu 22.04 based; manylinux_2_31 (Ubuntu 20.04) defaults to GCC 9, without <span>.
36
+ # The resulting wheels still only need glibc 2.17: auditwheel tags them accordingly.
37
+ manylinux-armv7l-image = "manylinux_2_35"
38
+
39
+ [tool.cibuildwheel.linux]
40
+ before-all = "sh {project}/tooling/ci/linux-before-all.sh"
41
+
42
+ [tool.cibuildwheel.macos]
43
+ environment = { MACOSX_DEPLOYMENT_TARGET = "12.0" }
44
+
45
+ [tool.cibuildwheel.windows]
46
+ # No Python extension modules; mdr-shared.dll links the CRT statically.
47
+ repair-wheel-command = ""
libmdr-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
libmdr-0.1.0/setup.py ADDED
@@ -0,0 +1,283 @@
1
+ """Build script for libmdr (also the setuptools entry point).
2
+
3
+ The CMake build helpers that previously lived in a separate ``cmake_build.py``
4
+ are inlined here. Like that module, this is build-time code and is NOT packaged
5
+ into the wheel; it runs only during ``build_py`` / ``editable_wheel`` to compile
6
+ and stage the native shared library into the package's ``_native`` directory.
7
+
8
+ C++ source resolution order:
9
+ 1. ``MDR_SOURCE_DIR`` env var (explicit override)
10
+ 2. ``<repo>/cpp`` (git submodule, preferred)
11
+ 3. ``<repo>/build/cpp-src`` (shallow fetch of ``_SOURCE_COMMIT`` on every build)
12
+
13
+ The third option lets ``pip install .`` / ``python -m build`` work from a
14
+ plain source checkout or sdist without the caller having to run
15
+ ``git submodule update --init`` first.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import configparser
21
+ import os
22
+ import re
23
+ import shutil
24
+ import stat
25
+ import subprocess
26
+ import sys
27
+ import time
28
+ from pathlib import Path
29
+
30
+ from setuptools import Distribution, setup
31
+ from setuptools.command.bdist_wheel import bdist_wheel as _bdist_wheel
32
+ from setuptools.command.build_py import build_py as _build_py
33
+ from setuptools.command.editable_wheel import editable_wheel as _editable_wheel
34
+ from setuptools.command.sdist import sdist as _sdist
35
+
36
+ # Python repo root (this file lives in <repo>/<pkg>/setup.py).
37
+ _PY_ROOT = Path(__file__).resolve().parents[1]
38
+ DEFAULT_BUILD_DIR = _PY_ROOT / "build" / "python"
39
+ # Fresh C++ source clone location (separate from the CMake build dir so the
40
+ # build dir can be wiped between builds without discarding the clone).
41
+ _CLONE_DIR = _PY_ROOT / "build" / "cpp-src"
42
+ # Preferred on-disk location of the C++ sources.
43
+ _SUBMODULE_DIR = _PY_ROOT / "cpp"
44
+ # Upstream coordinates, used only when fetching on demand. The URL is
45
+ # overridden by .gitmodules when present. Upstream has no releases yet, so the
46
+ # sources are pinned to a commit matching the generated bindings.
47
+ _SOURCE_URL = "https://github.com/mos9527/SonyHeadphonesClient.git"
48
+ _SOURCE_COMMIT = "2edf09098db27642c89e533bf86c0a6b162f6ad4"
49
+
50
+ # CMake target -> on-disk base name (OUTPUT_NAME set in the C++ CMakeLists).
51
+ _TARGET_BASENAMES = {
52
+ "mdr_shared": "mdr-shared",
53
+ "mdr-bt_shared": "mdr-bt-shared",
54
+ }
55
+ _SUFFIXES = (".dll", ".so", ".dylib")
56
+
57
+
58
+ def _cmake() -> str:
59
+ env = os.environ.get("CMAKE")
60
+ if env:
61
+ return env
62
+ found = shutil.which("cmake")
63
+ if found:
64
+ return found
65
+ raise RuntimeError("cmake not found on PATH; install CMake or set CMAKE.")
66
+
67
+
68
+ def _run(cmd: list[str]) -> None:
69
+ print("+", " ".join(cmd), flush=True)
70
+ subprocess.check_call(cmd)
71
+
72
+
73
+ def _read_gitmodules() -> str | None:
74
+ """Return the url of the 'cpp' submodule from .gitmodules, if present."""
75
+ gm = _PY_ROOT / ".gitmodules"
76
+ if not gm.is_file():
77
+ return None
78
+ cp = configparser.ConfigParser()
79
+ cp.read(gm, encoding="utf-8")
80
+ for section in cp.sections():
81
+ if cp.has_option(section, "path") and cp.get(section, "path") == "cpp":
82
+ return cp.get(section, "url", fallback=_SOURCE_URL)
83
+ return None
84
+
85
+
86
+ def _rmtree(path: Path) -> None:
87
+ """Remove a directory tree, tolerating read-only files (git marks .git
88
+ objects read-only on Windows) and transient file locks."""
89
+ if not path.exists():
90
+ return
91
+
92
+ def _clear_ro(func, p, exc): # noqa: ANN001
93
+ try:
94
+ os.chmod(p, stat.S_IWRITE)
95
+ func(p)
96
+ except OSError:
97
+ raise
98
+
99
+ last_err: OSError | None = None
100
+ for _ in range(3):
101
+ try:
102
+ if sys.version_info >= (3, 12):
103
+ shutil.rmtree(path, onexc=_clear_ro)
104
+ else:
105
+ shutil.rmtree(path, onerror=_clear_ro)
106
+ return
107
+ except OSError as exc:
108
+ last_err = exc
109
+ time.sleep(0.5)
110
+ if last_err:
111
+ raise last_err
112
+
113
+
114
+ def _clone_source(dest: Path) -> None:
115
+ url = _read_gitmodules() or _SOURCE_URL
116
+ _rmtree(dest)
117
+ dest.mkdir(parents=True, exist_ok=True)
118
+ _run(["git", "-C", str(dest), "init", "-q"])
119
+ _run(["git", "-C", str(dest), "fetch", "--depth", "1", url, _SOURCE_COMMIT])
120
+ _run(["git", "-C", str(dest), "checkout", "-q", "FETCH_HEAD"])
121
+
122
+
123
+ def _ensure_source() -> Path:
124
+ """Resolve the C++ source directory, cloning it into the build dir if needed."""
125
+ override = os.environ.get("MDR_SOURCE_DIR")
126
+ if override:
127
+ return Path(override)
128
+ if _SUBMODULE_DIR.is_dir() and any(_SUBMODULE_DIR.iterdir()):
129
+ return _SUBMODULE_DIR
130
+ clone_dir = _CLONE_DIR
131
+ print(f"cpp source not found at {_SUBMODULE_DIR}; fetching {_SOURCE_COMMIT} into {clone_dir}", flush=True)
132
+ _clone_source(clone_dir)
133
+ return clone_dir
134
+
135
+
136
+ def _config() -> str:
137
+ return os.environ.get("MDR_CMAKE_CONFIG", "MinSizeRel")
138
+
139
+
140
+ def _platform_args() -> list[str]:
141
+ args: list[str] = []
142
+ if sys.platform == "darwin":
143
+ # PEP 517 frontends / cibuildwheel request the target architecture(s)
144
+ # via ARCHFLAGS (e.g. "-arch x86_64"), which CMake does not read.
145
+ archs = re.findall(r"-arch\s+(\S+)", os.environ.get("ARCHFLAGS", ""))
146
+ if archs:
147
+ args.append("-DCMAKE_OSX_ARCHITECTURES=" + ";".join(archs))
148
+ return args
149
+
150
+
151
+ def _configure(build_dir: Path | None = None, *, config: str | None = None, extra_args: list[str] | None = None) -> Path:
152
+ build_dir = Path(build_dir or os.environ.get("MDR_PYTHON_BUILD_DIR", DEFAULT_BUILD_DIR))
153
+ # Always start from a clean build dir so a stale CMake cache or an
154
+ # interrupted dependency fetch (e.g. fmt via FetchContent) cannot poison
155
+ # the next configure. The C++ source is re-cloned separately each time.
156
+ _rmtree(build_dir)
157
+ build_dir.mkdir(parents=True, exist_ok=True)
158
+ cmd = [
159
+ _cmake(), "-S", str(_ensure_source()), "-B", str(build_dir),
160
+ # Single-config generators (Makefiles/Ninja) ignore `--build --config`.
161
+ f"-DCMAKE_BUILD_TYPE={config or _config()}",
162
+ "-DMDR_BUILD_TESTING=OFF",
163
+ "-DMDR_BUILD_CLIENT=OFF",
164
+ "-DMDR_BUILD_BT=OFF",
165
+ "-DMDR_BUILD_CODEGEN=OFF",
166
+ "-DMDR_CLIENT_DEBUGGER=OFF",
167
+ "-DMDR_ENABLE_LOG=OFF",
168
+ ]
169
+ cmd.extend(_platform_args())
170
+ if extra_args:
171
+ cmd.extend(extra_args)
172
+ gen = os.environ.get("MDR_CMAKE_GENERATOR")
173
+ if gen:
174
+ cmd[1:1] = ["-G", gen]
175
+ _run(cmd)
176
+ return build_dir
177
+
178
+
179
+ def _build(targets: list[str], build_dir: Path | None = None, *, config: str | None = None, extra_args: list[str] | None = None) -> Path:
180
+ config = config or _config()
181
+ build_dir = _configure(build_dir, config=config, extra_args=extra_args)
182
+ cmd = [_cmake(), "--build", str(build_dir), "--config", config, "--parallel"]
183
+ for target in targets:
184
+ cmd.extend(["--target", target])
185
+ _run(cmd)
186
+ return build_dir
187
+
188
+
189
+ def _find_built(destination: Path, target: str) -> Path:
190
+ base = _TARGET_BASENAMES[target]
191
+ names = {f"{base}{suf}" for suf in _SUFFIXES}
192
+ names |= {f"lib{base}{suf}" for suf in (".so", ".dylib")}
193
+ for path in destination.rglob("*"):
194
+ if path.is_file() and path.name in names:
195
+ return path.resolve()
196
+ raise FileNotFoundError(
197
+ f"Built library for {target} not found under {destination}. "
198
+ f"Looked for: {', '.join(sorted(names))}"
199
+ )
200
+
201
+
202
+ def stage_libraries(targets: list[str], destination: Path) -> list[Path]:
203
+ """Build the shared targets and emit them (flattened) into `destination`."""
204
+ destination = Path(destination).resolve()
205
+ destination.mkdir(parents=True, exist_ok=True)
206
+ # Build into the default build directory (e.g. build/python/) instead of
207
+ # redirecting CMake output directly into _native/. That avoids CMake's
208
+ # per-config subdir (MinSizeRel/ on MSVC) being discovered as a namespace
209
+ # package by setuptools.
210
+ build_dir = _build(targets)
211
+ staged: list[Path] = []
212
+ for target in targets:
213
+ found = _find_built(build_dir, target)
214
+ final = destination / found.name
215
+ if found.resolve() != final.resolve():
216
+ shutil.copy2(found, final)
217
+ staged.append(final)
218
+ return staged
219
+
220
+
221
+ def stage_library(target: str, destination: Path) -> Path:
222
+ """Build one shared target and emit it (flattened) into `destination`."""
223
+ return stage_libraries([target], destination)[0]
224
+
225
+
226
+ def _stage_native() -> None:
227
+ destination = Path(__file__).resolve().parent / "src" / "libmdr" / "_native"
228
+ stage_library("mdr_shared", destination)
229
+
230
+
231
+ class build_py(_build_py):
232
+ def run(self) -> None:
233
+ _stage_native()
234
+ super().run()
235
+
236
+
237
+ class BinaryDistribution(Distribution):
238
+ """Mark the distribution as platform specific.
239
+
240
+ There are no ext_modules (the native library is package data), so without
241
+ this the wheel is pure and installs into purelib, which auditwheel rejects.
242
+ """
243
+
244
+ def has_ext_modules(self) -> bool:
245
+ return True
246
+
247
+
248
+ class bdist_wheel(_bdist_wheel):
249
+ """Tag wheels ``py3-none-<platform>``.
250
+
251
+ The native library is loaded through ctypes and has no CPython ABI
252
+ dependency, so a single wheel per platform serves every Python 3
253
+ interpreter (CPython of any version, free-threaded builds, PyPy).
254
+ """
255
+
256
+ def get_tag(self) -> tuple[str, str, str]:
257
+ _, _, plat = super().get_tag()
258
+ return "py3", "none", plat
259
+
260
+
261
+ class editable_wheel(_editable_wheel):
262
+ def run(self) -> None:
263
+ # build_py.run() (called internally by super()) already handles
264
+ # the native build via _stage_native(). Calling it here too
265
+ # would trigger a wasteful second configure+build cycle.
266
+ super().run()
267
+
268
+
269
+ class sdist(_sdist):
270
+ def run(self) -> None:
271
+ # Source distributions do not embed platform binaries.
272
+ super().run()
273
+
274
+
275
+ setup(
276
+ distclass=BinaryDistribution,
277
+ cmdclass={
278
+ "bdist_wheel": bdist_wheel,
279
+ "build_py": build_py,
280
+ "editable_wheel": editable_wheel,
281
+ "sdist": sdist,
282
+ }
283
+ )
@@ -0,0 +1,18 @@
1
+ """Python bindings for libmdr (mdr-shared C ABI)."""
2
+
3
+ from . import constants, result
4
+ from .connection import Connection, DeviceInfo
5
+ from .headphones import Headphones
6
+ from .result import ABI_VERSION, MDRError, check, result_string
7
+
8
+ __all__ = [
9
+ "ABI_VERSION",
10
+ "Connection",
11
+ "DeviceInfo",
12
+ "Headphones",
13
+ "MDRError",
14
+ "check",
15
+ "constants",
16
+ "result",
17
+ "result_string",
18
+ ]
@@ -0,0 +1,10 @@
1
+ """ctypes structs and prototypes for the mdr-c ABI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from . import _dll
6
+ from ._generated_api import * # noqa: F403
7
+ from ._generated_api import bind as _bind
8
+
9
+
10
+ _bind(_dll.lib())
@@ -0,0 +1,92 @@
1
+ """ctypes loader for mdr-shared."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from ctypes import CDLL, RTLD_GLOBAL
8
+ from pathlib import Path
9
+
10
+ _LIB: CDLL | None = None
11
+
12
+ _ENV_OVERRIDE = "MDR_DLL"
13
+
14
+ _CANDIDATE_NAMES = (
15
+ "mdr-shared.dll",
16
+ "mdr.dll",
17
+ "libmdr-shared.so",
18
+ "libmdr-shared.dylib",
19
+ "mdr-shared.so",
20
+ "mdr-shared.dylib",
21
+ )
22
+
23
+
24
+ def _package_native_dir() -> Path:
25
+ return Path(__file__).resolve().parent / "_native"
26
+
27
+
28
+ def _candidate_paths() -> list[Path]:
29
+ paths: list[Path] = []
30
+ override = os.environ.get(_ENV_OVERRIDE)
31
+ if override:
32
+ paths.append(Path(override))
33
+
34
+ native = _package_native_dir()
35
+ for name in _CANDIDATE_NAMES:
36
+ paths.append(native / name)
37
+
38
+ # Editable / in-tree builds may leave artifacts in the shared CMake tree.
39
+ repo_build = Path(__file__).resolve().parents[4] / "build" / "python"
40
+ if repo_build.is_dir():
41
+ for name in _CANDIDATE_NAMES:
42
+ paths.extend(repo_build.rglob(name))
43
+
44
+ return paths
45
+
46
+
47
+ def _load_path(path: Path) -> CDLL:
48
+ path = path.resolve()
49
+ if not path.is_file():
50
+ raise FileNotFoundError(path)
51
+
52
+ if sys.platform == "win32":
53
+ os.add_dll_directory(str(path.parent))
54
+ return CDLL(str(path))
55
+
56
+ mode = RTLD_GLOBAL if hasattr(os, "RTLD_GLOBAL") else 0
57
+ return CDLL(str(path), mode=mode)
58
+
59
+
60
+ def load() -> CDLL:
61
+ global _LIB
62
+ if _LIB is not None:
63
+ return _LIB
64
+
65
+ errors: list[str] = []
66
+ seen: set[Path] = set()
67
+ for candidate in _candidate_paths():
68
+ try:
69
+ resolved = candidate.resolve()
70
+ except OSError as exc:
71
+ errors.append(f"{candidate}: {exc}")
72
+ continue
73
+ if resolved in seen:
74
+ continue
75
+ seen.add(resolved)
76
+ try:
77
+ _LIB = _load_path(resolved)
78
+ return _LIB
79
+ except OSError as exc:
80
+ errors.append(f"{resolved}: {exc}")
81
+
82
+ hint = (
83
+ f"Set {_ENV_OVERRIDE} to the full path of mdr-shared "
84
+ f"(or mdr.dll), or `pip install -e libmdr` to build it."
85
+ )
86
+ raise FileNotFoundError(
87
+ "Unable to load mdr-shared.\n" + "\n".join(errors) + ("\n" + hint if errors else hint)
88
+ )
89
+
90
+
91
+ def lib() -> CDLL:
92
+ return load()