Coverage for pydelica/compiler.py: 68%
157 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-19 07:38 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-19 07:38 +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 if "OPENMODELICAHOME" in os.environ:
41 _omc_cmd = "omc.exe" if platform.system() == "Windows" else "omc"
42 self._omc_binary = os.path.join(
43 os.environ["OPENMODELICAHOME"], "bin", _omc_cmd
44 )
45 elif shutil.which("omc"):
46 self._omc_binary = shutil.which("omc")
48 if not self._omc_binary:
49 raise pde.BinaryNotFoundError("Failed to find OMC binary")
51 if platform.system() == "Windows":
52 _mod_tool_bin = os.path.join(
53 os.environ["OPENMODELICAHOME"],
54 "tools",
55 "msys",
56 "mingw64",
57 "bin",
58 )
59 self._environment["PATH"] = (
60 f"{_mod_tool_bin}{os.pathsep}" + self._environment["PATH"]
61 )
63 self._environment["PATH"] = (
64 f"{os.path.dirname(self._omc_binary)}"
65 + os.pathsep
66 + self._environment["PATH"]
67 )
68 self._logger.debug(f"Using Compiler: {self._omc_binary}")
70 def clear_cache(self) -> None:
71 """Remove all build directories"""
72 for dir in self._binary_dirs:
73 shutil.rmtree(dir)
75 def set_profile_level(self, profile_level: str | None = None) -> None:
76 """ "Sets the OMC profiling level, deactivates it if None"""
77 if not profile_level:
78 self._omc_flags.pop("--profiling", None)
79 else:
80 self.set_omc_flag("--profiling", profile_level)
82 def set_omc_flag(self, flag: str, value: str | None = None) -> None:
83 """Sets a flag for the OMC compiler
85 Flags are added as:
87 omc <flag>
89 or
91 omc <flag>=<value>
93 Parameters
94 ----------
95 flag : str
96 flag to append
97 value : str, optional
98 value for the flag if appropriate
99 """
100 if value:
101 self._logger.debug(f"Setting OMC compiler flag '{flag}={value}'")
102 else:
103 self._logger.debug(f"Setting OMC compiler flag '{flag}'")
104 self._omc_flags[flag] = value
106 def remove_omc_flag(self, flag: str) -> None:
107 """Removes a flag from the OMC compiler if it exists"""
108 if flag not in self._omc_flags:
109 self._logger.debug(f"Flag '{flag}' is unset, ignoring removal")
110 return
111 self._logger.debug(f"Removing flag '{flag}' from OMC compiler")
112 self._omc_flags.pop(flag, None)
114 @pydantic.validate_call
115 def compile(
116 self,
117 modelica_source_file: pydantic.FilePath,
118 model_addr: str | None = None,
119 c_source_dir: pydantic.DirectoryPath | None = None,
120 extra_models: list[str] | None = None,
121 custom_library_spec: list[dict[str, str]] | None = None,
122 ) -> pathlib.Path:
123 """Compile Modelica source file
125 Parameters
126 ----------
127 modelica_source_file : str
128 Modelica source file to compile
129 model_addr : str, optional
130 Model within source file to compile, default is first found
131 c_source_dir : str, optional
132 directory containing any additional required C sources
133 extra_models: list[str], optional
134 Additional other model dependencies
135 custom_library_spec: list[dict[str, str]], optional
136 Use specific library versions
138 Returns
139 -------
140 pathlib.Path
141 location of output binary
142 """
143 _temp_build_dir = tempfile.mkdtemp()
145 # Check if there is a 'Resources/Include' directory in the same
146 # location as the Modelica script
148 _candidate_c_inc = modelica_source_file.parent.joinpath(
149 "Resources", "Include"
150 )
152 if os.path.exists(_candidate_c_inc) and not c_source_dir:
153 c_source_dir = _candidate_c_inc
155 with tempfile.TemporaryDirectory() as _temp_source_dir:
156 if c_source_dir:
157 self._prepare_c_incls(f"{c_source_dir}", f"{_temp_source_dir}")
158 modelica_source_file = modelica_source_file.absolute()
160 # Copy sources to a temporary source location
161 self._logger.debug(
162 "Copying sources to temporary directory '%s'", _temp_source_dir
163 )
164 _temp_model_source = os.path.join(
165 _temp_source_dir, os.path.basename(modelica_source_file)
166 )
167 shutil.copy(modelica_source_file, _temp_model_source)
169 if not modelica_source_file.exists():
170 raise FileNotFoundError(
171 f"Could not compile Modelica file '{modelica_source_file}',"
172 " file does not exist"
173 )
175 _args = [self._omc_binary, "-s", _temp_model_source]
177 if extra_models:
178 for model in extra_models:
179 _orig_model = modelica_source_file.parent.joinpath(model)
180 if not os.path.exists(_orig_model):
181 raise FileNotFoundError(
182 f"Could not compile supplementary Modelica file '{model}',"
183 " file does not exist"
184 )
185 _temp_model_source = os.path.join(
186 _temp_source_dir, os.path.basename(model)
187 )
188 shutil.copy(_orig_model, _temp_model_source)
189 _args.append(_temp_model_source)
191 _args.append("Modelica")
193 if model_addr:
194 _args.append(f"+i={model_addr}")
196 for flag, value in self._omc_flags.items():
197 if not value:
198 _args.append(flag)
199 else:
200 _args.append(f"{flag}={value}")
202 _cmd_str = " ".join(_args)
204 self._logger.debug(f"Executing Command: {_cmd_str}")
206 _gen = None
208 with LibrarySetup() as library:
209 for lib in custom_library_spec or []:
210 library.use_library(**lib)
212 # Only use custom library location if required else use default
213 _environ = os.environ.copy()
214 if library.session_library:
215 _environ["OPENMODELICALIBRARY"] = library.session_library
217 try:
218 _gen = subprocess.run(
219 _args,
220 shell=False,
221 stderr=subprocess.PIPE,
222 stdout=subprocess.PIPE,
223 text=True,
224 env=_environ,
225 cwd=_temp_build_dir,
226 )
228 pde.parse_error_string_compiler(_gen.stdout, _gen.stderr)
229 except FileNotFoundError as e:
230 self._logger.error("Failed to run command '%s'", _cmd_str)
231 self._logger.debug("PATH: %s", self._environment["PATH"])
232 if _gen:
233 self._logger.error("Traceback: %s", _gen.stdout)
234 raise e from e
235 except pde.OMExecutionError as e:
236 self._logger.error("Failed to run command '%s'", _cmd_str)
237 if _gen:
238 self._logger.error("Traceback: %s", _gen.stdout)
239 raise e from e
240 except pde.OMBuildError as e:
241 if "lexer failed" in e.args[0]:
242 self._logger.warning(e.args[0])
243 else:
244 if _gen:
245 self._logger.error("Traceback: %s", _gen.stdout)
246 raise e from e
248 if not _gen:
249 raise RuntimeError("Failed to execute model generation")
251 if _gen.returncode != 0:
252 raise pde.OMBuildError(
253 f"Model build configuration failed with exit code {_gen.returncode}:\n\t{_gen.stderr}"
254 )
256 self._logger.debug(_gen.stdout)
258 if _gen and _gen.stderr:
259 self._logger.error(_gen.stderr)
261 _make_file = glob.glob(os.path.join(_temp_build_dir, "*.makefile"))
263 if not _make_file:
264 self._logger.error(
265 "Output directory contents [%s]: %s",
266 _temp_build_dir,
267 os.listdir(_temp_build_dir),
268 )
269 raise pde.ModelicaFileGenerationError(
270 f"Failed to find a Makefile in the directory: {_temp_build_dir}, "
271 "Modelica failed to generated required files."
272 )
274 # Use the OM included MSYS Mingw32Make for Windows
275 if platform.system() == "Windows":
276 _make_binaries = glob.glob(
277 os.path.join(
278 os.environ["OPENMODELICAHOME"],
279 "tools",
280 "msys",
281 "mingw*",
282 "bin",
283 "mingw*-make.exe",
284 )
285 )
287 if not _make_binaries:
288 raise pde.BinaryNotFoundError(
289 "Failed to find Make binary in Modelica directories"
290 )
292 _make_cmd = _make_binaries[0]
294 elif not shutil.which("make"):
295 raise pde.BinaryNotFoundError("Could not find GNU-Make on this system")
296 else:
297 _make_cmd = shutil.which("make")
299 _make_file = _make_file[0]
301 _build_cmd = [_make_cmd, "-f", _make_file]
303 if platform.system() == "Windows":
304 _build_cmd.extend(("-w", "OMC_LDFLAGS_LINK_TYPE=static"))
305 self._logger.debug(f"Build Command: {' '.join(_build_cmd)}")
307 _build = subprocess.run(
308 _build_cmd,
309 shell=False,
310 stderr=subprocess.PIPE,
311 stdout=subprocess.PIPE,
312 text=True,
313 env=self._environment,
314 cwd=_temp_build_dir,
315 )
317 try:
318 pde.parse_error_string_compiler(_build.stdout, _build.stderr)
319 except pde.OMBuildError as e:
320 self._logger.error(_build.stderr)
321 raise e from e
323 if _build.stdout:
324 self._logger.debug(_build.stdout)
326 if _build.stderr:
327 self._logger.error(_build.stderr)
329 if _build.returncode != 0:
330 raise pde.OMBuildError(
331 f"Model build failed with exit code {_build.returncode}:\n\t{_build.stderr}"
332 )
334 self._binary_dirs.append(_temp_build_dir)
336 return pathlib.Path(_temp_build_dir)
338 def _prepare_c_incls(self, c_source_dir: str, _temp_dir: str) -> None:
339 self._logger.debug("Checking for C sources in '%s'", c_source_dir)
340 _c_sources = glob.glob(os.path.join(c_source_dir, "*.c"))
341 _c_sources += glob.glob(os.path.join(c_source_dir, "*.C"))
342 _include = os.path.join(_temp_dir, "Resources", "Include")
343 os.makedirs(_include)
344 for source in _c_sources:
345 _file_name = os.path.basename(source)
346 self._logger.debug("Found '%s'", _file_name)
347 shutil.copy(source, os.path.join(_include, _file_name))