diff --git a/src/fprime/fbuild/settings.py b/src/fprime/fbuild/settings.py index dc13a7ad..5ae3b70e 100644 --- a/src/fprime/fbuild/settings.py +++ b/src/fprime/fbuild/settings.py @@ -16,6 +16,37 @@ from typing import Any, Callable, Dict, List, Union +class EnvironmentVariableInterpolation(configparser.BasicInterpolation): + """Interpolation for environment variables embedded in setting.ini + + settings.ini will extrapolate $/${} as an environment variable. This will allow basic environment variable + replacement. It is illegal to supply an environment variable with a $ or %( as part of the value because that might + trigger unanticipated recursive parsing. + + Note: environment variable substitution is only performed in the environment section and nowhere else. + + Based off: https://stackoverflow.com/questions/26586801/configparser-and-string-interpolation-with-env-variable + """ + + def before_get(self, parser, section, option, value, defaults): + """Pre-process sections to replace environment variables + + Runs before the value is gotten to replace environment variables. It will use os.path.expandvars to do the + actual substitution on any value in the "environment" section. Other pre-processing is done first. + + Args: + parser: unused + section: environment substitution will be done only when set to "environment" + option: unused + value: will be searched for and replace environment variables + defaults: unused + Returns: + the value post substitution + """ + value = super().before_get(parser, section, option, value, defaults) + return os.path.expandvars(value) if section == "environment" else value + + class SettingType(Enum): """Designates the type of the setting""" @@ -171,7 +202,9 @@ def load(settings_file: Path, platform: str = "native", is_ut: bool = False): # Setup a config parser, or none if the settings file does not exist confparse = None if settings_file.exists(): - confparse = configparser.ConfigParser() + confparse = configparser.ConfigParser( + interpolation=EnvironmentVariableInterpolation() + ) confparse.read(settings_file) else: print(f"[WARNING] {settings_file} does not exist", file=sys.stderr) @@ -235,7 +268,9 @@ def load_environment(env_file): :param env_file: load environment from this file :return: environment dictionary """ - parser = configparser.ConfigParser() + parser = configparser.ConfigParser( + interpolation=EnvironmentVariableInterpolation() + ) parser.optionxform = str parser.read(env_file) env_dict = {} diff --git a/test/fprime/fbuild/settings-data/settings-environment.ini b/test/fprime/fbuild/settings-data/settings-environment.ini new file mode 100644 index 00000000..8d698691 --- /dev/null +++ b/test/fprime/fbuild/settings-data/settings-environment.ini @@ -0,0 +1,6 @@ +[fprime] +framework_path: ../.. + +[environment] +MY_VARIABLE: my value +MY_VARIABLE_2: ${TEST_SETTING_1}:$TEST_SETTING_2 diff --git a/test/fprime/fbuild/test_settings.py b/test/fprime/fbuild/test_settings.py index 8de2bef0..3fcb1f06 100644 --- a/test/fprime/fbuild/test_settings.py +++ b/test/fprime/fbuild/test_settings.py @@ -4,7 +4,7 @@ Tests the F prime settings module. @author joshuaa """ - +import os from pathlib import Path from fprime.fbuild.settings import IniSettings @@ -126,8 +126,28 @@ def test_settings(): "default_cmake_options": "OPTION1=ABC\nOPTION2=123\nOPTION3=Something", }, }, + { + "file": "settings-environment.ini", + "expected": { + "settings_file": full_path("settings-data/settings-environment.ini"), + "default_toolchain": "native", + "default_ut_toolchain": "native", + "framework_path": full_path(".."), + "install_destination": full_path("settings-data/build-artifacts"), + "library_locations": [], + "environment_file": full_path("settings-data/settings-environment.ini"), + "environment": {"MY_VARIABLE": "my value", "MY_VARIABLE_2": "abc:123"}, + "component_cookiecutter": "default", + "deployment_cookiecutter": "default", + "project_root": full_path(".."), + "config_directory": full_path("..") / "config", + "default_cmake_options": "", + }, + }, ] - + # Prep for substitution + os.environ["TEST_SETTING_1"] = "abc" + os.environ["TEST_SETTING_2"] = "123" for case in test_cases: fp = full_path("settings-data/" + case["file"]) results = IniSettings.load(fp)