-
Notifications
You must be signed in to change notification settings - Fork 43
Expand file tree
/
Copy pathsetup.py
More file actions
143 lines (118 loc) · 5.66 KB
/
Copy pathsetup.py
File metadata and controls
143 lines (118 loc) · 5.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""Build configuration hook for the iai-mcp wheel.
All project metadata lives in pyproject.toml. This file exists solely to
register a custom build_py subclass that:
- compiles the TypeScript MCP wrapper and stages the resulting JS files into
the wheel build directory (build_lib) before the wheel is assembled;
- stages the native-extension type stubs (*.pyi, py.typed) from the Rust
workspace beside the compiled extension in the wheel.
Both operations write into build_lib, never into the source tree, so an
editable checkout stays clean.
Editable installs (pip install -e .) skip the npm build entirely. The
install script (scripts/install.sh) builds the wrapper separately; the
resolver falls back to mcp-wrapper/dist/ on an editable install.
"""
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
from setuptools import setup
from setuptools.command.build_py import build_py as _OrigBuildPy
_REPO_ROOT = Path(__file__).parent
_WRAPPER_SRC = _REPO_ROOT / "mcp-wrapper"
# Tracked type stubs for the native extension; staged flat beside the
# compiled .so in the wheel so that `Path(iai_mcp_native.__file__).parent`
# finds them after installation.
_NATIVE_STUB_SRC = _REPO_ROOT / "rust" / "iai_mcp_native" / "iai_mcp_native"
_NATIVE_STUB_FILES = [
# Primary stub for the flat-layout wheel (importable as iai_mcp_native.pyi).
("__init__.pyi", "iai_mcp_native.pyi"),
("embed.pyi", "embed.pyi"),
("graph.pyi", "graph.pyi"),
("py.typed", "py.typed"),
]
class _BuildWithWrapper(_OrigBuildPy):
"""build_py subclass that compiles the TS wrapper and stages native stubs.
At wheel-build time: collects the package into build_lib (via the parent
``build_py``), runs ``npm ci && npm run build`` inside mcp-wrapper/, then
stages the resulting JS files into build_lib/iai_mcp/_wrapper/ and stages
the native extension type stubs flat into build_lib/ so they ship beside
the compiled extension. The source tree is never touched.
At editable-install time (``pip install -e .``): returns immediately without
touching npm. The editable resolver finds the wrapper via mcp-wrapper/dist/
and the stubs via the maturin editable package directory.
"""
def run(self) -> None:
# Editable installs must never trigger npm. The install script builds
# the wrapper as a separate step.
if self.editable_mode:
super().run()
return
# Collect the package into build_lib FIRST so build_lib/iai_mcp/ exists,
# THEN stage the freshly compiled JS into build_lib/iai_mcp/_wrapper/
# and the native stubs flat into build_lib/.
# Staging into build_lib (not the source tree) keeps the checkout clean.
super().run()
self._build_ts_wrapper()
self._stage_native_stubs()
def _build_ts_wrapper(self) -> None:
"""Compile the TypeScript wrapper and stage the JS output into build_lib."""
if not (_WRAPPER_SRC / "package.json").exists():
raise RuntimeError(
"mcp-wrapper/package.json not found. The TypeScript source must be "
"present to build the MCP wrapper. If you are building from an sdist, "
"ensure MANIFEST.in includes the mcp-wrapper source."
)
npm_exe = shutil.which("npm")
if npm_exe is None:
raise RuntimeError(
"Node.js/npm is required to build the MCP wrapper. "
"Install Node.js >=18 and ensure 'npm' is on your PATH, then retry."
)
# Install exact locked dependencies.
subprocess.run(
[npm_exe, "ci", "--prefer-offline"],
cwd=str(_WRAPPER_SRC),
check=True,
)
# Compile TypeScript → dist/*.js
subprocess.run(
[npm_exe, "run", "build"],
cwd=str(_WRAPPER_SRC),
check=True,
)
dist_dir = _WRAPPER_SRC / "dist"
if not dist_dir.exists():
raise RuntimeError(
f"Expected {dist_dir} after 'npm run build' but the directory is absent. "
"Check the TypeScript compiler output above for errors."
)
# Stage the freshly built JS into the wheel build directory
# (build_lib/iai_mcp/_wrapper/) — never into the source tree, so an
# editable checkout stays clean and its resolver falls back to
# mcp-wrapper/dist/ as intended.
# Copy *.js only — source maps (.js.map) are excluded from the wheel.
wrapper_dest = Path(self.build_lib) / "iai_mcp" / "_wrapper"
if wrapper_dest.exists():
shutil.rmtree(wrapper_dest)
wrapper_dest.mkdir(parents=True)
js_files = sorted(dist_dir.glob("*.js"))
if not js_files:
raise RuntimeError(
f"No *.js files found in {dist_dir}. "
"The 'npm run build' step produced no output."
)
for js_file in js_files:
shutil.copy2(js_file, wrapper_dest / js_file.name)
def _stage_native_stubs(self) -> None:
"""Stage the native extension type stubs flat into build_lib.
setuptools-rust places the compiled extension flat at the build_lib
root (i.e. ``build_lib/iai_mcp_native.cpython-*.so``), so stubs must
land at the same level to be found via
``Path(iai_mcp_native.__file__).parent`` after installation.
"""
build_lib_root = Path(self.build_lib)
for src_name, dest_name in _NATIVE_STUB_FILES:
src = _NATIVE_STUB_SRC / src_name
if src.exists():
shutil.copy2(src, build_lib_root / dest_name)
setup(cmdclass={"build_py": _BuildWithWrapper})