11"""
2- Bridge to Bun/ JavaScript renderer.
2+ Bridge to JavaScript renderer.
33
4- Handles subprocess communication with the bundled JS renderer.
4+ Handles subprocess communication with the JS renderer.
5+ Uses Node.js when available (required for native modules), falls back to Bun.
56"""
67
78import json
89import os
10+ import shutil
911import subprocess
1012import sys
1113from collections .abc import Callable
1214from pathlib import Path
1315from typing import Any
1416
15- import pybun
16-
1717from .exceptions import MlnativeError
1818
1919
@@ -58,9 +58,55 @@ def get_vendor_dir() -> Path:
5858 f"Supported platforms: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64"
5959 )
6060
61+ # Check that node_modules is actually installed
62+ node_modules = vendor_dir / "node_modules" / "@maplibre" / "maplibre-gl-native"
63+ if not node_modules .exists ():
64+ raise MlnativeError (
65+ f"Native binaries not installed for { vendor_name } .\n \n "
66+ f"To fix, run:\n "
67+ f" cd { vendor_dir } && npm install\n \n "
68+ f"Or if using pip, try reinstalling:\n "
69+ f" pip install --force-reinstall mlnative"
70+ )
71+
6172 return vendor_dir
6273
6374
75+ def _get_js_runtime () -> str :
76+ """
77+ Get path to JavaScript runtime.
78+
79+ Prefers Node.js (required for native modules like maplibre-gl-native),
80+ falls back to bundled Bun from pybun package.
81+ """
82+ # First, try to find node in PATH (required for native modules)
83+ node_path = shutil .which ("node" )
84+ if node_path :
85+ return node_path
86+
87+ # Fall back to bundled bun from pybun
88+ # Note: Bun may not work with all native modules due to V8/JSC differences
89+ try :
90+ import pybun
91+
92+ pybun_file = pybun .__file__
93+ if pybun_file :
94+ bun_path = Path (pybun_file ).parent / "bun"
95+ if bun_path .exists ():
96+ return str (bun_path )
97+ except ImportError :
98+ pass
99+
100+ raise MlnativeError (
101+ "No JavaScript runtime found.\n \n "
102+ "Install Node.js (recommended for native module compatibility):\n "
103+ " - macOS: brew install node\n "
104+ " - Linux: apt install nodejs or use nvm\n "
105+ " - Windows: https://nodejs.org/\n \n "
106+ "Or install pybun: pip install pybun"
107+ )
108+
109+
64110def _validate_png_output (stdout : bytes ) -> bytes :
65111 """Validate that output is valid PNG bytes."""
66112 if not stdout .startswith (b"\x89 PNG" ):
@@ -73,16 +119,16 @@ def _validate_png_output(stdout: bytes) -> bytes:
73119 return stdout
74120
75121
76- def _run_bun_process (
77- bun_path : str , renderer_js : Path , config : dict [str , Any ], vendor_dir : Path
122+ def _run_js_process (
123+ runtime_path : str , renderer_js : Path , config : dict [str , Any ], vendor_dir : Path
78124) -> bytes :
79- """Execute Bun subprocess and return output."""
125+ """Execute JavaScript subprocess and return output."""
80126 env = os .environ .copy ()
81127 env ["MLNATIVE_VENDOR_DIR" ] = str (vendor_dir )
82128
83129 try :
84130 result = subprocess .run (
85- [bun_path , str (renderer_js )],
131+ [runtime_path , str (renderer_js )],
86132 input = json .dumps (config ).encode ("utf-8" ),
87133 capture_output = True ,
88134 env = env ,
@@ -92,11 +138,11 @@ def _run_bun_process(
92138 raise MlnativeError ("Render timeout (60s exceeded)" ) from None
93139 except subprocess .CalledProcessError as e :
94140 stderr = e .stderr .decode ("utf-8" , errors = "replace" ) if e .stderr else "Unknown error"
95- raise MlnativeError (f"Bun process failed:\n { stderr } " ) from e
141+ raise MlnativeError (f"JS process failed:\n { stderr } " ) from e
96142
97143 if result .returncode != 0 :
98144 stderr = result .stderr .decode ("utf-8" , errors = "replace" )
99- raise MlnativeError (f"Bun render failed:\n { stderr } " )
145+ raise MlnativeError (f"Render failed:\n { stderr } " )
100146
101147 return result .stdout
102148
@@ -105,7 +151,7 @@ def render_with_bun(
105151 config : dict [str , Any ], request_handler : Callable [[Any ], bytes ] | None = None
106152) -> bytes :
107153 """
108- Render map using Bun subprocess.
154+ Render map using JavaScript subprocess.
109155
110156 Args:
111157 config: Map configuration dict
@@ -123,11 +169,10 @@ def render_with_bun(
123169 if not renderer_js .exists ():
124170 raise MlnativeError (f"Renderer script not found: { renderer_js } " )
125171
126- # Get bun path from pybun package (bundled binary)
127- bun_path = str (Path (pybun .__file__ ).parent / "bun" )
172+ runtime_path = _get_js_runtime ()
128173
129174 # Flag for custom handler (JS side uses temp file protocol)
130175 config ["_hasCustomHandler" ] = request_handler is not None
131176
132- stdout = _run_bun_process ( bun_path , renderer_js , config , vendor_dir )
177+ stdout = _run_js_process ( runtime_path , renderer_js , config , vendor_dir )
133178 return _validate_png_output (stdout )
0 commit comments