diff --git a/src/fprime/fbuild/builder.py b/src/fprime/fbuild/builder.py index 1036cfff..050bf736 100644 --- a/src/fprime/fbuild/builder.py +++ b/src/fprime/fbuild/builder.py @@ -313,7 +313,7 @@ def get_cmake_args(self) -> dict: if "FPRIME_LIBRARY_LOCATIONS" in cmake_args: cmake_args["FPRIME_LIBRARY_LOCATIONS"] = ";".join( - cmake_args["FPRIME_LIBRARY_LOCATIONS"] + [str(location) for location in cmake_args["FPRIME_LIBRARY_LOCATIONS"]] ) # When the new v3 autocoder directory exists, this means we can use the new UT api and preserve the build type v3_autocoder_directory = Path( @@ -495,7 +495,11 @@ def __setup_default(self, platform: str = None, build_dir: Path = None): assert self.platform is None, "Already setup it is invalid to re-setup" assert self.build_dir is None, "Already setup it is invalid to re-setup" - self.settings = IniSettings.load(self.deployment / "settings.ini") + self.settings = IniSettings.load( + self.deployment / "settings.ini", + platform, + self.build_type == BuildType.BUILD_TESTING, + ) if platform is not None and platform != "default": self.platform = platform @@ -503,9 +507,6 @@ def __setup_default(self, platform: str = None, build_dir: Path = None): self.platform = self.settings.get("default_ut_toolchain", "native") else: self.platform = self.settings.get("default_toolchain", "native") - self.settings.update( - IniSettings.load(self.deployment / "settings.ini", self.platform) - ) self.build_dir = build_dir if build_dir is not None else self.get_build_cache() diff --git a/src/fprime/fbuild/settings.py b/src/fprime/fbuild/settings.py index fe0f27b4..1c990f0b 100644 --- a/src/fprime/fbuild/settings.py +++ b/src/fprime/fbuild/settings.py @@ -8,30 +8,73 @@ """ import os import configparser +from functools import partial +from enum import Enum from typing import Dict, List from pathlib import Path +class SettingType(Enum): + """Designates the type of the setting""" + + PATH = 0 + PATH_LIST = 1 + STRING = 2 + + +def find_fprime(settings: dict) -> Path: + """ + Finds F prime by recursing parent to parent until a matching directory is found. + """ + needle = Path("cmake/FPrime.cmake") + path = settings["_deployment"] + while path != path.parent: + if (path / needle).is_file(): + return path + path = path.parent + raise FprimeLocationUnknownException( + "Please set 'framework_path' in [fprime] section in 'settings.ini" + ) + + +def join(key: Path, addition: str, settings: dict): + """Joins a settings key to the addition""" + return settings[key] / addition + + class IniSettings: """Class to load settings from INI files""" DEF_FILE = "settings.ini" SET_ENV = "FPRIME_SETTINGS_FILE" - @staticmethod - def find_fprime(cwd: Path) -> Path: - """ - Finds F prime by recursing parent to parent until a matching directory is found. - """ - needle = Path("cmake/FPrime.cmake") - path = cwd.resolve() - while path != path.parent: - if Path(path, needle).is_file(): - return path - path = path.parent - raise FprimeLocationUnknownException( - "Please set 'framework_path' in [fprime] section in 'settings.ini" - ) + FPRIME_FIELDS = [ + ("framework_path", SettingType.PATH, find_fprime), + ("project_root", SettingType.PATH, lambda settings: settings["framework_path"]), + ("default_toolchain", SettingType.STRING, "native"), + ("default_ut_toolchain", SettingType.STRING, "native"), + ("library_locations", SettingType.PATH_LIST, []), + ("component_cookiecutter", SettingType.STRING, "default"), + ] + + PLATFORM_FIELDS = [ + ("config_dir", SettingType.PATH, partial(join, "framework_path", "config")), + ( + "ac_constants", + SettingType.PATH, + partial(join, "config_dir", "AcConstants.ini"), + ), + ( + "install_dest", + SettingType.PATH, + partial(join, "_deployment", "build-artifacts"), + ), + ( + "environment_file", + SettingType.PATH, + lambda settings: settings["settings_file"], + ), + ] @staticmethod def read_safe_path( @@ -40,7 +83,7 @@ def read_safe_path( key: str, ini_file: Path, exists: bool = True, - ) -> List[str]: + ) -> List[Path]: """ Reads path(s), safely, from the config parser. Validates the path(s) exists or raises an exception. Paths are separated by ':'. This will also expand relative paths relative to the settings file. @@ -61,17 +104,56 @@ def read_safe_path( raise FprimeSettingsException( f"Nonexistent path '{path}' found in section '{section}' option '{key}' of file '{ini_file}'" ) - expanded.append(full_path) + expanded.append(Path(full_path).resolve()) return expanded @staticmethod - def load(settings_file: Path, platform: str = "fprime"): + def read_setting( + config_parser: configparser.ConfigParser, + settings: dict, + section: str, + key: str, + settings_type: SettingType, + default, + ): + """Reads an individual setting""" + get_default_value = lambda: default(settings) if callable(default) else default + + if config_parser is None: + value = get_default_value() + elif settings_type == SettingType.STRING: + value = config_parser.get(section, key, fallback=get_default_value()) + elif settings_type == SettingType.PATH: + paths_list = IniSettings.read_safe_path( + config_parser, + section, + key, + settings["settings_file"], + key != "install_dest", + ) + value = paths_list[0] if paths_list else get_default_value() + elif settings_type == SettingType.PATH_LIST: + paths_list = IniSettings.read_safe_path( + config_parser, + section, + key, + settings["settings_file"], + key != "install_dest", + ) + value = paths_list if paths_list else get_default_value() + else: + raise FprimeSettingsException("Invalid settings specification") + return value + + @staticmethod + def load(settings_file: Path, platform: str = "native", is_ut: bool = False): """ Load settings from specified file or from specified build directory. Either a specific file or the build directory must be not None. :param settings_file: file to load settings from (in INI format). Must be specified if build_dir is not. :param platform: platform to read platform specific settings + :param is_ut: is this a unit test build :return: a dictionary of needed settings """ settings_file = ( @@ -79,82 +161,47 @@ def load(settings_file: Path, platform: str = "fprime"): if settings_file is None else settings_file ).resolve() - dfl_install_dest = Path(settings_file.parent, "build-artifacts") - - # Check file existence if specified - if not os.path.exists(settings_file): - print(f"[WARNING] Failed to find settings file: {settings_file}") - fprime_location = IniSettings.find_fprime(settings_file.parent) - return {"framework_path": fprime_location, "install_dest": dfl_install_dest} - confparse = configparser.ConfigParser() - confparse.read(settings_file) - # Search through F prime locations - fprime_location = IniSettings.read_safe_path( - confparse, "fprime", "framework_path", settings_file - ) - fprime_location = ( - Path(fprime_location[0]) - if fprime_location - else IniSettings.find_fprime(settings_file.parent) - ) - # Read project root if it is available - proj_root = IniSettings.read_safe_path( - confparse, "fprime", "project_root", settings_file - ) - proj_root = proj_root[0] if proj_root else None - # Read ac constants if it is available - ac_consts = IniSettings.read_safe_path( - confparse, platform, "ac_constants", settings_file - ) - ac_consts = ac_consts[0] if ac_consts else None - # Read include constants if it is available - config_dir = IniSettings.read_safe_path( - confparse, platform, "config_directory", settings_file - ) - config_dir = config_dir[0] if config_dir else None - - install_dest = IniSettings.read_safe_path( - confparse, platform, "install_dest", settings_file, False - ) - - install_dest = Path(install_dest[0]) if install_dest else dfl_install_dest - - # Read separate environment file if necessary - env_file = IniSettings.read_safe_path( - confparse, platform, "environment_file", settings_file - ) - env_file = env_file[0] if env_file else settings_file - libraries = IniSettings.read_safe_path( - confparse, platform, "library_locations", settings_file + # Setup a config parser, or none if the settings file does not exist + confparse = None + if settings_file.exists(): + confparse = configparser.ConfigParser() + confparse.read(settings_file) + else: + print(f"[WARNING] {settings_file} does not exist") + + settings = {"settings_file": settings_file, "_deployment": settings_file.parent} + + # Read fprime and platform settings from the "fprime" section + for key, settings_type, default in ( + IniSettings.FPRIME_FIELDS + IniSettings.PLATFORM_FIELDS + ): + settings[key] = IniSettings.read_setting( + confparse, settings, "fprime", key, settings_type, default + ) + + # Calculate the platform if not specified + if not platform or platform == "default": + platform = ( + settings["default_ut_toolchain"] + if is_ut + else settings["default_toolchain"] + ) + + # Read platform settings overtop of fprime settings + for key, settings_type, default in IniSettings.PLATFORM_FIELDS: + settings[key] = IniSettings.read_setting( + confparse, + settings, + platform, + key, + settings_type, + settings.get(key, default), + ) + + settings["environment"] = IniSettings.load_environment( + settings["environment_file"] ) - environment = IniSettings.load_environment(env_file) - - settings = { - "settings_file": settings_file, - "framework_path": fprime_location, - "library_locations": libraries, - "default_toolchain": confparse.get( - "fprime", "default_toolchain", fallback="native" - ), - "default_ut_toolchain": confparse.get( - "fprime", "default_ut_toolchain", fallback="native" - ), - "install_dest": install_dest, - "environment_file": env_file, - "environment": environment, - "component_cookiecutter": confparse.get( - "fprime", "component_cookiecutter", fallback="default" - ), - } - # Set the project root - if proj_root is not None: - settings["project_root"] = proj_root - # Set AC constants if available - if ac_consts is not None: - settings["ac_constants"] = ac_consts - # Set the config dir - if config_dir is not None: - settings["config_dir"] = config_dir + del settings["_deployment"] return settings @staticmethod diff --git a/test/fprime/fbuild/test_settings.py b/test/fprime/fbuild/test_settings.py index c8d10d45..e770d8b2 100644 --- a/test/fprime/fbuild/test_settings.py +++ b/test/fprime/fbuild/test_settings.py @@ -36,6 +36,9 @@ def test_settings(): "environment_file": full_path("settings-data/settings-empty.ini"), "environment": {}, "component_cookiecutter": "default", + "ac_constants": full_path("..") / "config" / "AcConstants.ini", + "project_root": full_path(".."), + "config_dir": full_path("..") / "config", }, }, { @@ -52,6 +55,9 @@ def test_settings(): ), "environment": {}, "component_cookiecutter": "default", + "ac_constants": full_path("..") / "config" / "AcConstants.ini", + "project_root": full_path(".."), + "config_dir": full_path("..") / "config", }, }, { @@ -70,6 +76,9 @@ def test_settings(): ), "environment": {}, "component_cookiecutter": "default", + "ac_constants": full_path("..") / "config" / "AcConstants.ini", + "project_root": full_path(".."), + "config_dir": full_path("..") / "config", }, }, { @@ -88,6 +97,9 @@ def test_settings(): ), "environment": {}, "component_cookiecutter": "gh:SterlingPeet/cookiecutter-fprime-deployment", + "ac_constants": full_path("..") / "config" / "AcConstants.ini", + "project_root": full_path(".."), + "config_dir": full_path("..") / "config", }, }, ]