Skip to content

Commit 2522363

Browse files
committed
Install: Add python script to generate shell scripts from spec.json
1 parent 562e95c commit 2522363

1 file changed

Lines changed: 232 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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("\nScript 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("\nThese 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

Comments
 (0)