Coverage for apio/managers/examples.py: 91%
111 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"""Manage apio examples"""
11import shutil
12import os
13from pathlib import Path
14from dataclasses import dataclass
15from apio.common.apio_console import cout, cstyle, fatal_error
16from apio.common.apio_styles import SUCCESS, EMPH3
17from apio.common.proto.stubs.apio_common_pb2 import ApioArch
18from apio.apio_context import ApioContext
19from apio.utils import util
20from apio.common import proto_util
23@dataclass
24class ExampleInfo:
25 """Information about a single example."""
27 board_id: str
28 example_name: str
29 path: Path
30 description: str
31 fpga_arch: str
32 fpga_vendor_part: str
33 fpga_size: str
35 @property
36 def name(self) -> str:
37 """Returns the full id of the example."""
38 return self.board_id + "/" + self.example_name
41class Examples:
42 """Manage the apio examples"""
44 def __init__(self, apio_ctx: ApioContext):
46 # -- Save the apio context.
47 self.apio_ctx = apio_ctx
49 # -- Folder where the example packages was installed
50 self.examples_dir = (
51 apio_ctx.get_package_dir("definitions") / "examples"
52 )
54 def check_dst_dir_is_empty(self, path: Path):
55 """Check that the destination directory at the path is empty. If not,
56 print an error and exit.
57 """
59 # -- Check prerequisites.
60 assert path.is_dir(), f"Not a dir: {path}"
62 # -- Get the dir content, including hidden entries.
63 dir_content: list[str] = os.listdir(path)
65 # -- We don't care about macOS
66 ignore_list = [".DS_Store"]
67 dir_content = [f for f in dir_content if f not in ignore_list]
69 # -- Error if not empty.
70 if dir_content: 70 ↛ 71line 70 didn't jump to line 71 because the condition on line 70 was never true
71 fatal_error(
72 f"Destination directory '{str(path)}' "
73 + f"is not empty (e.g, '{dir_content[0]}')."
74 )
76 def get_examples_infos(self) -> list[ExampleInfo]:
77 """Scans the examples and returns a list of ExampleInfos.
78 Returns null if an error."""
80 # pylint: disable=too-many-locals
82 # -- The context should have the board, fpgas, and programmer
83 # -- definitions.
84 assert self.apio_ctx.definitions is not None
86 # -- Collect the examples home dir each board.
87 boards_dirs: list[Path] = []
89 for board_dir in self.examples_dir.iterdir():
90 if board_dir.is_dir():
91 boards_dirs.append(board_dir)
93 # -- Collect the examples of each boards.
94 examples: list[ExampleInfo] = []
95 for board_dir in boards_dirs:
96 # -- Convert board dir to board id
97 board_id = board_dir.name
99 # -- Verify that the board id exists.
100 definitions = self.apio_ctx.definitions
101 boards_definitions = definitions.boards
102 if board_id not in boards_definitions: 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true
103 fatal_error(
104 "Apio examples contain an invalid board "
105 + f"id '{board_id}'"
106 )
108 # -- Get board definitions
109 board_definition = boards_definitions[board_id]
111 # -- Iterate board's example subdirectories.
112 for example_dir in board_dir.iterdir():
114 # -- Skip files. We care just about directories.
115 if not example_dir.is_dir():
116 continue
118 # -- Try to load description from the example info file.
119 info_file = example_dir / "info"
120 if info_file.exists(): 120 ↛ 124line 120 didn't jump to line 124 because the condition on line 120 was always true
121 with open(info_file, "r", encoding="utf-8") as f:
122 description = f.read().replace("\n", "")
123 else:
124 description = ""
126 # -- Extract the fpga arch and part number, with "" as
127 # -- default value if not found.
128 proto_util.check_is_required(board_definition, "fpga_id")
129 fpga_id = board_definition.fpga_id
130 fpga_definition = definitions.fpgas[fpga_id]
131 proto_util.check_is_required(
132 fpga_definition, "arch", "vendor_part", "size"
133 )
134 fpga_arch = ApioArch.Name(fpga_definition.arch)
135 fpga_vendor_part = fpga_definition.vendor_part
136 fpga_size = fpga_definition.size
138 # -- Append this example to the list.
139 example_info = ExampleInfo(
140 board_id=board_id,
141 example_name=example_dir.name,
142 path=example_dir,
143 description=description,
144 fpga_arch=fpga_arch,
145 fpga_vendor_part=fpga_vendor_part,
146 fpga_size=fpga_size,
147 )
148 examples.append(example_info)
150 # -- Sort in-place by acceding example name, case insensitive.
151 examples.sort(key=lambda x: x.name.lower())
153 return examples
155 def count_examples_by_board(self) -> dict[str, int]:
156 """Returns a dictionary with example count per board. Boards
157 that have no examples are not included in the dictionary."""
159 # -- Get list of examples.
160 examples: list[ExampleInfo] = self.get_examples_infos()
162 # -- Count examples by board
163 counts: dict[str, int] = {}
164 for example in examples:
165 board = example.board_id
166 old_count = counts.get(board, 0)
167 counts[board] = old_count + 1
169 # -- All done
170 return counts
172 def lookup_example_info(self, example_name) -> ExampleInfo | None:
173 """Return the example info for given example or None if not found.
174 Example_name looks like 'alhambra-ii/ledon'.
175 """
177 example_infos = self.get_examples_infos()
178 for ex in example_infos: 178 ↛ 181line 178 didn't jump to line 181 because the loop on line 178 didn't complete
179 if example_name == ex.name:
180 return ex
181 return None
183 def copy_example_files(self, example_name: str, dst_dir_path: Path):
184 """Copy the files from the given example to the destination dir.
185 If destination dir exists, it must be empty.
186 If it doesn't exist, it's created with any necessary parent.
187 The arg 'example_name' looks like 'alhambra-ii/ledon'.
188 """
190 # Check that the example name exists.
191 example_info: ExampleInfo | None = self.lookup_example_info(
192 example_name
193 )
195 if not example_info: 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true
196 fatal_error(
197 f"Example '{example_name}' not found.",
198 info=[
199 "Run 'apio example list' for the list of examples.",
200 "Expecting an example name like alhambra-ii/ledon.",
201 ],
202 )
204 # -- Get the example dir path.
205 src_example_path = example_info.path
207 # -- Prepare an empty destination directory. To avoid confusion,
208 # -- we ignore hidden files and directory.
209 if dst_dir_path.exists():
210 self.check_dst_dir_is_empty(dst_dir_path)
211 else:
212 dst_dir_path.mkdir(parents=True, exist_ok=False)
214 cout("Copying " + example_name + " example files.")
216 # -- Go though all the files in the example folder.
217 for entry_path in src_example_path.iterdir():
218 # -- Case 1: Skip 'info' files.
219 if entry_path.name == "info":
220 continue
221 # -- Case 2: Copy subdirectory.
222 if entry_path.is_dir():
223 shutil.copytree(
224 entry_path, # src
225 dst_dir_path / entry_path.name, # dst
226 dirs_exist_ok=False,
227 )
228 continue
229 # -- Case 3: Copy file.
230 shutil.copy(entry_path, dst_dir_path)
232 # -- Inform the user.
233 cout(f"Example '{example_name}' fetched successfully.", style=SUCCESS)
235 def get_board_examples(self, board_id) -> list[ExampleInfo]:
236 """Returns the list of examples with given board id."""
237 return [x for x in self.get_examples_infos() if x.board_id == board_id]
239 def copy_board_examples(self, board_id: str, dst_dir: Path):
240 """Copy the example creating the folder
241 Ex. The example alhambra-ii/ledon --> the folder alhambra-ii/ledon
242 is created
243 * INPUTS:
244 * board_id: e.g. 'alhambra-ii.
245 * dst_dir: (optional) destination directory.
246 """
248 # -- Get the working dir (current or given)
249 # dst_dir = util.resolve_project_dir(
250 # dst_dir, create_if_missing=True
251 # )
252 board_examples: list[ExampleInfo] = self.get_board_examples(board_id)
254 if not board_examples: 254 ↛ 255line 254 didn't jump to line 255 because the condition on line 254 was never true
255 fatal_error(
256 f"No examples for board '{board_id}.",
257 info=[
258 "Run 'apio examples list' for the list of examples.",
259 "Expecting a board id such as 'alhambra-ii.",
260 ],
261 )
263 # -- Build the source example path (where the example was installed)
264 src_board_dir = self.examples_dir / board_id
266 # -- If the source example path is not a folder... it is an error
267 if not src_board_dir.is_dir(): 267 ↛ 268line 267 didn't jump to line 268 because the condition on line 267 was never true
268 fatal_error(
269 f"Examples for board [{board_id}] not found.",
270 info=[
271 "Run 'apio examples list' for the list of available "
272 + "examples.",
273 "Expecting a board id such as 'alhambra-ii'.",
274 ],
275 )
277 if dst_dir.exists():
278 self.check_dst_dir_is_empty(dst_dir)
279 else:
280 cout(f"Creating directory {dst_dir}.")
281 dst_dir.mkdir(parents=True, exist_ok=False)
283 # -- Create an ignore callback to skip 'info' files.
284 ignore_callback = shutil.ignore_patterns("info")
286 cout(
287 f'Found {util.plurality(board_examples, "example")} '
288 f"for board '{board_id}'"
289 )
291 for board_example in board_examples:
292 example_name = board_example.example_name
293 styled_name = cstyle(example_name, style=EMPH3)
294 cout(f"Fetching {board_id}/{styled_name}")
295 shutil.copytree(
296 src_board_dir / example_name,
297 dst_dir / example_name,
298 dirs_exist_ok=False,
299 ignore=ignore_callback,
300 )
302 cout(
303 f"{util.plurality(board_examples, 'Example', include_num=False)} "
304 "fetched successfully.",
305 style=SUCCESS,
306 )