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
« 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
9"""Utilities related to the Apio resource files."""
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
27# -- Relative path from apio root to the config file.
28CONFIG_JSON = "resources/config.jsonc"
31@dataclass(frozen=True)
32class ApioConfig:
33 """Represents the information extracted from the config file for the
34 scope of current platform."""
36 platform_id: str
37 all_packages: dict[str, ApioPackageSpec]
38 platform_packages: dict[str, ApioPackageSpec]
39 platform_env: ApioPlatformEnvSpec
40 remote_config: ApioRemoteConfigSpec
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)
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)
55 for k, v in self.platform_packages.items():
56 assert isinstance(k, str)
57 assert isinstance(v, ApioPackageSpec)
60def _resolve_str_placeholders(text: str, mapping: dict) -> str:
61 """Replace all {key} place holder with their respective values."""
63 def replacer(match):
64 key = match.group(1)
65 assert key in mapping, f"Unknown placeholder '{key}'"
66 return str(mapping[key])
68 return re.sub(r"\{([^{}]+)\}", replacer, text)
71def read_apio_config_file(
72 platform: ApioPlatform, packages_dir: Path
73) -> ApioConfig:
74 """Read and validate the config.json resource file."""
76 # pylint: disable=broad-exception-caught
77 # pylint: disable=too-many-locals
79 # -- Find config file path within the Apio package
80 filepath = util.get_path_in_apio_package(CONFIG_JSON)
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)
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 )
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")
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}"
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."
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
116 # -- Get the spec of current platform.
117 platform_spec = apio_config.platforms[platform.id]
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
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
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 )
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)
145 # -- NOTE: No need resolve delete_env_vars since they have no values.
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)
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)
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)
171 # -- All done
172 return result