1
1
import argparse
2
+ import shutil
2
3
import sys
3
4
import os
5
+ import platform
6
+ import subprocess
4
7
5
8
def main ():
6
9
"""
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]``
8
15
9
16
Parameters
10
17
----------
11
18
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.
31
31
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.).
33
53
"""
34
54
35
55
parser = argparse .ArgumentParser (
@@ -44,18 +64,28 @@ def main():
44
64
action = "store_true" ,
45
65
help = "Generate files without starting the web server"
46
66
)
67
+ parser .add_argument ("--skip-install" ,
68
+ action = "store_true" ,
69
+ help = "Run Brian2WASM without installing/activating EMSDK"
70
+ )
71
+
47
72
args = parser .parse_args ()
48
73
49
74
script_path = args .script
50
75
51
76
# Check if the script exists and is a Python file
52
77
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 )
54
80
sys .exit (1 )
55
81
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 )
57
83
sys .exit (1 )
58
84
85
+ if not args .skip_install :
86
+ # Check emsdk setup
87
+ check_emsdk ()
88
+
59
89
# Read the original script
60
90
with open (script_path , 'r' ) as f :
61
91
script_content = f .read ()
@@ -69,41 +99,87 @@ def main():
69
99
html_file_path = os .path .join (script_dir , html_file )
70
100
has_html_file = os .path .isfile (html_file_path )
71
101
72
- # Inject the required lines at the top
102
+ # Inject required lines at the top
73
103
if has_html_file :
74
- print (f"html file found: '{ html_file_path } '" )
104
+ print (f"✅ HTML file found: '{ html_file_path } '" )
75
105
injection = (
76
106
"from brian2 import set_device\n "
77
107
"import brian2wasm\n "
78
108
f"set_device('wasm_standalone', directory='{ script_name } ', html_file='{ html_file } ')\n "
79
109
)
80
110
else :
81
- print (f"html file not found: using default html template" )
111
+ print ("ℹ️ HTML file not found: using default HTML template. " )
82
112
injection = (
83
113
"from brian2 import set_device\n "
84
114
"import brian2wasm\n "
85
115
f"set_device('wasm_standalone', directory='{ script_name } ')\n "
86
116
)
117
+
87
118
modified_script = injection + script_content
88
119
89
- # Set the working directory to the script's directory
120
+ # Set working directory to script's directory
90
121
original_cwd = os .getcwd ()
91
122
os .chdir (script_dir )
92
123
93
124
try :
94
- # Execute the modified script in memory with __file__ set
95
125
if args .no_server :
96
126
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 } " )
99
130
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
+
101
134
except Exception as e :
102
- print (f"Error running script: { e } " , file = sys .stderr )
135
+ print (f"❌ Error running script: { e } " , file = sys .stderr )
103
136
sys .exit (1 )
137
+
104
138
finally :
105
- # Restore the original working directory
106
139
os .chdir (original_cwd )
107
140
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
+
108
184
if __name__ == "__main__" :
109
185
main ()
0 commit comments