mpflash 1.25.1__py3-none-any.whl → 1.26.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mpflash/basicgit.py CHANGED
@@ -135,6 +135,25 @@ def get_local_tags(repo: Optional[Path] = None, minver: Optional[str] = None) ->
135
135
  return sorted(tags)
136
136
 
137
137
 
138
+ def get_current_branch(repo: Optional[Union[Path, str]] = None) -> Optional[str]:
139
+ """
140
+ Get the current branch name of a local repository.
141
+
142
+ Args:
143
+ repo: Path to the repository directory
144
+
145
+ Returns:
146
+ Current branch name or None if error
147
+ """
148
+ cmd = ["git", "branch", "--show-current"]
149
+ result = _run_local_git(cmd, repo=repo, expect_stderr=True)
150
+ if not result:
151
+ return None
152
+
153
+ branch = result.stdout.strip()
154
+ return branch if branch else None
155
+
156
+
138
157
  @cachetools.func.ttl_cache(maxsize=16, ttl=60) # 60 seconds
139
158
  def get_tags(repo: str, minver: Optional[str] = None) -> List[str]:
140
159
  """
@@ -242,7 +261,7 @@ def fetch(repo: Union[Path, str]) -> bool:
242
261
  return result.returncode == 0 if result else False
243
262
 
244
263
 
245
- def pull(repo: Union[Path, str], branch: str = "main") -> bool:
264
+ def pull(repo: Union[Path, str], branch: str = "main", force: bool = True) -> bool:
246
265
  """
247
266
  pull a repo origin into main
248
267
  repo should be in the form of : path/.git
@@ -253,7 +272,9 @@ def pull(repo: Union[Path, str], branch: str = "main") -> bool:
253
272
  raise NotADirectoryError
254
273
  repo = Path(repo)
255
274
  # first checkout HEAD
256
- cmd = ["git", "checkout", branch, "--quiet", "--force"]
275
+ cmd = ["git", "checkout", branch, "--quiet"]
276
+ if force:
277
+ cmd.append("--force")
257
278
  result = _run_local_git(cmd, repo=repo, expect_stderr=True)
258
279
  if not result:
259
280
  log.error("error during git checkout main", result)
mpflash/cli_add.py ADDED
@@ -0,0 +1,131 @@
1
+ """CLI to add a custom MicroPython firmware."""
2
+
3
+ from pathlib import Path
4
+ from typing import Union
5
+
6
+ import rich_click as click
7
+ from loguru import logger as log
8
+
9
+ from mpflash.connected import connected_ports_boards_variants
10
+ from mpflash.custom import Firmware, add_firmware, custom_fw_from_path
11
+ from mpflash.downloaded import clean_downloaded_firmwares
12
+ from mpflash.errors import MPFlashError
13
+ from mpflash.mpboard_id import find_known_board
14
+ from mpflash.mpboard_id.alternate import add_renamed_boards
15
+ from mpflash.versions import clean_version
16
+
17
+ from .ask_input import ask_missing_params
18
+ from .cli_group import cli
19
+ from .config import config
20
+ from .download import download
21
+
22
+
23
+ @cli.command(
24
+ "add",
25
+ help="Add a custom MicroPython firmware.",
26
+ )
27
+ # @click.option(
28
+ # "--version",
29
+ # "-v",
30
+ # "versions",
31
+ # default=["stable"],
32
+ # multiple=False,
33
+ # show_default=True,
34
+ # help="The version of MicroPython to to download.",
35
+ # metavar="SEMVER, 'stable', 'preview' or '?'",
36
+ # )
37
+ @click.option(
38
+ "--path",
39
+ "-p",
40
+ "fw_path",
41
+ multiple=False,
42
+ default="",
43
+ show_default=False,
44
+ help="a local path to the firmware file to add.",
45
+ metavar="FIRMWARE_PATH",
46
+ )
47
+ @click.option(
48
+ "--description",
49
+ "-d",
50
+ "description",
51
+ default="",
52
+ help="An Optional description for the firmware.",
53
+ metavar="TXT",
54
+ )
55
+ # @click.option(
56
+ # "--board",
57
+ # "-b",
58
+ # "boards",
59
+ # multiple=True,
60
+ # default=[],
61
+ # show_default=True,
62
+ # help="The board(s) to download the firmware for.",
63
+ # metavar="BOARD_ID or ?",
64
+ # )
65
+ # @click.option(
66
+ # "--serial",
67
+ # "--serial-port",
68
+ # "-s",
69
+ # "serial",
70
+ # default=["*"],
71
+ # show_default=True,
72
+ # multiple=True,
73
+ # help="Which serial port(s) (or globs) to flash",
74
+ # metavar="SERIALPORT",
75
+ # )
76
+ # @click.option(
77
+ # "--ignore",
78
+ # "-i",
79
+ # is_eager=True,
80
+ # help="Serial port(s) to ignore. Defaults to MPFLASH_IGNORE.",
81
+ # multiple=True,
82
+ # default=[],
83
+ # envvar="MPFLASH_IGNORE",
84
+ # show_default=True,
85
+ # metavar="SERIALPORT",
86
+ # )
87
+ # @click.option(
88
+ # "--clean/--no-clean",
89
+ # default=True,
90
+ # show_default=True,
91
+ # help="""Remove dates and hashes from the downloaded firmware filenames.""",
92
+ # )
93
+ @click.option(
94
+ "--force",
95
+ "-f",
96
+ default=False,
97
+ is_flag=True,
98
+ show_default=True,
99
+ help="""Overwrite existing firmware.""",
100
+ )
101
+ def cli_add_custom(
102
+ fw_path: Union[Path, str],
103
+ force: bool = False,
104
+ description: str = "",
105
+ ) -> int:
106
+ """Add a custom MicroPython firmware from a local file."""
107
+ if not fw_path:
108
+ log.error("No firmware path provided. Use --path to specify a firmware file.")
109
+ return 1
110
+ fw_path = Path(fw_path).expanduser().resolve()
111
+ if not fw_path.exists():
112
+ log.error(f"Firmware file does not exist: {fw_path}")
113
+ return 1
114
+
115
+ try:
116
+ fw_dict = custom_fw_from_path(fw_path)
117
+ if description:
118
+ fw_dict["description"] = description
119
+ if add_firmware(
120
+ source=fw_path,
121
+ fw_info=fw_dict,
122
+ custom=True,
123
+ force=force,
124
+ ):
125
+ log.success(f"Added custom firmware: {fw_dict['custom_id']} for {fw_dict['firmware_file']}")
126
+ return 0
127
+ else:
128
+ return 1
129
+ except MPFlashError as e:
130
+ log.error(f"{e}")
131
+ return 1
mpflash/cli_download.py CHANGED
@@ -2,10 +2,11 @@
2
2
 
3
3
  from pathlib import Path
4
4
 
5
+ from pytest import param
5
6
  import rich_click as click
6
7
  from loguru import logger as log
7
8
 
8
- from mpflash.connected import connected_ports_boards
9
+ from mpflash.connected import connected_ports_boards_variants
9
10
  from mpflash.downloaded import clean_downloaded_firmwares
10
11
  from mpflash.errors import MPFlashError
11
12
  from mpflash.mpboard_id import find_known_board
@@ -97,7 +98,7 @@ def cli_download(**kwargs) -> int:
97
98
  log.error(f"{e}")
98
99
  else:
99
100
  # no boards specified - detect connected ports and boards
100
- params.ports, params.boards, _ = connected_ports_boards(include=params.serial, ignore=params.ignore)
101
+ params.ports, params.boards, _, _ = connected_ports_boards_variants(include=params.serial, ignore=params.ignore)
101
102
 
102
103
  params = ask_missing_params(params)
103
104
  if not params: # Cancelled by user
mpflash/cli_flash.py CHANGED
@@ -6,7 +6,7 @@ from loguru import logger as log
6
6
  import mpflash.download.jid as jid
7
7
  import mpflash.mpboard_id as mpboard_id
8
8
  from mpflash.ask_input import ask_missing_params
9
- from mpflash.cli_download import connected_ports_boards
9
+ from mpflash.cli_download import connected_ports_boards_variants
10
10
  from mpflash.cli_group import cli
11
11
  from mpflash.cli_list import show_mcus
12
12
  from mpflash.common import BootloaderMethod, FlashParams, filtered_comports
@@ -93,7 +93,6 @@ from mpflash.versions import clean_version
93
93
  @click.option(
94
94
  "--cpu",
95
95
  "--chip",
96
- "-c",
97
96
  "cpu",
98
97
  help="The CPU type to flash. If not specified will try to read the CPU from the connected MCU.",
99
98
  metavar="CPU",
@@ -129,6 +128,14 @@ from mpflash.versions import clean_version
129
128
  show_default=True,
130
129
  help="""Flash mode for ESP boards. (default: keep)""",
131
130
  )
131
+ @click.option(
132
+ "--custom",
133
+ "-c",
134
+ default=False,
135
+ is_flag=True,
136
+ show_default=True,
137
+ help="""Flash a custom firmware""",
138
+ )
132
139
  def cli_flash_board(**kwargs) -> int:
133
140
  # version to versions, board to boards
134
141
  kwargs["versions"] = [kwargs.pop("version")] if kwargs["version"] is not None else []
@@ -158,9 +165,13 @@ def cli_flash_board(**kwargs) -> int:
158
165
  all_boards: List[MPRemoteBoard] = []
159
166
  if not params.boards:
160
167
  # nothing specified - detect connected boards
161
- params.ports, params.boards, all_boards = connected_ports_boards(
162
- include=params.ports, ignore=params.ignore, bluetooth=params.bluetooth
168
+ params.ports, params.boards, variants, all_boards = connected_ports_boards_variants(
169
+ include=params.ports,
170
+ ignore=params.ignore,
171
+ bluetooth=params.bluetooth,
163
172
  )
173
+ if variants and len(variants) >= 1:
174
+ params.variant = variants[0]
164
175
  if params.boards == []:
165
176
  # No MicroPython boards detected, but it could be unflashed or in bootloader mode
166
177
  # Ask for serial port and board_id to flash
@@ -188,7 +199,7 @@ def cli_flash_board(**kwargs) -> int:
188
199
  # A one or more serial port including the board / variant
189
200
  comports = filtered_comports(
190
201
  ignore=params.ignore,
191
- include=params.ports,
202
+ include=params.serial,
192
203
  bluetooth=params.bluetooth,
193
204
  )
194
205
  board_id = f"{params.boards[0]}-{params.variant}" if params.variant else params.boards[0]
@@ -198,12 +209,13 @@ def cli_flash_board(**kwargs) -> int:
198
209
  comports,
199
210
  board_id=board_id,
200
211
  version=params.versions[0],
212
+ custom=params.custom,
201
213
  )
202
214
  # if serial port == auto and there are one or more specified/detected boards
203
215
  elif params.serial == ["*"] and params.boards:
204
216
  if not all_boards:
205
217
  log.trace("No boards detected yet, scanning for connected boards")
206
- _, _, all_boards = connected_ports_boards(include=params.ports, ignore=params.ignore)
218
+ _, _, _, all_boards = connected_ports_boards_variants(include=params.ports, ignore=params.ignore)
207
219
  # if variant id provided on the cmdline, treat is as an override
208
220
  if params.variant:
209
221
  for b in all_boards:
@@ -233,7 +245,8 @@ def cli_flash_board(**kwargs) -> int:
233
245
  serial=params.serial[0],
234
246
  version=params.versions[0],
235
247
  )
236
- jid.ensure_firmware_downloaded(worklist, version=params.versions[0], force=params.force)
248
+ if not params.custom:
249
+ jid.ensure_firmware_downloaded(worklist, version=params.versions[0], force=params.force)
237
250
  if flashed := flash_list(
238
251
  worklist,
239
252
  params.erase,
@@ -246,5 +259,3 @@ def cli_flash_board(**kwargs) -> int:
246
259
  else:
247
260
  log.error("No boards were flashed")
248
261
  return 1
249
-
250
-
mpflash/cli_group.py CHANGED
@@ -14,6 +14,7 @@ from .logger import log, make_quiet, set_loglevel
14
14
  set_loglevel("INFO")
15
15
  config.verbose = False
16
16
 
17
+
17
18
  def cb_verbose(ctx, param, value):
18
19
  """Callback to set the log level to DEBUG if verbose is set"""
19
20
  if value and not config.quiet:
mpflash/cli_main.py CHANGED
@@ -5,6 +5,9 @@ import os
5
5
  import click.exceptions as click_exceptions
6
6
  from loguru import logger as log
7
7
 
8
+ from mpflash.errors import MPFlashError
9
+
10
+ from .cli_add import cli_add_custom
8
11
  from .cli_download import cli_download
9
12
  from .cli_flash import cli_flash_board
10
13
  from .cli_group import cli
@@ -19,6 +22,7 @@ def mpflash():
19
22
  cli.add_command(cli_list_mcus)
20
23
  cli.add_command(cli_download)
21
24
  cli.add_command(cli_flash_board)
25
+ cli.add_command(cli_add_custom)
22
26
 
23
27
  # cli(auto_envvar_prefix="MPFLASH")
24
28
  if False and os.environ.get("COMPUTERNAME").upper().startswith("JOSVERL"):
@@ -37,6 +41,9 @@ def mpflash():
37
41
  except click_exceptions.Abort:
38
42
  # Aborted - Ctrl-C
39
43
  exit(-3)
44
+ except MPFlashError as e:
45
+ log.error(f"MPFlashError: {e}")
46
+ exit(-4)
40
47
 
41
48
 
42
49
  if __name__ == "__main__":
mpflash/common.py CHANGED
@@ -4,7 +4,6 @@ import os
4
4
  import platform
5
5
  from dataclasses import dataclass, field
6
6
  from enum import Enum
7
- from pathlib import Path
8
7
  from typing import List, Optional, Union
9
8
 
10
9
  from serial.tools import list_ports
@@ -65,6 +64,7 @@ class FlashParams(Params):
65
64
  bootloader: BootloaderMethod = BootloaderMethod.NONE
66
65
  cpu: str = ""
67
66
  flash_mode: str = "keep" # keep, qio, qout, dio, dout
67
+ custom: bool = False
68
68
 
69
69
  def __post_init__(self):
70
70
  if isinstance(self.bootloader, str):
@@ -85,6 +85,7 @@ def filtered_comports(
85
85
  """
86
86
  return [p.device for p in filtered_portinfos(ignore, include, bluetooth)]
87
87
 
88
+
88
89
  def filtered_portinfos(
89
90
  ignore: Optional[List[str]] = None,
90
91
  include: Optional[List[str]] = None,
@@ -94,6 +95,7 @@ def filtered_portinfos(
94
95
  Get a list of filtered comports using the include and ignore lists.
95
96
  both can be globs (e.g. COM*) or exact port names (e.g. COM1)
96
97
  """
98
+ log.trace(f"filtered_portinfos: {ignore=}, {include=}, {bluetooth=}")
97
99
  if not ignore:
98
100
  ignore = []
99
101
  elif not isinstance(ignore, list): # type: ignore
mpflash/config.py CHANGED
@@ -4,6 +4,7 @@ import os
4
4
  from importlib.metadata import version
5
5
  from pathlib import Path
6
6
  from typing import List, Optional
7
+
7
8
  import platformdirs
8
9
 
9
10
  from mpflash.errors import MPFlashError
@@ -46,14 +47,17 @@ class MPFlashConfig:
46
47
  """The folder where firmware files are stored"""
47
48
  if not self._firmware_folder:
48
49
  from mpflash.logger import log
50
+
49
51
  # Check if MPFLASH_FIRMWARE environment variable is set
50
52
  env_firmware_path = os.getenv("MPFLASH_FIRMWARE")
51
53
  if env_firmware_path:
52
- firmware_path = Path(env_firmware_path)
54
+ firmware_path = Path(env_firmware_path).expanduser().resolve()
53
55
  if firmware_path.exists() and firmware_path.is_dir():
54
56
  self._firmware_folder = firmware_path
55
57
  else:
56
- log.warning(f"Environment variable MPFLASH_FIRMWARE points to invalid directory: {env_firmware_path}. Using default location.")
58
+ log.warning(
59
+ f"Environment variable MPFLASH_FIRMWARE points to invalid directory: {env_firmware_path}. Using default location."
60
+ )
57
61
  # allow testing in CI
58
62
  if Path(os.getenv("GITHUB_ACTIONS", "")).as_posix().lower() == "true":
59
63
  workspace = os.getenv("GITHUB_WORKSPACE")
@@ -83,6 +87,7 @@ class MPFlashConfig:
83
87
  def db_path(self) -> Path:
84
88
  """The path to the database file"""
85
89
  return self.firmware_folder / "mpflash.db"
90
+
86
91
  @property
87
92
  def db_version(self) -> str:
88
93
  return "1.24.1"
mpflash/connected.py CHANGED
@@ -8,9 +8,9 @@ from mpflash.common import filtered_portinfos, find_serial_by_path
8
8
  from mpflash.mpremoteboard import MPRemoteBoard
9
9
 
10
10
 
11
- def connected_ports_boards(
11
+ def connected_ports_boards_variants(
12
12
  *, include: List[str], ignore: List[str], bluetooth: bool = False
13
- ) -> Tuple[List[str], List[str], List[MPRemoteBoard]]:
13
+ ) -> Tuple[List[str], List[str], List[str], List[MPRemoteBoard]]:
14
14
  """
15
15
  Returns a tuple containing lists of unique ports and boards from the connected MCUs.
16
16
  Boards that are physically connected, but give no tangible response are ignored.
@@ -21,14 +21,15 @@ def connected_ports_boards(
21
21
  - A list of unique board names of the connected MCUs.
22
22
  - A list of MPRemoteBoard instances of the connected MCUs.
23
23
  """
24
- conn_mcus = [b for b in list_mcus(include=include, ignore=ignore, bluetooth=bluetooth)]
24
+ # conn_mcus = [b for b in list_mcus(include=include, ignore=ignore, bluetooth=bluetooth)]
25
25
  conn_mcus = [b for b in list_mcus(include=include, ignore=ignore, bluetooth=bluetooth) if b.connected]
26
26
  # ignore boards that have the [mpflash] ignore flag set
27
27
  conn_mcus = [item for item in conn_mcus if not (item.toml.get("mpflash", {}).get("ignore", False))]
28
28
 
29
29
  ports = list({b.port for b in conn_mcus})
30
30
  boards = list({b.board for b in conn_mcus})
31
- return (ports, boards, conn_mcus)
31
+ variants = list({b.variant for b in conn_mcus if b.variant})
32
+ return (ports, boards, variants, conn_mcus)
32
33
 
33
34
 
34
35
  # #########################################################################################################
@@ -47,7 +48,7 @@ def list_mcus(*, ignore: List[str], include: List[str], bluetooth: bool = False)
47
48
  ConnectionError: If there is an error connecting to a board.
48
49
  """
49
50
  # conn_mcus = [MPRemoteBoard(sp) for sp in MPRemoteBoard.connected_boards(bluetooth) if sp not in config.ignore_ports]
50
-
51
+ vid_pid = True
51
52
  comports = filtered_portinfos(
52
53
  ignore=ignore,
53
54
  include=include,
@@ -0,0 +1,144 @@
1
+ import shutil
2
+ import sqlite3
3
+ from pathlib import Path
4
+ from typing import Union
5
+
6
+ import jsonlines
7
+ import requests
8
+ from loguru import logger as log
9
+ # re-use logic from mpremote
10
+ from mpremote.mip import _rewrite_url as rewrite_url # type: ignore
11
+
12
+ from mpflash.config import config
13
+ from mpflash.db.core import Session
14
+ from mpflash.db.models import Firmware
15
+ from mpflash.errors import MPFlashError
16
+ from mpflash.versions import get_preview_mp_version, get_stable_mp_version
17
+
18
+ from .naming import (custom_fw_from_path, extract_commit_count,
19
+ port_and_boardid_from_path)
20
+
21
+ #
22
+ # github.com/<owner>/<repo>@<branch>#<commit>
23
+ # $remote_url = git remote get-url origin
24
+ # $branch = git rev-parse --abbrev-ref HEAD
25
+ # $commit = git rev-parse --short HEAD
26
+ # if ($remote_url -match "github.com[:/](.+)/(.+?)(\.git)?$") {
27
+ # $owner = $matches[1]
28
+ # $repo = $matches[2]
29
+ # "github.com/$owner/$repo@$branch#$commit"
30
+ # }
31
+
32
+
33
+ # 1) local path
34
+
35
+
36
+ def add_firmware(
37
+ source: Path,
38
+ fw_info: dict,
39
+ *,
40
+ force: bool = False,
41
+ custom: bool = False,
42
+ ) -> bool:
43
+ """
44
+ Add a firmware to the database , and firmware folder.
45
+ stored in the port folder, with the filename.
46
+
47
+ fw_info is a dict with the following keys:
48
+ - board_id: str, required
49
+ - version: str, required
50
+ - port: str, required
51
+ - firmware_file: str, required, the filename to store in the firmware folder
52
+ - source: str, optional, the source of the firmware, can be a local path
53
+ - description: str, optional, a description of the firmware
54
+ - custom: bool, optional, if the firmware is a custom firmware, default False
55
+ """
56
+ try:
57
+ source = source.expanduser().absolute()
58
+ if not source.exists() or not source.is_file():
59
+ log.error(f"Source file {source} does not exist or is not a file")
60
+ return False
61
+ with Session() as session:
62
+ # Check minimal info needed
63
+ new_fw = Firmware(**fw_info)
64
+ if custom:
65
+ new_fw.custom = True
66
+
67
+ if not new_fw.board_id:
68
+ log.error("board_id is required")
69
+ return False
70
+
71
+ # assume the the firmware_file has already been prepared
72
+ fw_filename = config.firmware_folder / new_fw.firmware_file
73
+
74
+ if not copy_firmware(source, fw_filename, force):
75
+ log.error(f"Failed to copy {source} to {fw_filename}")
76
+ return False
77
+ # add to inventory
78
+ # check if the firmware already exists
79
+ if custom:
80
+ qry = session.query(Firmware).filter(Firmware.custom_id == new_fw.custom_id)
81
+ else:
82
+ qry = session.query(Firmware).filter(Firmware.board_id == new_fw.board_id)
83
+
84
+ qry = qry.filter(
85
+ Firmware.board_id == new_fw.board_id,
86
+ Firmware.version == new_fw.version,
87
+ Firmware.port == new_fw.port,
88
+ Firmware.custom == new_fw.custom,
89
+ )
90
+ existing_fw = qry.first()
91
+
92
+ if existing_fw:
93
+ if not force:
94
+ log.warning(f"Firmware {existing_fw} already exists")
95
+ return False
96
+ # update the existing firmware
97
+ existing_fw.firmware_file = new_fw.firmware_file
98
+ existing_fw.source = new_fw.source
99
+ existing_fw.description = new_fw.description
100
+ existing_fw.custom = custom
101
+ if custom:
102
+ existing_fw.custom_id = new_fw.custom_id
103
+ else:
104
+ session.add(new_fw)
105
+ session.commit()
106
+
107
+ return True
108
+ except sqlite3.DatabaseError as e:
109
+ raise MPFlashError(
110
+ f"Failed to add firmware {fw_info['firmware_file']}: {e}"
111
+ ) from e
112
+
113
+
114
+ def copy_firmware(source: Path, fw_filename: Path, force: bool = False):
115
+ """Add a firmware to the firmware folder.
116
+ stored in the port folder, with the same filename as the source.
117
+ """
118
+ if fw_filename.exists() and not force:
119
+ log.error(f" {fw_filename} already exists. Use --force to overwrite")
120
+ return False
121
+ fw_filename.parent.mkdir(parents=True, exist_ok=True)
122
+ if isinstance(source, Path):
123
+ if not source.exists():
124
+ log.error(f"File {source} does not exist")
125
+ return False
126
+ # file copy
127
+ log.debug(f"Copy {source} to {fw_filename}")
128
+ shutil.copy(source, fw_filename)
129
+ return True
130
+ # TODO: handle github urls
131
+ # url = rewrite_url(source)
132
+ # if str(source).startswith("http://") or str(source).startswith("https://"):
133
+ # log.debug(f"Download {url} to {fw_filename}")
134
+ # response = requests.get(url)
135
+
136
+ # if response.status_code == 200:
137
+ # with open(fw_filename, "wb") as file:
138
+ # file.write(response.content)
139
+ # log.info("File downloaded and saved successfully.")
140
+ # return True
141
+ # else:
142
+ # print("Failed to download the file.")
143
+ # return False
144
+ # return False
@@ -0,0 +1,91 @@
1
+ import re
2
+ from pathlib import Path
3
+ from typing import Dict, List, Optional, Tuple, Union
4
+
5
+ import mpflash.basicgit as git
6
+ from mpflash.logger import log
7
+
8
+
9
+ def custom_fw_from_path(fw_path: Path) -> Dict[str, Union[str, int, bool]]:
10
+ """Generate a custom name for the firmware file based on its path.
11
+
12
+ Args:
13
+ firmware_path: Path to firmware file
14
+
15
+ Returns:
16
+ Custom name for the firmware file
17
+ """
18
+ repo_path = fw_path.expanduser().absolute().parent
19
+ port, board_id = port_and_boardid_from_path(fw_path)
20
+ if not port or not board_id:
21
+ raise ValueError(f"Could not extract port and board_id from path: {fw_path}")
22
+ if "wsl.localhost" in str(repo_path):
23
+ log.info("Accessing WSL path; please note that it may take a few seconds to get git info across filesystems")
24
+ version = git.get_local_tag(repo_path) or "unknown"
25
+ describe = git.get_git_describe(repo_path)
26
+ if describe:
27
+ build = extract_commit_count(describe)
28
+ else:
29
+ build = 0
30
+ branch = git.get_current_branch(repo_path)
31
+ if branch:
32
+ branch = branch.split("/")[-1] # Use only last part of the branch name (?)
33
+ build_str = f".{build}" if build > 0 else ""
34
+ branch_str = f"@{branch}" if branch else ""
35
+ new_fw_path = Path(port) / f"{board_id}{branch_str}-{version}{build_str}{fw_path.suffix}"
36
+
37
+ return {
38
+ "port": port,
39
+ "board_id": f"{board_id}",
40
+ "custom_id": f"{board_id}{branch_str}",
41
+ "version": version,
42
+ "build": build,
43
+ "custom": True,
44
+ "firmware_file": new_fw_path.as_posix(),
45
+ "source": fw_path.expanduser().absolute().as_uri() if isinstance(fw_path, Path) else fw_path, # Use URI for local files
46
+ }
47
+
48
+
49
+ def port_and_boardid_from_path(firmware_path: Path) -> Tuple[Optional[str], Optional[str]]:
50
+ """Extract port and board_id from firmware path.
51
+
52
+ Args:
53
+ firmware_path: Path to firmware file
54
+
55
+ Returns:
56
+ Tuple of (port, board_id) or (None, None) if not found
57
+ """
58
+ path_str = str(firmware_path).replace("\\", "/") # Normalize path for regex matching
59
+
60
+ # Pattern: /path/to/micropython/ports/{port}/build-{board_id}/firmware.ext
61
+ build_match = re.search(r"/ports/([^/]+)/build-([^/]+)/", path_str)
62
+ if build_match:
63
+ port = build_match.group(1)
64
+ board_id = build_match.group(2)
65
+ # Remove variant suffix (e.g., GENERIC_S3-SPIRAM_OCT -> GENERIC_S3)
66
+ board_id = board_id.split("-")[0]
67
+ return port, board_id
68
+
69
+ # Pattern: /path/to/micropython/ports/{port}/firmware.ext
70
+ port_match = re.search(r"/ports/([^/]+)/[^/]*firmware\.[^/]*$", path_str)
71
+ if port_match:
72
+ port = port_match.group(1)
73
+ return port, None
74
+
75
+ return None, None
76
+
77
+
78
+ def extract_commit_count(git_describe: str) -> int:
79
+ """Extract commit count from git describe string.
80
+
81
+ Args:
82
+ git_describe: Git describe output like 'v1.26.0-preview-214-ga56a1eec7b-dirty'
83
+
84
+ Returns:
85
+ Commit count as integer or None if not found
86
+ """
87
+ # Match patterns like v1.26.0-preview-214-g... or v1.26.0-214-g...
88
+ match = re.search(r"-(\d+)-g[a-f0-9]+", git_describe)
89
+ if match:
90
+ return int(match.group(1))
91
+ return 0