Coverage for apio/utils/resource_util.py: 95%

69 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-07 05:08 +0000

1# This file is part of the Apio project 

2# (C) 2016-present FPGAwars 

3# Project authors: 

4# * Jesús Arroyo (Jesus89) (2016-2019) 

5# * Juan Gonzalez (obijuan) (2019-2024) 

6# * The Apio Team (2024-present) 

7# License: GPLv2 

8 

9"""Utilities related to the Apio resource files.""" 

10 

11import json 

12import re 

13from pathlib import Path 

14from dataclasses import dataclass 

15from apio.utils import util, jsonc 

16from apio.common.apio_console import fatal_error 

17from apio.common.apio_platforms import get_apio_platforms 

18from apio.common import proto_util 

19from apio.common.proto.stubs.apio_config_pb2 import ( 

20 ApioRawConfig, 

21 ApioRemoteConfigSpec, 

22 ApioPlatformEnvSpec, 

23 ApioPackageSpec, 

24) 

25from apio.common.apio_platforms import ApioPlatform 

26 

27# -- Relative path from apio root to the config file. 

28CONFIG_JSON = "resources/config.jsonc" 

29 

30 

31@dataclass(frozen=True) 

32class ApioConfig: 

33 """Represents the information extracted from the config file for the 

34 scope of current platform.""" 

35 

36 platform_id: str 

37 all_packages: dict[str, ApioPackageSpec] 

38 platform_packages: dict[str, ApioPackageSpec] 

39 platform_env: ApioPlatformEnvSpec 

40 remote_config: ApioRemoteConfigSpec 

41 

42 def __post_init__(self) -> None: 

43 """Verify member types.""" 

44 assert isinstance(self.platform_id, str) 

45 assert isinstance(self.all_packages, dict) 

46 assert isinstance(self.platform_packages, dict) 

47 assert isinstance(self.platform_env, ApioPlatformEnvSpec) 

48 assert isinstance(self.remote_config, ApioRemoteConfigSpec) 

49 

50 # Optional: check dict value types 

51 for k, v in self.all_packages.items(): 

52 assert isinstance(k, str) 

53 assert isinstance(v, ApioPackageSpec) 

54 

55 for k, v in self.platform_packages.items(): 

56 assert isinstance(k, str) 

57 assert isinstance(v, ApioPackageSpec) 

58 

59 

60def _resolve_str_placeholders(text: str, mapping: dict) -> str: 

61 """Replace all {key} place holder with their respective values.""" 

62 

63 def replacer(match): 

64 key = match.group(1) 

65 assert key in mapping, f"Unknown placeholder '{key}'" 

66 return str(mapping[key]) 

67 

68 return re.sub(r"\{([^{}]+)\}", replacer, text) 

69 

70 

71def read_apio_config_file( 

72 platform: ApioPlatform, packages_dir: Path 

73) -> ApioConfig: 

74 """Read and validate the config.json resource file.""" 

75 

76 # pylint: disable=broad-exception-caught 

77 # pylint: disable=too-many-locals 

78 

79 # -- Find config file path within the Apio package 

80 filepath = util.get_path_in_apio_package(CONFIG_JSON) 

81 

82 # -- Read the and parse the jsonc file 

83 try: 

84 jsonc_text = filepath.read_text(encoding="utf-8") 

85 json_text = jsonc.to_json(jsonc_text) 

86 json_dict = json.loads(json_text) 

87 

88 except Exception as e: 

89 fatal_error( 

90 "Failed to read Apio json config file file", 

91 f"File path: '{str(filepath)}'", 

92 cause=e, 

93 ) 

94 

95 # -- Convert to proto 

96 apio_config: ApioRawConfig | None = proto_util.proto_from_json_dict( 

97 json_dict, 

98 ApioRawConfig, 

99 ) 

100 if apio_config is None: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true

101 fatal_error("Failed to parse apio config as a protocol buffer") 

102 

103 # -- Verify that config includes all platforms 

104 for p in get_apio_platforms(): 

105 assert p in apio_config.platforms, f"Missing platform {p}" 

106 

107 # -- verify that config doesn't have additional unknown platforms. 

108 assert len(get_apio_platforms()) == len( 

109 apio_config.platforms 

110 ), "Extra platforms in config files." 

111 

112 # -- Verify remote config params. 

113 assert apio_config.remote_config.ttl_days >= 1, apio_config 

114 assert apio_config.remote_config.retry_minutes >= 0, apio_config 

115 

116 # -- Get the spec of current platform. 

117 platform_spec = apio_config.platforms[platform.id] 

118 

119 # -- Construct a map with all packages specs. 

120 all_packages_dict: dict[str, ApioPackageSpec] = {} 

121 for k, v in apio_config.packages.items(): 

122 all_packages_dict[k] = v 

123 

124 # -- Construct a map with platform packages specs only. 

125 platform_packages_dict: dict[str, ApioPackageSpec] = {} 

126 for k, v in apio_config.packages.items(): 

127 if k in platform_spec.packages: 

128 platform_packages_dict[k] = v 

129 

130 # -- Construct the result. 

131 result = ApioConfig( 

132 platform_id=platform.id, 

133 all_packages=all_packages_dict, 

134 platform_packages=platform_packages_dict, 

135 platform_env=platform_spec.env, 

136 remote_config=apio_config.remote_config, 

137 ) 

138 

139 # -- Construct mapping of package name to package string path. 

140 # -- We will use it to resolve the placeholders. 

141 mapping: dict[str, str] = {} 

142 for package_name in result.platform_packages: 

143 mapping[package_name] = str(packages_dir / package_name) 

144 

145 # -- NOTE: No need resolve delete_env_vars since they have no values. 

146 

147 # -- Resolve placeholders in add_to_path 

148 new_list = [ 

149 _resolve_str_placeholders(v, mapping) 

150 for v in result.platform_env.add_to_path 

151 ] 

152 result.platform_env.add_to_path.clear() 

153 result.platform_env.add_to_path.extend(new_list) 

154 

155 # -- Resolve placeholders in add_env_var 

156 new_dict = { 

157 k: _resolve_str_placeholders(v, mapping) 

158 for k, v in result.platform_env.add_env_vars.items() 

159 } 

160 result.platform_env.add_env_vars.clear() 

161 result.platform_env.add_env_vars.update(new_dict) 

162 

163 # -- Resolve placeholders in define_consts 

164 new_dict = { 

165 k: _resolve_str_placeholders(v, mapping) 

166 for k, v in result.platform_env.define_consts.items() 

167 } 

168 result.platform_env.define_consts.clear() 

169 result.platform_env.define_consts.update(new_dict) 

170 

171 # -- All done 

172 return result