Skip to content

Commit d084276

Browse files
ENH: Update README and requirements, refactor OpenRocket integration in cli.py
1 parent 6bbce95 commit d084276

6 files changed

Lines changed: 303 additions & 72 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ pip install rocketserializer
3838
### Java
3939

4040
You need Java to be installed on your system to use `rocketserializer`.
41-
We recommend downloading Java 17, which is required to run OpenRocket-23.09.
41+
We recommend downloading Java 17, which is required to run recent OpenRocket
42+
JARs (for example OpenRocket-24.12).
4243

4344
https://www.oracle.com/java/technologies/downloads/
4445

@@ -47,7 +48,7 @@ https://www.oracle.com/java/technologies/downloads/
4748
You also need to download the OpenRocket JAR file. You can download it from the
4849
following link:
4950

50-
https://openrocket.info/downloads.html?vers=23.09#content-JAR
51+
https://openrocket.info/downloads.html
5152

5253
Each version of OpenRocket has its own jar file, and it is important to use the
5354
correct java version to run the jar file.
@@ -61,7 +62,7 @@ will be automatically installed:
6162
- click>=8.0.0
6263
- lxml
6364
- numpy
64-
- orhelper==0.1.3
65+
- jpype1<1.5
6566
- pyyaml
6667
- rocketpy>=1.1.0
6768
- nbformat>=5.2.0
@@ -89,7 +90,7 @@ The options are the following:
8990

9091
- `--filepath`: The .ork file to be serialized.
9192
- `--output` : Path to the output folder. If not set, the output will be saved in the same folder as the `filepath`.
92-
- `--ork_jar` : Specify the path to the OpenRocket jar file. If not set, the library will try to find the jar file in the current directory.
93+
- `--ork_jar` : Specify the path to the OpenRocket jar file. If not set, the library will use the newest `OpenRocket*.jar` found in the current directory.
9394
- `--encoding` : The encoding of the .ork file. By default, it is set to `utf-8`.
9495
- `--verbose` : If you want to see the progress of the serialization, set this option to True. By default, it is set to False.
9596

@@ -139,4 +140,4 @@ The 3 main ways of contributing to this project are:
139140
- If you allow us to use and share your .ork file, we can add it to the test suite.
140141
3. **Developing new features and fixing bugs thorough pull requests on GitHub.**
141142
- If you want to develop new features, you are more than welcome to do so.
142-
- Please reach out to the maintainers to discuss the new feature before starting the development.
143+
- Please reach out to the maintainers to discuss the new feature before starting the development.

requirements.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ bs4
22
click>=8.0.0
33
lxml
44
numpy
5-
orhelper==0.1.3
5+
jpype1<1.5
66
pyyaml
77
rocketpy>=1.1.0
88
nbformat>=5.2.0

rocketserializer/cli.py

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
from pathlib import Path
55

66
import click
7-
import orhelper
87

98
from ._helpers import extract_ork_from_zip, parse_ork_file
109
from .nb_builder import NotebookBuilder
10+
from .openrocket_runtime import OpenRocketSession, select_latest_openrocket_jar
1111
from .ork_extractor import ork_extractor
1212

1313
logging.basicConfig(
@@ -138,18 +138,15 @@ def ork2json(filepath, output=None, ork_jar=None, encoding="utf-8", verbose=Fals
138138
raise ValueError(message)
139139

140140
if not ork_jar:
141-
# get any .jar file in the current directory that starts with "OpenRocket"
142-
ork_jar = [
143-
f for f in os.listdir() if f.startswith("OpenRocket") and f.endswith(".jar")
144-
]
145-
if len(ork_jar) == 0:
146-
raise ValueError(
147-
"[ork2json] It was not possible to find the OpenRocket .jar file in "
148-
"the current directory. Please specify the path to the .jar file."
149-
)
150-
ork_jar = ork_jar[0]
151-
logger.info(
152-
"[ork2json] Found OpenRocket .jar file: '%s'", Path(ork_jar).as_posix()
141+
ork_jar = select_latest_openrocket_jar(Path.cwd())
142+
logger.info("[ork2json] Found OpenRocket .jar file: '%s'", ork_jar.as_posix())
143+
else:
144+
ork_jar = Path(ork_jar)
145+
146+
if not ork_jar.exists():
147+
raise FileNotFoundError(
148+
"[ork2json] The specified OpenRocket .jar file does not exist: "
149+
f"'{ork_jar.as_posix()}'"
153150
)
154151

155152
if not output:
@@ -160,17 +157,12 @@ def ork2json(filepath, output=None, ork_jar=None, encoding="utf-8", verbose=Fals
160157
Path(output).as_posix(),
161158
)
162159

163-
# orhelper options are: OFF, ERROR, WARN, INFO, DEBUG, TRACE and ALL
164-
# log_level = "OFF" if verbose else "OFF"
165-
# TODO: even if the log level is set to OFF, the orhelper still prints msgs
166-
167-
with orhelper.OpenRocketInstance(ork_jar, log_level="OFF") as instance:
160+
with OpenRocketSession(ork_jar, log_level="OFF") as instance:
168161
# create the output folder if it does not exist
169162
if os.path.exists(output) is False:
170163
os.mkdir(output)
171164

172-
orh = orhelper.Helper(instance)
173-
ork = orh.load_doc(str(filepath))
165+
ork = instance.load_doc(str(filepath))
174166

175167
settings = ork_extractor(
176168
bs=bs,
@@ -217,21 +209,20 @@ def ork2notebook(filepath, output, ork_jar=None, encoding="utf-8", verbose=False
217209
"[ork2notebook] Output folder not specified. Using '%s' instead.",
218210
Path(output).as_posix(),
219211
)
220-
ork2json(
221-
[
222-
"--filepath",
223-
filepath,
224-
"--output",
225-
output,
226-
"--ork_jar",
227-
ork_jar,
228-
"--encoding",
229-
encoding,
230-
"--verbose",
231-
verbose,
232-
],
233-
standalone_mode=False,
234-
)
212+
args = [
213+
"--filepath",
214+
str(filepath),
215+
"--output",
216+
str(output),
217+
"--encoding",
218+
str(encoding),
219+
"--verbose",
220+
str(verbose),
221+
]
222+
if ork_jar:
223+
args.extend(["--ork_jar", str(ork_jar)])
224+
225+
ork2json(args, standalone_mode=False)
235226

236227
instance = NotebookBuilder(parameters_json=os.path.join(output, "parameters.json"))
237228
instance.build(destination=output)
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import logging
2+
import os
3+
import re
4+
from pathlib import Path
5+
6+
import jpype
7+
import jpype.imports
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
def _jar_version_tuple(jar_path: Path):
13+
match = re.search(r"OpenRocket[-_]?(\d+(?:\.\d+)*)", jar_path.name, re.IGNORECASE)
14+
if not match:
15+
return (0,)
16+
17+
version = []
18+
for token in match.group(1).split("."):
19+
if token.isdigit():
20+
version.append(int(token))
21+
return tuple(version) if version else (0,)
22+
23+
24+
def select_latest_openrocket_jar(search_dir: Path):
25+
jars = [
26+
path
27+
for path in search_dir.iterdir()
28+
if path.is_file()
29+
and path.suffix.lower() == ".jar"
30+
and path.name.lower().startswith("openrocket")
31+
]
32+
33+
if not jars:
34+
raise FileNotFoundError(
35+
"It was not possible to find an OpenRocket .jar file in the current "
36+
"directory. Please specify one explicitly with --ork_jar."
37+
)
38+
39+
jars.sort(
40+
key=lambda path: (_jar_version_tuple(path), path.name.lower()), reverse=True
41+
)
42+
return jars[0]
43+
44+
45+
def _extract_java_major(value: str):
46+
if not value:
47+
return None
48+
49+
normalized = value.replace("\\", "/").lower()
50+
51+
legacy = re.search(r"(?:jdk|jre)[-_]?1\.(\d+)", normalized)
52+
if legacy:
53+
return int(legacy.group(1))
54+
55+
modern = re.search(r"(?:jdk|jre|java)[-_]?(\d{2})", normalized)
56+
if modern:
57+
return int(modern.group(1))
58+
59+
return None
60+
61+
62+
def _minimum_java_required(jar_path: Path):
63+
version = _jar_version_tuple(jar_path)
64+
if version and version[0] >= 23:
65+
return 17
66+
return 8
67+
68+
69+
def _find_windows_jdk(minimum_major: int):
70+
search_roots = [
71+
Path("C:/Program Files/Java"),
72+
Path("C:/Program Files/Eclipse Adoptium"),
73+
Path("C:/Program Files/AdoptOpenJDK"),
74+
]
75+
76+
candidates = []
77+
for root in search_roots:
78+
if not root.exists():
79+
continue
80+
81+
for child in root.iterdir():
82+
if not child.is_dir():
83+
continue
84+
major = _extract_java_major(child.name)
85+
if major is None:
86+
continue
87+
if major >= minimum_major and (child / "bin" / "java.exe").exists():
88+
candidates.append((major, child))
89+
90+
if not candidates:
91+
return None
92+
93+
candidates.sort(key=lambda item: item[0], reverse=True)
94+
return candidates[0][1]
95+
96+
97+
def ensure_java_compatibility(jar_path: Path):
98+
required_major = _minimum_java_required(jar_path)
99+
if required_major <= 8:
100+
return
101+
102+
java_home_major = _extract_java_major(os.environ.get("JAVA_HOME", ""))
103+
if java_home_major and java_home_major >= required_major:
104+
return
105+
106+
default_jvm_major = None
107+
try:
108+
default_jvm_major = _extract_java_major(jpype.getDefaultJVMPath())
109+
except Exception:
110+
default_jvm_major = None
111+
112+
if default_jvm_major and default_jvm_major >= required_major:
113+
return
114+
115+
if os.name != "nt":
116+
logger.warning(
117+
"OpenRocket %s requires Java %d+, but no compatible JVM was detected.",
118+
jar_path.name,
119+
required_major,
120+
)
121+
return
122+
123+
selected_jdk = _find_windows_jdk(required_major)
124+
if not selected_jdk:
125+
logger.warning(
126+
"OpenRocket %s requires Java %d+, but no compatible JDK was found in "
127+
"standard Windows locations.",
128+
jar_path.name,
129+
required_major,
130+
)
131+
return
132+
133+
os.environ["JAVA_HOME"] = str(selected_jdk)
134+
os.environ["PATH"] = (
135+
str(selected_jdk / "bin") + os.pathsep + os.environ.get("PATH", "")
136+
)
137+
logger.info(
138+
"Using Java from '%s' for OpenRocket compatibility.", selected_jdk.as_posix()
139+
)
140+
141+
142+
class OpenRocketSession:
143+
def __init__(self, jar_path, log_level="OFF"):
144+
self.jar_path = Path(jar_path)
145+
if not self.jar_path.exists():
146+
raise FileNotFoundError(
147+
f"Jar file '{self.jar_path.as_posix()}' does not exist"
148+
)
149+
150+
self.log_level = log_level
151+
self.openrocket = None
152+
self.started = False
153+
154+
def _resolve_packages(self):
155+
try:
156+
legacy = jpype.JPackage("net").sf.openrocket
157+
_ = legacy.startup.Application
158+
return legacy, legacy
159+
except Exception:
160+
modern = jpype.JPackage("info").openrocket
161+
return modern.core, modern.swing
162+
163+
@staticmethod
164+
def _block_loader(gui_module, field_name):
165+
try:
166+
field = gui_module.getClass().getDeclaredField(field_name)
167+
field.setAccessible(True)
168+
loader = field.get(gui_module)
169+
field.setAccessible(False)
170+
loader.blockUntilLoaded()
171+
except Exception:
172+
# New OpenRocket versions can change internals; loading still works
173+
# without explicitly waiting in most cases.
174+
return
175+
176+
def __enter__(self):
177+
ensure_java_compatibility(self.jar_path)
178+
179+
jvm_path = jpype.getDefaultJVMPath()
180+
logger.info(
181+
"Starting JVM from '%s' with OpenRocket '%s'",
182+
jvm_path,
183+
self.jar_path.as_posix(),
184+
)
185+
186+
jpype.startJVM(
187+
jvm_path,
188+
"-ea",
189+
f"-Djava.class.path={self.jar_path.as_posix()}",
190+
)
191+
192+
self.openrocket, swing = self._resolve_packages()
193+
194+
guice = jpype.JPackage("com").google.inject.Guice
195+
logger_factory = jpype.JPackage("org").slf4j.LoggerFactory
196+
logger_class = jpype.JPackage("ch").qos.logback.classic.Logger
197+
logger_level = jpype.JPackage("ch").qos.logback.classic.Level
198+
199+
gui_module = swing.startup.GuiModule()
200+
plugin_module = self.openrocket.plugin.PluginModule()
201+
202+
injector = guice.createInjector(gui_module, plugin_module)
203+
204+
app = self.openrocket.startup.Application
205+
app.setInjector(injector)
206+
207+
gui_module.startLoader()
208+
self._block_loader(gui_module, "presetLoader")
209+
self._block_loader(gui_module, "motorLoader")
210+
211+
root_logger = logger_factory.getLogger(logger_class.ROOT_LOGGER_NAME)
212+
root_logger.setLevel(
213+
getattr(logger_level, str(self.log_level), logger_level.ERROR)
214+
)
215+
216+
self.started = True
217+
return self
218+
219+
def __exit__(self, ex_type, ex, tb):
220+
try:
221+
if jpype.isJVMStarted():
222+
try:
223+
for window in jpype.java.awt.Window.getWindows():
224+
window.dispose()
225+
except Exception:
226+
pass
227+
jpype.shutdownJVM()
228+
finally:
229+
self.started = False
230+
231+
def load_doc(self, ork_filename: str):
232+
if not self.started:
233+
raise RuntimeError("OpenRocketSession has not been started")
234+
235+
java_file = jpype.java.io.File(ork_filename)
236+
loader = self.openrocket.file.GeneralRocketLoader(java_file)
237+
return loader.load()

0 commit comments

Comments
 (0)