From 1d768b6a6704b2af6c1e62e99af7b72d3590e5fa Mon Sep 17 00:00:00 2001 From: Bastien Sagetat Date: Mon, 15 Jun 2026 13:32:24 +0200 Subject: [PATCH] snagflash: fastboot: add support of "paths-relative-to" env variable Path resolution can be controlled through the "paths-relative-to" environement variable, which can be configured using the existing "set" command. Supported values: - CWD (default): resolve paths relative to the current working directory. - THIS_FILE: resolve paths relative to the command file's directory. Any other value raises an error. The "paths-relative-to" environment variable is documented in the help text, as well as in docs/snagflash.md Add unittests for testing this feature (tests/fastboot_uboot.py), most notably for testing the SnagflashFastbootUboot.resolve_path() method. Note: The snagrecover config file already implements this feature, with the same variable name. Signed-off-by: Christophe Guerreiro Signed-off-by: Bastien Sagetat --- docs/snagflash.md | 6 ++++- src/snagflash/fastboot.py | 2 +- src/snagflash/fastboot_uboot.py | 31 ++++++++++++++++++++-- tests/fastboot_uboot.py | 46 ++++++++++++++++++++++++++++++++- 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/docs/snagflash.md b/docs/snagflash.md index 6ef9fc9..c306fcb 100644 --- a/docs/snagflash.md +++ b/docs/snagflash.md @@ -108,6 +108,7 @@ flash [] Optional environment variables: - fb-size + - paths-relative-to If a file named ".bmap" exists, snagflash will automatically parse it and flash only the block ranges described. @@ -127,8 +128,11 @@ fb-addr: address in memory of the Fastboot buffer eraseblk-size: size in bytes of an erase block on the target Flash device fb-size: size in bytes of the Fastboot buffer, this can only be used to reduce - the U-Boot Fastboot buffer size, not increase it. + the U-Boot Fastboot buffer size, not increase it. +paths-relative-to: controls how relative image paths in flash commands are resolved + CWD (default) paths are relative to the current working directory + THIS_FILE paths are relative to the directory containing the cmdfile ``` ## UMS mode diff --git a/src/snagflash/fastboot.py b/src/snagflash/fastboot.py index 50e6def..312fca7 100644 --- a/src/snagflash/fastboot.py +++ b/src/snagflash/fastboot.py @@ -145,7 +145,7 @@ def fastboot(args): with open(args.interactive_cmdfile, "r") as file: cmds = file.read(-1).splitlines() - session.run(cmds) + session.run(cmds, args.interactive_cmdfile) if args.interactive: if args.protocol == "fastboot": diff --git a/src/snagflash/fastboot_uboot.py b/src/snagflash/fastboot_uboot.py index 328a2f4..b05b974 100644 --- a/src/snagflash/fastboot_uboot.py +++ b/src/snagflash/fastboot_uboot.py @@ -48,6 +48,7 @@ class SnagflashFastbootUboot: Optional environment variables: - fb-size + - paths-relative-to If a file named ".bmap" exists, snagflash will automatically parse it and flash only the block ranges described. @@ -67,7 +68,11 @@ class SnagflashFastbootUboot: eraseblk-size: size in bytes of an erase block on the target Flash device fb-size: size in bytes of the Fastboot buffer, this can only be used to reduce - the U-Boot Fastboot buffer size, not increase it. + the U-Boot Fastboot buffer size, not increase it. + +paths-relative-to: controls how relative image paths in flash commands are resolved + CWD (default) paths are relative to the current working directory + THIS_FILE paths are relative to the directory containing the cmdfile """ op_pattern = r"[\w\-]+" @@ -77,6 +82,7 @@ def __init__(self, fast): self.fast = fast self.env = {} self.checked = False + self.cmdfile = None def err(self, msg: str): print(f"CLI Error: {msg}") @@ -206,6 +212,24 @@ def preflash_checks(self): self.checked = True + def resolve_path(self, path: str) -> str: + if os.path.isabs(path): + return path + paths_relative_to = self.env.get("paths-relative-to", "CWD") + if paths_relative_to == "CWD": + pass # path is relative to CWD by default + elif paths_relative_to == "THIS_FILE": + if self.cmdfile is None: + raise SnagflashCmdError( + "paths-relative-to is THIS_FILE but no cmdfile is set" + ) + path = os.path.join(os.path.dirname(self.cmdfile), path) + else: + raise SnagflashCmdError( + f"unsupported paths-relative-to value: '{paths_relative_to}'" + ) + return path + def cmd_flash(self, args: str): self.preflash_checks() @@ -215,6 +239,8 @@ def cmd_flash(self, args: str): path = path.strip('"').strip('"') rest = rest.strip(" ") + path = self.resolve_path(path) + if " " in rest: offset, sep, part = rest.partition(" ") part = part.strip(" ") @@ -420,7 +446,8 @@ def flash_mmc( MMC_LBA_SIZE, ) - def run(self, cmds: list): + def run(self, cmds: list, cmdfile: str = None): + self.cmdfile = os.path.abspath(cmdfile) if cmdfile is not None else None for cmd in cmds: cmd = cmd.strip() diff --git a/tests/fastboot_uboot.py b/tests/fastboot_uboot.py index 159bc11..7675e94 100644 --- a/tests/fastboot_uboot.py +++ b/tests/fastboot_uboot.py @@ -1,9 +1,14 @@ +import os import unittest import unittest.mock import tempfile import random -from snagflash.fastboot_uboot import SnagflashFastbootUboot, MMC_LBA_SIZE +from snagflash.fastboot_uboot import ( + SnagflashFastbootUboot, + SnagflashCmdError, + MMC_LBA_SIZE, +) MAX_IMAGE_LEN = 100000000 MAX_FLASH_OFFSET = 4096 @@ -79,3 +84,42 @@ def test_flash_range(self): ) offset += rd_size + + +class TestResolvePath(unittest.TestCase): + def setUp(self): + self.fb = SnagflashFastbootUboot(unittest.mock.MagicMock()) + + def test_default_is_cwd(self): + # paths-relative-to unset -> path returned unchanged (relative to CWD) + self.assertEqual(self.fb.resolve_path("img/boot.bin"), "img/boot.bin") + + def test_explicit_cwd(self): + self.fb.env["paths-relative-to"] = "CWD" + self.assertEqual(self.fb.resolve_path("img/boot.bin"), "img/boot.bin") + + def test_absolute_path_is_untouched(self): + self.fb.env["paths-relative-to"] = "THIS_FILE" + self.fb.cmdfile = "/some/dir/flash.cmds" + self.assertEqual(self.fb.resolve_path("/abs/boot.bin"), "/abs/boot.bin") + + def test_this_file_joins_cmdfile_dir(self): + self.fb.env["paths-relative-to"] = "THIS_FILE" + self.fb.cmdfile = "/some/dir/flash.cmds" + self.assertEqual( + self.fb.resolve_path("img/boot.bin"), + os.path.join("/some/dir", "img/boot.bin"), + ) + + def test_this_file_without_cmdfile_raises(self): + self.fb.env["paths-relative-to"] = "THIS_FILE" + self.fb.cmdfile = None + with self.assertRaises(SnagflashCmdError): + self.fb.resolve_path("img/boot.bin") + + def test_unsupported_value_raises(self): + self.fb.env["paths-relative-to"] = ( + "this_file" # case-sensitive: lowercase invalid + ) + with self.assertRaises(SnagflashCmdError): + self.fb.resolve_path("img/boot.bin")