Skip to content

Commit 8dbe146

Browse files
Merge branch 'main' into sphinx
2 parents f0156c1 + 4c18d13 commit 8dbe146

File tree

4 files changed

+161
-50
lines changed

4 files changed

+161
-50
lines changed

brian2wasm/__main__.py

Lines changed: 109 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,55 @@
11
import argparse
2+
import shutil
23
import sys
34
import os
5+
import platform
6+
import subprocess
47

58
def main():
69
"""
7-
Run a Brian2Wasm simulation from the command line.
10+
Command-line interface for **Brian2Wasm**.
11+
12+
Usage
13+
-----
14+
``python -m brian2wasm <script.py> [--no-server] [--skip-install]``
815
916
Parameters
1017
----------
1118
script : str
12-
Path to the Python model file. Must end with ``.py`` and must not call
13-
``set_device`` directly, as the CLI automatically inserts the required
14-
``set_device('wasm_standalone', …)`` line.
15-
no_server : bool, optional
16-
If True, generate the WASM/HTML output without starting the local
17-
preview server (sets the ``BRIAN2WASM_NO_SERVER`` environment variable).
18-
19-
Raises
20-
------
21-
FileNotFoundError
22-
If the given script does not exist.
23-
ValueError
24-
If the provided file is not a ``.py`` script.
25-
RuntimeError
26-
If execution of the model fails for any reason.
27-
28-
Notes
29-
-----
30-
This command is typically invoked as::
19+
Path to the user’s Python model. The file **must** end with
20+
``.py`` and must not call ``set_device`` itself – the CLI inserts
21+
the appropriate ``set_device('wasm_standalone', …)`` line
22+
automatically.
23+
--no-server : flag, optional
24+
Generate the WASM/HTML output without starting the local preview
25+
web-server (sets the ``BRIAN2WASM_NO_SERVER`` environment
26+
variable for the subprocess).
27+
--skip-install : flag, optional
28+
Run Brian2WASM without checking or installing EMSDK. Use this if
29+
you are sure EMSDK is already installed and configured in your
30+
environment.
3131
32-
python -m brian2wasm <script.py> [--no-server]
32+
Behaviour
33+
---------
34+
1. Validates that *script* exists and is a ``.py`` file.
35+
2. Looks for an ``<scriptname>.html`` file in the same directory.
36+
* If found, passes the HTML file to ``set_device`` so the custom
37+
template is used.
38+
* Otherwise falls back to the default template.
39+
3. Unless *--skip-install* is given, verifies EMSDK installation
40+
(Pixi/Conda/CONDA_EMSDK_DIR) and attempts to activate it.
41+
4. Prepends the required ``set_device('wasm_standalone', …)`` call to
42+
the script source in-memory.
43+
5. Executes the modified script with its own directory as working
44+
directory, so any relative paths inside the model behave as
45+
expected.
46+
47+
Exit status
48+
-----------
49+
* ``0`` – build finished successfully (and server started unless
50+
*--no-server* was given).
51+
* ``1`` – any error (missing file, not a ``.py`` file, EMSDK not found
52+
or not activated, exception during model execution, etc.).
3353
"""
3454

3555
parser = argparse.ArgumentParser(
@@ -44,18 +64,28 @@ def main():
4464
action="store_true",
4565
help="Generate files without starting the web server"
4666
)
67+
parser.add_argument("--skip-install",
68+
action="store_true",
69+
help="Run Brian2WASM without installing/activating EMSDK"
70+
)
71+
4772
args = parser.parse_args()
4873

4974
script_path = args.script
5075

5176
# Check if the script exists and is a Python file
5277
if not os.path.isfile(script_path):
53-
print(f"Error: File '{script_path}' does not exist.", file=sys.stderr)
78+
full_path = os.path.abspath(script_path)
79+
print(f"❌ Error: File '{full_path}' does not exist.", file=sys.stderr)
5480
sys.exit(1)
5581
if not script_path.endswith(".py"):
56-
print(f"Error: File '{script_path}' is not a Python script (.py).", file=sys.stderr)
82+
print(f"Error: File '{script_path}' is not a Python script (.py).", file=sys.stderr)
5783
sys.exit(1)
5884

85+
if not args.skip_install:
86+
# Check emsdk setup
87+
check_emsdk()
88+
5989
# Read the original script
6090
with open(script_path, 'r') as f:
6191
script_content = f.read()
@@ -69,41 +99,87 @@ def main():
6999
html_file_path = os.path.join(script_dir, html_file)
70100
has_html_file = os.path.isfile(html_file_path)
71101

72-
# Inject the required lines at the top
102+
# Inject required lines at the top
73103
if has_html_file:
74-
print(f"html file found: '{html_file_path}'")
104+
print(f"✅ HTML file found: '{html_file_path}'")
75105
injection = (
76106
"from brian2 import set_device\n"
77107
"import brian2wasm\n"
78108
f"set_device('wasm_standalone', directory='{script_name}', html_file='{html_file}')\n"
79109
)
80110
else:
81-
print(f"html file not found: using default html template")
111+
print("ℹ️ HTML file not found: using default HTML template.")
82112
injection = (
83113
"from brian2 import set_device\n"
84114
"import brian2wasm\n"
85115
f"set_device('wasm_standalone', directory='{script_name}')\n"
86116
)
117+
87118
modified_script = injection + script_content
88119

89-
# Set the working directory to the script's directory
120+
# Set working directory to script's directory
90121
original_cwd = os.getcwd()
91122
os.chdir(script_dir)
92123

93124
try:
94-
# Execute the modified script in memory with __file__ set
95125
if args.no_server:
96126
os.environ['BRIAN2WASM_NO_SERVER'] = '1'
97-
print(f"Script path: {os.path.abspath(script_path)}")
98-
print(f"Directory: {script_dir}")
127+
128+
print(f"📄 Script path: {os.path.abspath(script_path)}")
129+
print(f"📁 Directory: {script_dir}")
99130
exec_globals = {'__name__': '__main__', '__file__': os.path.abspath(script_path)}
100-
exec(modified_script, exec_globals)
131+
compiled_script = compile(modified_script, script_path, 'exec')
132+
exec(compiled_script, exec_globals)
133+
101134
except Exception as e:
102-
print(f"Error running script: {e}", file=sys.stderr)
135+
print(f"Error running script: {e}", file=sys.stderr)
103136
sys.exit(1)
137+
104138
finally:
105-
# Restore the original working directory
106139
os.chdir(original_cwd)
107140

141+
142+
def check_emsdk():
143+
emsdk = shutil.which("emsdk")
144+
conda_emsdk_dir = os.environ.get("CONDA_EMSDK_DIR")
145+
146+
if not emsdk and not conda_emsdk_dir:
147+
print("❌ EMSDK and CONDA_EMSDK_DIR not found. That means EMSDK is not installed.")
148+
print(" ➤ If you are using **Pixi**, run:")
149+
print(" pixi add emsdk && pixi install")
150+
print(" ➤ If you are using **Conda**, run:")
151+
print(" conda install emsdk -c conda-forge")
152+
print(" ➤ Else refer to Emscripten documentation:")
153+
print(" https://emscripten.org/index.html#")
154+
sys.exit(1)
155+
156+
print(f"✅ EMSDK is installed and CONDA_EMSDK_DIR is found")
157+
158+
try:
159+
print("🔧 Attempting to activate EMSDK with: emsdk activate latest")
160+
result = subprocess.run(["./emsdk", "activate", "latest"], cwd=conda_emsdk_dir, check=False, capture_output=True, text=True)
161+
if result.returncode != 0:
162+
print("❌ Failed to activate EMSDK:")
163+
choice = input("Do you want to install and activate EMSDK now? (y/n) ")
164+
if choice == 'y':
165+
try:
166+
subprocess.run(["./emsdk", "install", "latest"], cwd=conda_emsdk_dir, check=True)
167+
print("✅ EMSDK install & activation succeeded. You can run the script now.")
168+
except subprocess.CalledProcessError as e:
169+
print("❌ Failed to activate EMSDK:")
170+
print(" ➤ Please run the following manually in your terminal and try again:")
171+
print(" cd $CONDA_EMSDK_DIR && ./emsdk install latest && ./emsdk activate latest")
172+
else:
173+
print(" ➤ Please run the following manually in your terminal and try again:")
174+
print(" cd $CONDA_EMSDK_DIR && ./emsdk install latest && ./emsdk activate latest")
175+
176+
sys.exit(1)
177+
else:
178+
print("✅ EMSDK activation succeeded.")
179+
except Exception as e:
180+
print(f"❌ Error while running EMSDK activation: {e}")
181+
sys.exit(1)
182+
183+
108184
if __name__ == "__main__":
109185
main()

brian2wasm/device.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,13 @@ def generate_makefile(self, writer, compiler, compiler_flags, linker_flags, nb_t
254254
source_files = ' '.join(sorted(writer.source_files))
255255
preamble_file = os.path.join(os.path.dirname(__file__), 'templates', 'pre.js')
256256

257-
emsdk_path = (
257+
prefs.devices.wasm_standalone.emsdk_directory = (
258258
prefs.devices.wasm_standalone.emsdk_directory
259259
or os.environ.get("EMSDK")
260260
or os.environ.get("CONDA_EMSDK_DIR")
261-
or ""
262261
)
262+
emsdk_path = prefs.devices.wasm_standalone.emsdk_directory
263+
263264
emsdk_version = prefs.devices.wasm_standalone.emsdk_version
264265
if not emsdk_path:
265266
# Check whether EMSDK is already activated
@@ -662,22 +663,19 @@ def run(self, directory, results_directory, with_output, run_args):
662663
if os.environ.get('BRIAN2WASM_NO_SERVER','0') == '1':
663664
print("Skipping server startup (--no-server flag set)")
664665
return
665-
666+
667+
emsdk_path = prefs.devices.wasm_standalone.emsdk_directory
668+
os.environ['EMSDK_QUIET'] = '1'
669+
666670
if platform.system() == "Windows":
667-
cmd_line = f'emrun "index.html"'
668-
try:
669-
os.system(cmd_line)
670-
except Exception as e:
671-
raise RuntimeError(f"Failed to run emrun command: {cmd_line}. "
672-
"Please ensure that emrun is installed and available in your PATH.") from e
673-
674-
if prefs.devices.wasm_standalone.emsdk_directory:
675-
emsdk_path = prefs.devices.wasm_standalone.emsdk_directory
676-
run_cmd = ['source', f'{emsdk_path}/emsdk_env.sh', '&&', 'emrun', 'index.html']
671+
cmd_line = f'cmd.exe /C "call {emsdk_path}\\emsdk_env.bat & emrun index.html"'
672+
677673
else:
678-
run_cmd = ['emrun', 'index.html']
674+
run_cmd = ['source', f'{emsdk_path}/emsdk_env.sh', '&&', 'emrun', 'index.html']
675+
cmd_line = f"/bin/bash -c '{' '.join(run_cmd + run_args)}'"
676+
679677
start_time = time.time()
680-
os.system(f"/bin/bash -c '{' '.join(run_cmd + run_args)}'")
678+
os.system(cmd_line)
681679
self.timers['run_binary'] = time.time() - start_time
682680

683681
def build(self, html_file=None, html_content=None, **kwds):

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ python = ">=3.10"
4949
emsdk = "*"
5050

5151
[tool.pixi.pypi-dependencies]
52-
brian2wasm = "*"
53-
brian2 = "==2.9.0"
52+
brian2wasm = "*"
53+
brian2 = ">=2.9.0"
5454

5555
[tool.pixi.tasks]
5656
setup = "emsdk install latest && emsdk activate latest"

recipe/meta.yaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{% set name = "brian2wasm" %}
2+
{% set version = "0.4.2" %}
3+
4+
package:
5+
name: {{ name }}
6+
version: {{ version }}
7+
8+
source:
9+
path: ..
10+
11+
build:
12+
number: 0
13+
script: {{ PYTHON }} -m pip install . --no-deps -vv
14+
15+
requirements:
16+
host:
17+
- python
18+
- pip
19+
- setuptools
20+
run:
21+
- python
22+
- brian2 >=2.9.0
23+
- numpy
24+
- emsdk
25+
26+
about:
27+
home: https://github.com/brian-team/brian2wasm
28+
summary: WebAssembly backend for Brian2 simulator
29+
description: |
30+
brian2wasm extends the Brian2 simulator to generate WebAssembly-compatible code
31+
using Emscripten. This enables running spiking neural network simulations directly
32+
in the browser.
33+
34+
extra:
35+
recipe-maintainers:
36+
- mstimberg
37+
- PalashChitnavis

0 commit comments

Comments
 (0)