1+ #!/usr/bin/env python3
2+ """
3+ Script to generate shell installation scripts from Jinja2 templates and spec.json
4+
5+ This script reads the MHKiT specification from spec.json and generates
6+ platform-specific shell scripts for Python environment setup.
7+
8+ Usage:
9+ python generate_scripts.py
10+
11+ Generated files:
12+ - ../scripts/install_mhkit_python_unix.sh (for macOS/Linux)
13+ - ../scripts/install_mhkit_python_windows.ps1 (for Windows)
14+ """
15+
16+ import json
17+ import os
18+ import sys
19+ from pathlib import Path
20+
21+ try :
22+ from jinja2 import Environment , FileSystemLoader
23+ except ImportError :
24+ print ("Error: jinja2 is required to generate scripts" )
25+ print ("Install with: pip install jinja2" )
26+ sys .exit (1 )
27+
28+
29+ def load_spec ():
30+ """Load the MHKiT specification from spec.json"""
31+ script_dir = Path (__file__ ).parent
32+ spec_path = script_dir .parent / "+mhkit" / "spec.json"
33+
34+ if not spec_path .exists ():
35+ raise FileNotFoundError (f"spec.json not found at { spec_path } " )
36+
37+ with open (spec_path , 'r' ) as f :
38+ return json .load (f )
39+
40+
41+ def setup_jinja_environment ():
42+ """Set up Jinja2 environment with templates directory"""
43+ script_dir = Path (__file__ ).parent
44+ template_dir = script_dir
45+
46+ return Environment (
47+ loader = FileSystemLoader (template_dir ),
48+ trim_blocks = True ,
49+ lstrip_blocks = True ,
50+ keep_trailing_newline = True
51+ )
52+
53+
54+ def replace_placeholders_in_string (text , replacements ):
55+ """Replace all placeholders in a string with their values"""
56+ if not isinstance (text , str ):
57+ return text
58+
59+ result = text
60+ for placeholder , value in replacements .items ():
61+ result = result .replace (f"<{ placeholder } >" , str (value ))
62+
63+ return result
64+
65+
66+ def replace_placeholders_recursively (obj , replacements ):
67+ """Recursively replace placeholders in nested structures"""
68+ if isinstance (obj , str ):
69+ return replace_placeholders_in_string (obj , replacements )
70+ elif isinstance (obj , list ):
71+ return [replace_placeholders_recursively (item , replacements ) for item in obj ]
72+ elif isinstance (obj , dict ):
73+ return {key : replace_placeholders_recursively (value , replacements ) for key , value in obj .items ()}
74+ else :
75+ return obj
76+
77+
78+ def preprocess_spec_for_templates (spec ):
79+ """Preprocess spec to replace all placeholders with actual values"""
80+ import copy
81+
82+ # Define all replacements based on spec values
83+ replacements = {
84+ 'conda_env' : spec ['conda' ]['environment_name' ],
85+ 'python_version' : spec ['python' ]['install_version' ],
86+ 'mhkit_python_version' : spec ['mhkit_python' ]['version' ],
87+ 'version' : spec ['package' ]['version' ]
88+ }
89+
90+ # Deep copy spec and replace all placeholders
91+ processed_spec = copy .deepcopy (spec )
92+ processed_spec = replace_placeholders_recursively (processed_spec , replacements )
93+
94+ return processed_spec
95+
96+
97+ def generate_unix_scripts (spec , env , output_dir ):
98+ """Generate Unix (macOS/Linux) shell scripts - both monolithic and step-by-step"""
99+
100+ # Generate original monolithic script
101+ template = env .get_template ('install_unix.sh.j2' )
102+ content = template .render (** spec )
103+ output_path = output_dir / "install_mhkit_python_unix.sh"
104+ with open (output_path , 'w' ) as f :
105+ f .write (content )
106+ output_path .chmod (0o755 )
107+ print (f"Generated: { output_path } " )
108+
109+ # Generate step-by-step scripts
110+ step_templates = [
111+ 'step1_detect_conda.sh.j2' ,
112+ 'step2_install_conda.sh.j2' ,
113+ 'step3_create_env.sh.j2' ,
114+ 'step4_install_dependencies.sh.j2' ,
115+ 'step5_post_install.sh.j2'
116+ ]
117+
118+ for step_template in step_templates :
119+ template = env .get_template (step_template )
120+ content = template .render (** spec )
121+
122+ # Extract step name from template filename
123+ step_name = step_template .replace ('.sh.j2' , '.sh' )
124+ output_path = output_dir / step_name
125+
126+ with open (output_path , 'w' ) as f :
127+ f .write (content )
128+
129+ # Make executable
130+ output_path .chmod (0o755 )
131+ print (f"Generated: { output_path } " )
132+
133+
134+ def generate_windows_scripts (spec , env , output_dir ):
135+ """Generate Windows PowerShell scripts - both monolithic and step-by-step"""
136+
137+ # Generate original monolithic script
138+ template = env .get_template ('install_windows.ps1.j2' )
139+ content = template .render (** spec )
140+ output_path = output_dir / "install_mhkit_python_windows.ps1"
141+ with open (output_path , 'w' ) as f :
142+ f .write (content )
143+ print (f"Generated: { output_path } " )
144+
145+ # Generate step-by-step scripts
146+ step_templates = [
147+ 'step1_detect_conda.ps1.j2' ,
148+ 'step2_install_conda.ps1.j2' ,
149+ 'step3_create_env.ps1.j2' ,
150+ 'step4_install_dependencies.ps1.j2' ,
151+ 'step5_post_install.ps1.j2'
152+ ]
153+
154+ for step_template in step_templates :
155+ template = env .get_template (step_template )
156+ content = template .render (** spec )
157+
158+ # Extract step name from template filename
159+ step_name = step_template .replace ('.ps1.j2' , '.ps1' )
160+ output_path = output_dir / step_name
161+
162+ with open (output_path , 'w' ) as f :
163+ f .write (content )
164+
165+ print (f"Generated: { output_path } " )
166+
167+
168+ def create_output_directory ():
169+ """Create the shell_scripts output directory"""
170+ script_dir = Path (__file__ ).parent
171+ output_dir = script_dir .parent / "shell_scripts"
172+ output_dir .mkdir (exist_ok = True )
173+ return output_dir
174+
175+
176+ def validate_spec (spec ):
177+ """Validate that the spec contains required sections"""
178+ required_sections = [
179+ 'conda' , 'python' , 'mhkit_python' , 'hooks' , 'support'
180+ ]
181+
182+ for section in required_sections :
183+ if section not in spec :
184+ raise ValueError (f"Missing required section in spec.json: { section } " )
185+
186+ # Validate hooks structure
187+ hooks = spec ['hooks' ]
188+ required_hook_sections = ['pre_install' , 'post_install' , 'environment_setup' ]
189+ for hook_section in required_hook_sections :
190+ if hook_section not in hooks :
191+ raise ValueError (f"Missing required hook section: { hook_section } " )
192+
193+ if 'commands' not in hooks [hook_section ]:
194+ raise ValueError (f"Missing commands in hook section: { hook_section } " )
195+
196+
197+ def main ():
198+ """Main function to generate all scripts"""
199+ try :
200+ print ("Loading MHKiT specification..." )
201+ spec = load_spec ()
202+
203+ print ("Validating specification..." )
204+ validate_spec (spec )
205+
206+ print ("Setting up Jinja2 environment..." )
207+ env = setup_jinja_environment ()
208+
209+ print ("Creating output directory..." )
210+ output_dir = create_output_directory ()
211+
212+ print ("Preprocessing placeholders..." )
213+ processed_spec = preprocess_spec_for_templates (spec )
214+
215+ print ("Generating shell scripts..." )
216+ generate_unix_scripts (processed_spec , env , output_dir )
217+ generate_windows_scripts (processed_spec , env , output_dir )
218+
219+ print ("\n Script generation completed successfully!" )
220+ print (f"Unix script: { output_dir } /install_mhkit_python_unix.sh" )
221+ print (f"Windows script: { output_dir } /install_mhkit_python_windows.ps1" )
222+ print (f"Step-by-step scripts also generated in: { output_dir } " )
223+
224+ print ("\n These scripts can now be called from MATLAB's mhkit.install() function." )
225+
226+ except Exception as e :
227+ print (f"Error generating scripts: { e } " )
228+ sys .exit (1 )
229+
230+
231+ if __name__ == "__main__" :
232+ main ()
0 commit comments