|
| 1 | +import os, typing, hashlib, dataclasses |
| 2 | + |
| 3 | +from .case import Case |
| 4 | +from .printer import cons |
| 5 | +from .common import MFCException, system, delete_directory, create_directory, \ |
| 6 | + format_list_to_string |
| 7 | +from .state import ARG, CFG |
| 8 | +from .run import input |
| 9 | + |
| 10 | +@dataclasses.dataclass |
| 11 | +class MFCTarget: |
| 12 | + @dataclasses.dataclass |
| 13 | + class Dependencies: |
| 14 | + all: typing.List |
| 15 | + cpu: typing.List |
| 16 | + gpu: typing.List |
| 17 | + |
| 18 | + def compute(self) -> typing.Set: |
| 19 | + r = self.all[:] |
| 20 | + r += self.gpu[:] if ARG("gpu") else self.cpu[:] |
| 21 | + |
| 22 | + return r |
| 23 | + |
| 24 | + name: str # Name of the target |
| 25 | + flags: typing.List[str] # Extra flags to pass to CMakeMFCTarget |
| 26 | + isDependency: bool # Is it a dependency of an MFC target? |
| 27 | + isDefault: bool # Should it be built by default? (unspecified -t | --targets) |
| 28 | + isRequired: bool # Should it always be built? (no matter what -t | --targets is) |
| 29 | + requires: Dependencies # Build dependencies of the target |
| 30 | + runOrder: int # For MFC Targets: Order in which targets should logically run |
| 31 | + |
| 32 | + def __hash__(self) -> int: |
| 33 | + return hash(self.name) |
| 34 | + |
| 35 | + def get_slug(self, case: Case ) -> str: |
| 36 | + if self.isDependency: |
| 37 | + return self.name |
| 38 | + |
| 39 | + m = hashlib.sha256() |
| 40 | + m.update(self.name.encode()) |
| 41 | + m.update(CFG().make_slug().encode()) |
| 42 | + m.update(case.get_fpp(self, False).encode()) |
| 43 | + |
| 44 | + if case.params.get('chemistry', 'F') == 'T': |
| 45 | + m.update(case.get_cantera_solution().name.encode()) |
| 46 | + |
| 47 | + return m.hexdigest()[:10] |
| 48 | + |
| 49 | + # Get path to directory that will store the build files |
| 50 | + def get_staging_dirpath(self, case: Case ) -> str: |
| 51 | + return os.sep.join([os.getcwd(), "build", "staging", self.get_slug(case) ]) |
| 52 | + |
| 53 | + # Get the directory that contains the target's CMakeLists.txt |
| 54 | + def get_cmake_dirpath(self) -> str: |
| 55 | + # The CMakeLists.txt file is located: |
| 56 | + # * Regular: <root>/CMakelists.txt |
| 57 | + # * Dependency: <root>/toolchain/dependencies/CMakelists.txt |
| 58 | + return os.sep.join([ |
| 59 | + os.getcwd(), |
| 60 | + os.sep.join(["toolchain", "dependencies"]) if self.isDependency else "", |
| 61 | + ]) |
| 62 | + |
| 63 | + def get_install_dirpath(self, case: Case ) -> str: |
| 64 | + # The install directory is located <root>/build/install/<slug> |
| 65 | + return os.sep.join([os.getcwd(), "build", "install", self.get_slug(case)]) |
| 66 | + |
| 67 | + def get_install_binpath(self, case: Case ) -> str: |
| 68 | + # <root>/install/<slug>/bin/<target> |
| 69 | + return os.sep.join([self.get_install_dirpath(case), "bin", self.name]) |
| 70 | + |
| 71 | + def is_configured(self, case: Case ) -> bool: |
| 72 | + # We assume that if the CMakeCache.txt file exists, then the target is |
| 73 | + # configured. (this isn't perfect, but it's good enough for now) |
| 74 | + return os.path.isfile( |
| 75 | + os.sep.join([self.get_staging_dirpath(case), "CMakeCache.txt"]) |
| 76 | + ) |
| 77 | + |
| 78 | + def get_configuration_txt(self, case: Case ) -> typing.Optional[dict]: |
| 79 | + if not self.is_configured(case): |
| 80 | + return None |
| 81 | + |
| 82 | + configpath = os.path.join(self.get_staging_dirpath(case), "configuration.txt") |
| 83 | + if not os.path.exists(configpath): |
| 84 | + return None |
| 85 | + |
| 86 | + with open(configpath) as f: |
| 87 | + return f.read() |
| 88 | + |
| 89 | + def is_buildable(self) -> bool: |
| 90 | + if ARG("no_build"): |
| 91 | + return False |
| 92 | + |
| 93 | + if self.isDependency and ARG(f"sys_{self.name}", False): |
| 94 | + return False |
| 95 | + |
| 96 | + return True |
| 97 | + |
| 98 | + def configure(self, case: Case): |
| 99 | + build_dirpath = self.get_staging_dirpath(case) |
| 100 | + cmake_dirpath = self.get_cmake_dirpath() |
| 101 | + install_dirpath = self.get_install_dirpath(case) |
| 102 | + |
| 103 | + install_prefixes = ';'.join([ |
| 104 | + t.get_install_dirpath(case) for t in self.requires.compute() |
| 105 | + ]) |
| 106 | + |
| 107 | + flags: list = self.flags.copy() + [ |
| 108 | + # Disable CMake warnings intended for developers (us). |
| 109 | + # See: https://cmake.org/cmake/help/latest/manual/cmake.1.html. |
| 110 | + "-Wno-dev", |
| 111 | + # Disable warnings about unused command line arguments. This is |
| 112 | + # useful for passing arguments to CMake that are not used by the |
| 113 | + # current target. |
| 114 | + "--no-warn-unused-cli", |
| 115 | + # Save a compile_commands.json file with the compile commands used to |
| 116 | + # build the configured targets. This is mostly useful for debugging. |
| 117 | + # See: https://cmake.org/cmake/help/latest/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html. |
| 118 | + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", |
| 119 | + # Set build type (e.g Debug, Release, etc.). |
| 120 | + # See: https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html |
| 121 | + f"-DCMAKE_BUILD_TYPE={'Debug' if ARG('debug') else 'Release'}", |
| 122 | + # Used by FIND_PACKAGE (/FindXXX) to search for packages, with the |
| 123 | + # second highest level of priority, still letting users manually |
| 124 | + # specify <PackageName>_ROOT, which has precedence over CMAKE_PREFIX_PATH. |
| 125 | + # See: https://cmake.org/cmake/help/latest/command/find_package.html. |
| 126 | + f"-DCMAKE_PREFIX_PATH={install_prefixes}", |
| 127 | + # First directory that FIND_LIBRARY searches. |
| 128 | + # See: https://cmake.org/cmake/help/latest/command/find_library.html. |
| 129 | + f"-DCMAKE_FIND_ROOT_PATH={install_prefixes}", |
| 130 | + # First directory that FIND_PACKAGE searches. |
| 131 | + # See: https://cmake.org/cmake/help/latest/variable/CMAKE_FIND_PACKAGE_REDIRECTS_DIR.html. |
| 132 | + f"-DCMAKE_FIND_PACKAGE_REDIRECTS_DIR={install_prefixes}", |
| 133 | + # Location prefix to install bin/, lib/, include/, etc. |
| 134 | + # See: https://cmake.org/cmake/help/latest/command/install.html. |
| 135 | + f"-DCMAKE_INSTALL_PREFIX={install_dirpath}", |
| 136 | + f"-DMFC_SINGLE_PRECISION={'ON' if ARG('single') else 'OFF'}" |
| 137 | + ] |
| 138 | + |
| 139 | + if ARG("verbose"): |
| 140 | + flags.append('--debug-find') |
| 141 | + |
| 142 | + if not self.isDependency: |
| 143 | + flags.append(f"-DMFC_MPI={ 'ON' if ARG('mpi') else 'OFF'}") |
| 144 | + flags.append(f"-DMFC_OpenACC={'ON' if ARG('gpu') else 'OFF'}") |
| 145 | + flags.append(f"-DMFC_GCov={ 'ON' if ARG('gcov') else 'OFF'}") |
| 146 | + flags.append(f"-DMFC_Unified={'ON' if ARG('unified') else 'OFF'}") |
| 147 | + |
| 148 | + command = ["cmake"] + flags + ["-S", cmake_dirpath, "-B", build_dirpath] |
| 149 | + |
| 150 | + delete_directory(build_dirpath) |
| 151 | + create_directory(build_dirpath) |
| 152 | + |
| 153 | + case.generate_fpp(self) |
| 154 | + |
| 155 | + if system(command).returncode != 0: |
| 156 | + raise MFCException(f"Failed to configure the [bold magenta]{self.name}[/bold magenta] target.") |
| 157 | + |
| 158 | + cons.print(no_indent=True) |
| 159 | + |
| 160 | + def build(self, case: input.MFCInputFile): |
| 161 | + case.generate_fpp(self) |
| 162 | + |
| 163 | + command = ["cmake", "--build", self.get_staging_dirpath(case), |
| 164 | + "--target", self.name, |
| 165 | + "--parallel", ARG("jobs"), |
| 166 | + "--config", 'Debug' if ARG('debug') else 'Release'] |
| 167 | + if ARG('verbose'): |
| 168 | + command.append("--verbose") |
| 169 | + |
| 170 | + if system(command).returncode != 0: |
| 171 | + raise MFCException(f"Failed to build the [bold magenta]{self.name}[/bold magenta] target.") |
| 172 | + |
| 173 | + cons.print(no_indent=True) |
| 174 | + |
| 175 | + def install(self, case: input.MFCInputFile): |
| 176 | + command = ["cmake", "--install", self.get_staging_dirpath(case)] |
| 177 | + |
| 178 | + if system(command).returncode != 0: |
| 179 | + raise MFCException(f"Failed to install the [bold magenta]{self.name}[/bold magenta] target.") |
| 180 | + |
| 181 | + cons.print(no_indent=True) |
| 182 | + |
| 183 | +# name flags isDep isDef isReq dependencies run order |
| 184 | +FFTW = MFCTarget('fftw', ['-DMFC_FFTW=ON'], True, False, False, MFCTarget.Dependencies([], [], []), -1) |
| 185 | +HDF5 = MFCTarget('hdf5', ['-DMFC_HDF5=ON'], True, False, False, MFCTarget.Dependencies([], [], []), -1) |
| 186 | +SILO = MFCTarget('silo', ['-DMFC_SILO=ON'], True, False, False, MFCTarget.Dependencies([HDF5], [], []), -1) |
| 187 | +HIPFORT = MFCTarget('hipfort', ['-DMFC_HIPFORT=ON'], True, False, False, MFCTarget.Dependencies([], [], []), -1) |
| 188 | +PRE_PROCESS = MFCTarget('pre_process', ['-DMFC_PRE_PROCESS=ON'], False, True, False, MFCTarget.Dependencies([], [], []), 0) |
| 189 | +SIMULATION = MFCTarget('simulation', ['-DMFC_SIMULATION=ON'], False, True, False, MFCTarget.Dependencies([], [FFTW], [HIPFORT]), 1) |
| 190 | +POST_PROCESS = MFCTarget('post_process', ['-DMFC_POST_PROCESS=ON'], False, True, False, MFCTarget.Dependencies([FFTW, HDF5, SILO], [], []), 2) |
| 191 | +SYSCHECK = MFCTarget('syscheck', ['-DMFC_SYSCHECK=ON'], False, False, True, MFCTarget.Dependencies([], [], [HIPFORT]), -1) |
| 192 | +DOCUMENTATION = MFCTarget('documentation', ['-DMFC_DOCUMENTATION=ON'], False, False, False, MFCTarget.Dependencies([], [], []), -1) |
| 193 | + |
| 194 | +TARGETS = { FFTW, HDF5, SILO, HIPFORT, PRE_PROCESS, SIMULATION, POST_PROCESS, SYSCHECK, DOCUMENTATION } |
| 195 | + |
| 196 | +DEFAULT_TARGETS = { target for target in TARGETS if target.isDefault } |
| 197 | +REQUIRED_TARGETS = { target for target in TARGETS if target.isRequired } |
| 198 | +DEPENDENCY_TARGETS = { target for target in TARGETS if target.isDependency } |
| 199 | + |
| 200 | +TARGET_MAP = { target.name: target for target in TARGETS } |
| 201 | + |
| 202 | +def get_target(target: typing.Union[str, MFCTarget]) -> MFCTarget: |
| 203 | + if isinstance(target, MFCTarget): |
| 204 | + return target |
| 205 | + |
| 206 | + if target in TARGET_MAP: |
| 207 | + return TARGET_MAP[target] |
| 208 | + |
| 209 | + raise MFCException(f"Target '{target}' does not exist.") |
| 210 | + |
| 211 | + |
| 212 | +def get_targets(targets: typing.List[typing.Union[str, MFCTarget]]) -> typing.List[MFCTarget]: |
| 213 | + return [ get_target(t) for t in targets ] |
| 214 | + |
| 215 | + |
| 216 | +def __build_target(target: typing.Union[MFCTarget, str], case: input.MFCInputFile, history: typing.Set[str] = None): |
| 217 | + if history is None: |
| 218 | + history = set() |
| 219 | + |
| 220 | + target = get_target(target) |
| 221 | + |
| 222 | + if target.name in history or not target.is_buildable(): |
| 223 | + return |
| 224 | + |
| 225 | + history.add(target.name) |
| 226 | + |
| 227 | + for dep in target.requires.compute(): |
| 228 | + # If we have already built and installed this target, |
| 229 | + # do not do so again. This can be inferred by whether |
| 230 | + # the target requesting this dependency is already configured. |
| 231 | + if dep.isDependency and target.is_configured(case): |
| 232 | + continue |
| 233 | + |
| 234 | + build([dep], case, history) |
| 235 | + |
| 236 | + if not target.is_configured(case): |
| 237 | + target.configure(case) |
| 238 | + |
| 239 | + target.build(case) |
| 240 | + target.install(case) |
| 241 | + |
| 242 | + |
| 243 | +def get_configured_targets(case: input.MFCInputFile) -> typing.List[MFCTarget]: |
| 244 | + return [ target for target in TARGETS if target.is_configured(case) ] |
| 245 | + |
| 246 | + |
| 247 | +def __generate_header(case: input.MFCInputFile, targets: typing.List): |
| 248 | + feature_flags = [ |
| 249 | + 'Build', |
| 250 | + format_list_to_string([ t.name for t in get_targets(targets) ], 'magenta') |
| 251 | + ] |
| 252 | + if ARG("case_optimization"): |
| 253 | + feature_flags.append(f"Case Optimized: [magenta]{ARG('input')}[/magenta]") |
| 254 | + if case.params.get('chemistry', 'F') == 'T': |
| 255 | + feature_flags.append(f"Chemistry: [magenta]{case.get_cantera_solution().source}[/magenta]") |
| 256 | + |
| 257 | + return f"[bold]{' | '.join(feature_flags or ['Generic'])}[/bold]" |
| 258 | + |
| 259 | + |
| 260 | +def build(targets = None, case: input.MFCInputFile = None, history: typing.Set[str] = None): |
| 261 | + if history is None: |
| 262 | + history = set() |
| 263 | + if isinstance(targets, (MFCTarget, str)): |
| 264 | + targets = [ targets ] |
| 265 | + if targets is None: |
| 266 | + targets = ARG("targets") |
| 267 | + |
| 268 | + targets = get_targets(list(REQUIRED_TARGETS) + targets) |
| 269 | + case = case or input.load(ARG("input"), ARG("--"), {}) |
| 270 | + case.validate_params() |
| 271 | + |
| 272 | + if len(history) == 0: |
| 273 | + cons.print(__generate_header(case, targets)) |
| 274 | + cons.print(no_indent=True) |
| 275 | + |
| 276 | + for target in targets: |
| 277 | + __build_target(target, case, history) |
| 278 | + |
| 279 | + if len(history) == 0: |
| 280 | + cons.print(no_indent=True) |
0 commit comments