Coverage for pydelica/compiler.py: 68%
158 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-12 09:07 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-12 09:07 +0000
1import glob
2import logging
3import os
4import platform
5import shutil
6import subprocess
7import tempfile
8import pydantic
9import pathlib
11import pydelica.exception as pde
12from pydelica.options import LibrarySetup
15class Compiler:
16 """Performs compilation of Modelica using the OMC compiler"""
18 def __init__(self) -> None:
19 """Initialise a compiler object
21 Parameters
22 ----------
23 open_modelica_library_path : str, optional
24 location of the OM libraries, else use defaults for system
26 Raises
27 ------
28 pde.BinaryNotFoundError
29 if an OMC compiler binary could not be found
30 """
31 self._logger = logging.getLogger("PyDelica.Compiler")
32 self._environment = os.environ.copy()
33 self._omc_flags: dict[str, str | None] = {}
34 self._binary_dirs: list[str] = []
36 # If log level is debug, set OMC to be the same
37 if self._logger.getEffectiveLevel() == logging.DEBUG:
38 self._omc_flags["-d"] = None
40 self._omc_binary = None
42 if "OPENMODELICAHOME" in os.environ:
43 _omc_cmd = "omc.exe" if platform.system() == "Windows" else "omc"
44 self._omc_binary = os.path.join(
45 os.environ["OPENMODELICAHOME"], "bin", _omc_cmd
46 )
47 elif shutil.which("omc"):
48 self._omc_binary = shutil.which("omc")
50 if not self._omc_binary:
51 raise pde.BinaryNotFoundError("Failed to find OMC binary")
53 if platform.system() == "Windows":
54 _mod_tool_bin = os.path.join(
55 os.environ["OPENMODELICAHOME"],
56 "tools",
57 "msys",
58 "mingw64",
59 "bin",
60 )
61 self._environment["PATH"] = (
62 f"{_mod_tool_bin}{os.pathsep}" + self._environment["PATH"]
63 )
65 self._environment["PATH"] = (
66 f"{os.path.dirname(self._omc_binary)}"
67 + os.pathsep
68 + self._environment["PATH"]
69 )
70 self._logger.debug(f"Using Compiler: {self._omc_binary}")
72 def clear_cache(self) -> None:
73 """Remove all build directories"""
74 for dir in self._binary_dirs:
75 shutil.rmtree(dir)
77 def set_profile_level(self, profile_level: str | None = None) -> None:
78 """ "Sets the OMC profiling level, deactivates it if None"""
79 if not profile_level:
80 self._omc_flags.pop("--profiling", None)
81 else:
82 self.set_omc_flag("--profiling", profile_level)
84 def set_omc_flag(self, flag: str, value: str | None = None) -> None:
85 """Sets a flag for the OMC compiler
87 Flags are added as:
89 omc <flag>
91 or
93 omc <flag>=<value>
95 Parameters
96 ----------
97 flag : str
98 flag to append
99 value : str, optional
100 value for the flag if appropriate
101 """
102 if value:
103 self._logger.debug(f"Setting OMC compiler flag '{flag}={value}'")
104 else:
105 self._logger.debug(f"Setting OMC compiler flag '{flag}'")
106 self._omc_flags[flag] = value
108 def remove_omc_flag(self, flag: str) -> None:
109 """Removes a flag from the OMC compiler if it exists"""
110 if flag not in self._omc_flags:
111 self._logger.debug(f"Flag '{flag}' is unset, ignoring removal")
112 return
113 self._logger.debug(f"Removing flag '{flag}' from OMC compiler")
114 self._omc_flags.pop(flag, None)
116 @pydantic.validate_call
117 def compile(
118 self,
119 modelica_source_file: pydantic.FilePath,
120 model_addr: str | None = None,
121 c_source_dir: pydantic.DirectoryPath | None = None,
122 extra_models: list[str] | None = None,
123 custom_library_spec: list[dict[str, str]] | None = None,
124 ) -> pathlib.Path:
125 """Compile Modelica source file
127 Parameters
128 ----------
129 modelica_source_file : str
130 Modelica source file to compile
131 model_addr : str, optional
132 Model within source file to compile, default is first found
133 c_source_dir : str, optional
134 directory containing any additional required C sources
135 extra_models: list[str], optional
136 Additional other model dependencies
137 custom_library_spec: list[dict[str, str]], optional
138 Use specific library versions
140 Returns
141 -------
142 pathlib.Path
143 location of output binary
144 """
145 _temp_build_dir = tempfile.mkdtemp()
147 # Check if there is a 'Resources/Include' directory in the same
148 # location as the Modelica script
150 _candidate_c_inc = modelica_source_file.parent.joinpath(
151 "Resources", "Include"
152 )
154 if os.path.exists(_candidate_c_inc) and not c_source_dir:
155 c_source_dir = _candidate_c_inc
157 with tempfile.TemporaryDirectory() as _temp_source_dir:
158 if c_source_dir:
159 self._prepare_c_incls(f"{c_source_dir}", f"{_temp_source_dir}")
160 modelica_source_file = modelica_source_file.absolute()
162 # Copy sources to a temporary source location
163 self._logger.debug(
164 "Copying sources to temporary directory '%s'", _temp_source_dir
165 )
166 _temp_model_source = os.path.join(
167 _temp_source_dir, os.path.basename(modelica_source_file)
168 )
169 shutil.copy(modelica_source_file, _temp_model_source)
171 if not modelica_source_file.exists():
172 raise FileNotFoundError(
173 f"Could not compile Modelica file '{modelica_source_file}',"
174 " file does not exist"
175 )
177 _args = [self._omc_binary, "-s", _temp_model_source]
179 if extra_models:
180 for model in extra_models:
181 _orig_model = modelica_source_file.parent.joinpath(model)
182 if not os.path.exists(_orig_model):
183 raise FileNotFoundError(
184 f"Could not compile supplementary Modelica file '{model}',"
185 " file does not exist"
186 )
187 _temp_model_source = os.path.join(
188 _temp_source_dir, os.path.basename(model)
189 )
190 shutil.copy(_orig_model, _temp_model_source)
191 _args.append(_temp_model_source)
193 _args.append("Modelica")
195 if model_addr:
196 _args.append(f"+i={model_addr}")
198 for flag, value in self._omc_flags.items():
199 if not value:
200 _args.append(flag)
201 else:
202 _args.append(f"{flag}={value}")
204 _cmd_str = " ".join(_args)
206 self._logger.debug(f"Executing Command: {_cmd_str}")
208 _gen = None
210 with LibrarySetup() as library:
211 for lib in custom_library_spec or []:
212 library.use_library(**lib)
214 # Only use custom library location if required else use default
215 _environ = os.environ.copy()
216 if library.session_library:
217 _environ["OPENMODELICALIBRARY"] = library.session_library
219 try:
220 _gen = subprocess.run(
221 _args,
222 shell=False,
223 stderr=subprocess.PIPE,
224 stdout=subprocess.PIPE,
225 text=True,
226 env=_environ,
227 cwd=_temp_build_dir,
228 )
230 pde.parse_error_string_compiler(_gen.stdout, _gen.stderr)
231 except FileNotFoundError as e:
232 self._logger.error("Failed to run command '%s'", _cmd_str)
233 self._logger.debug("PATH: %s", self._environment["PATH"])
234 if _gen:
235 self._logger.error("Traceback: %s", _gen.stdout)
236 raise e from e
237 except pde.OMExecutionError as e:
238 self._logger.error("Failed to run command '%s'", _cmd_str)
239 if _gen:
240 self._logger.error("Traceback: %s", _gen.stdout)
241 raise e from e
242 except pde.OMBuildError as e:
243 if "lexer failed" in e.args[0]:
244 self._logger.warning(e.args[0])
245 else:
246 if _gen:
247 self._logger.error("Traceback: %s", _gen.stdout)
248 raise e from e
250 if not _gen:
251 raise RuntimeError("Failed to execute model generation")
253 if _gen.returncode != 0:
254 raise pde.OMBuildError(
255 f"Model build configuration failed with exit code {_gen.returncode}:\n\t{_gen.stderr}"
256 )
258 self._logger.debug(_gen.stdout)
260 if _gen and _gen.stderr:
261 self._logger.error(_gen.stderr)
263 _make_file = glob.glob(os.path.join(_temp_build_dir, "*.makefile"))
265 if not _make_file:
266 self._logger.error(
267 "Output directory contents [%s]: %s",
268 _temp_build_dir,
269 os.listdir(_temp_build_dir),
270 )
271 raise pde.ModelicaFileGenerationError(
272 f"Failed to find a Makefile in the directory: {_temp_build_dir}, "
273 "Modelica failed to generated required files."
274 )
276 # Use the OM included MSYS Mingw32Make for Windows
277 if platform.system() == "Windows":
278 _make_binaries = glob.glob(
279 os.path.join(
280 os.environ["OPENMODELICAHOME"],
281 "tools",
282 "msys",
283 "mingw*",
284 "bin",
285 "mingw*-make.exe",
286 )
287 )
289 if not _make_binaries:
290 raise pde.BinaryNotFoundError(
291 "Failed to find Make binary in Modelica directories"
292 )
294 _make_cmd = _make_binaries[0]
296 elif not shutil.which("make"):
297 raise pde.BinaryNotFoundError("Could not find GNU-Make on this system")
298 else:
299 _make_cmd = shutil.which("make")
301 _make_file = _make_file[0]
303 _build_cmd = [_make_cmd, "-f", _make_file]
305 if platform.system() == "Windows":
306 _build_cmd.extend(("-w", "OMC_LDFLAGS_LINK_TYPE=static"))
307 self._logger.debug(f"Build Command: {' '.join(_build_cmd)}")
309 _build = subprocess.run(
310 _build_cmd,
311 shell=False,
312 stderr=subprocess.PIPE,
313 stdout=subprocess.PIPE,
314 text=True,
315 env=self._environment,
316 cwd=_temp_build_dir,
317 )
319 try:
320 pde.parse_error_string_compiler(_build.stdout, _build.stderr)
321 except pde.OMBuildError as e:
322 self._logger.error(_build.stderr)
323 raise e from e
325 if _build.stdout:
326 self._logger.debug(_build.stdout)
328 if _build.stderr:
329 self._logger.error(_build.stderr)
331 if _build.returncode != 0:
332 raise pde.OMBuildError(
333 f"Model build failed with exit code {_build.returncode}:\n\t{_build.stderr}"
334 )
336 self._binary_dirs.append(_temp_build_dir)
338 return pathlib.Path(_temp_build_dir)
340 def _prepare_c_incls(self, c_source_dir: str, _temp_dir: str) -> None:
341 self._logger.debug("Checking for C sources in '%s'", c_source_dir)
342 _c_sources = glob.glob(os.path.join(c_source_dir, "*.c"))
343 _c_sources += glob.glob(os.path.join(c_source_dir, "*.C"))
344 _include = os.path.join(_temp_dir, "Resources", "Include")
345 os.makedirs(_include)
346 for source in _c_sources:
347 _file_name = os.path.basename(source)
348 self._logger.debug("Found '%s'", _file_name)
349 shutil.copy(source, os.path.join(_include, _file_name))