Просмотр исходного кода

fbt: fixes (#1352)

* fbt: added --git-tasks; fixed typos
* fbt: fixed --extra-int-apps handling; scripts: moved storage.py & selfupdate.py to App() framework
* fbt: changed pseudo-builders to PhonyTargets with commands; added link to latest build dir as build/latest
* fbt: Restored old ep git handling
* fbt: dropped git tasks & dirlink.py
* fbt: removed extra quoting in fbt.cmd
* docs: added flash_usb to ReadMe.md

Co-authored-by: あく <alleteam@gmail.com>
hedger 3 лет назад
Родитель
Сommit
b3767d143f

+ 7 - 1
ReadMe.md

@@ -27,9 +27,15 @@ They both must be flashed in order described.
 
 ## With offline update package
 
+With Flipper attached over USB:
+
+`./fbt --with-updater flash_usb`
+
+Just building the package:
+
 `./fbt --with-updater updater_package`
 
-Copy the resulting directory to Flipper's SD card and navigate to `update.fuf` file in Archive app. 
+To update, copy the resulting directory to Flipper's SD card and navigate to `update.fuf` file in Archive app. 
 
 ## With STLink
 

+ 31 - 57
SConstruct

@@ -17,7 +17,6 @@ fbt_variables = SConscript("site_scons/commandline.scons")
 cmd_environment = Environment(tools=[], variables=fbt_variables)
 Help(fbt_variables.GenerateHelpText(cmd_environment))
 
-
 # Building basic environment - tools, utility methods, cross-compilation
 # settings, gcc flags for Cortex-M4, basic builders and more
 coreenv = SConscript(
@@ -43,7 +42,6 @@ firmware_out = distenv.AddFwProject(
     fw_env_key="FW_ENV",
 )
 
-
 # If enabled, initialize updater-related targets
 if GetOption("fullenv"):
     updater_out = distenv.AddFwProject(
@@ -71,91 +69,67 @@ if GetOption("fullenv"):
             "--splash",
             distenv.subst("assets/slideshow/$UPDATE_SPLASH"),
         ]
-    selfupdate_dist = distenv.DistBuilder(
-        "selfupdate.pseudo",
+
+    selfupdate_dist = distenv.DistCommand(
+        "updater_package",
         (distenv["DIST_DEPENDS"], firmware_out["FW_RESOURCES"]),
         DIST_EXTRA=dist_arguments,
     )
-    distenv.Pseudo("selfupdate.pseudo")
-    AlwaysBuild(selfupdate_dist)
-    Alias("updater_package", selfupdate_dist)
 
     # Updater debug
-    debug_updater_elf = distenv.AddDebugTarget(updater_out, False)
-    Alias("updater_debug", debug_updater_elf)
+    distenv.AddDebugTarget("updater_debug", updater_out, False)
 
     # Installation over USB & CLI
     usb_update_package = distenv.UsbInstall(
-        "usbinstall.flag",
-        (distenv["DIST_DEPENDS"], firmware_out["FW_RESOURCES"], selfupdate_dist),
+        "#build/usbinstall.flag",
+        (
+            distenv["DIST_DEPENDS"],
+            firmware_out["FW_RESOURCES"],
+            selfupdate_dist,
+        ),
     )
     if distenv["FORCE"]:
-        AlwaysBuild(usb_update_package)
-    Depends(usb_update_package, selfupdate_dist)
-    Alias("flash_usb", usb_update_package)
-
+        distenv.AlwaysBuild(usb_update_package)
+    distenv.Depends(usb_update_package, selfupdate_dist)
+    distenv.Alias("flash_usb", usb_update_package)
 
 # Target for copying & renaming binaries to dist folder
-basic_dist = distenv.DistBuilder("dist.pseudo", distenv["DIST_DEPENDS"])
-distenv.Pseudo("dist.pseudo")
-AlwaysBuild(basic_dist)
-Alias("fw_dist", basic_dist)
-Default(basic_dist)
-
+basic_dist = distenv.DistCommand("fw_dist", distenv["DIST_DEPENDS"])
+distenv.Default(basic_dist)
 
 # Target for bundling core2 package for qFlipper
 copro_dist = distenv.CoproBuilder(
-    Dir("assets/core2_firmware"),
+    distenv.Dir("assets/core2_firmware"),
     [],
 )
-AlwaysBuild(copro_dist)
-Alias("copro_dist", copro_dist)
-
+distenv.Alias("copro_dist", copro_dist)
 
 # Debugging firmware
-
-debug_fw_elf = distenv.AddDebugTarget(firmware_out)
-Alias("debug", debug_fw_elf)
-
-
+distenv.AddDebugTarget("debug", firmware_out)
 # Debug alien elf
-debug_other = distenv.GDBPy(
-    "debugother.pseudo",
-    None,
+distenv.PhonyTarget(
+    "debug_other",
+    "$GDBPYCOM",
     GDBPYOPTS=
     # '-ex "source ${ROOT_DIR.abspath}/debug/FreeRTOS/FreeRTOS.py" '
     '-ex "source debug/PyCortexMDebug/PyCortexMDebug.py" ',
 )
-distenv.Pseudo("debugother.pseudo")
-AlwaysBuild(debug_other)
-Alias("debug_other", debug_other)
-
 
 # Just start OpenOCD
-openocd = distenv.OOCDCommand("openocd.pseudo", [])
-distenv.Pseudo("openocd.pseudo")
-AlwaysBuild(openocd)
-Alias("openocd", openocd)
-
+distenv.PhonyTarget(
+    "openocd",
+    "${OPENOCDCOM}",
+)
 
 # Linter
-lint_check = distenv.Command(
-    "lint.check.pseudo",
-    [],
-    "${PYTHON3} scripts/lint.py check $LINT_SOURCES",
+distenv.PhonyTarget(
+    "lint",
+    "${PYTHON3} scripts/lint.py check ${LINT_SOURCES}",
     LINT_SOURCES=firmware_out["LINT_SOURCES"],
 )
-distenv.Pseudo("lint.check.pseudo")
-AlwaysBuild(lint_check)
-Alias("lint", lint_check)
-
 
-lint_format = distenv.Command(
-    "lint.format.pseudo",
-    [],
-    "${PYTHON3} scripts/lint.py format $LINT_SOURCES",
+distenv.PhonyTarget(
+    "format",
+    "${PYTHON3} scripts/lint.py format ${LINT_SOURCES}",
     LINT_SOURCES=firmware_out["LINT_SOURCES"],
 )
-distenv.Pseudo("lint.format.pseudo")
-AlwaysBuild(lint_format)
-Alias("format", lint_format)

+ 24 - 24
assets/SConscript

@@ -28,38 +28,38 @@ icons_src = assetsenv.GlobRecursive("*.png", "icons")
 icons_src += assetsenv.GlobRecursive("frame_rate", "icons")
 
 icons = assetsenv.IconBuilder(Dir("compiled"), Dir("#/assets/icons"))
-Depends(icons, icons_src)
-Alias("icons", icons)
+assetsenv.Depends(icons, icons_src)
+assetsenv.Alias("icons", icons)
 
 
 # Protobuf .proto -> .c + .h
 
-proto_src = Glob("protobuf/*.proto", source=True)
-proto_options = Glob("protobuf/*.options", source=True)
-proto = assetsenv.ProtoBuilder(Dir("compiled"), proto_src)
-Depends(proto, proto_options)
+proto_src = assetsenv.Glob("protobuf/*.proto", source=True)
+proto_options = assetsenv.Glob("protobuf/*.options", source=True)
+proto = assetsenv.ProtoBuilder(assetsenv.Dir("compiled"), proto_src)
+assetsenv.Depends(proto, proto_options)
 # Precious(proto)
-Alias("proto", proto)
+assetsenv.Alias("proto", proto)
 
 
 # Internal animations
 
 dolphin_internal = assetsenv.DolphinSymBuilder(
-    Dir("compiled"),
-    Dir("#/assets/dolphin"),
+    assetsenv.Dir("compiled"),
+    assetsenv.Dir("#/assets/dolphin"),
     DOLPHIN_RES_TYPE="internal",
 )
-Alias("dolphin_internal", dolphin_internal)
+assetsenv.Alias("dolphin_internal", dolphin_internal)
 
 
 # Blocking animations
 
 dolphin_blocking = assetsenv.DolphinSymBuilder(
-    Dir("compiled"),
-    Dir("#/assets/dolphin"),
+    assetsenv.Dir("compiled"),
+    assetsenv.Dir("#/assets/dolphin"),
     DOLPHIN_RES_TYPE="blocking",
 )
-Alias("dolphin_blocking", dolphin_blocking)
+assetsenv.Alias("dolphin_blocking", dolphin_blocking)
 
 
 # Protobuf version meta
@@ -67,8 +67,8 @@ proto_ver = assetsenv.ProtoVerBuilder(
     "compiled/protobuf_version.h",
     "#/assets/protobuf/Changelog",
 )
-Depends(proto_ver, proto)
-Alias("proto_ver", proto_ver)
+assetsenv.Depends(proto_ver, proto)
+assetsenv.Alias("proto_ver", proto_ver)
 
 # Gather everything into a static lib
 assets_parts = (icons, proto, dolphin_blocking, dolphin_internal, proto_ver)
@@ -82,14 +82,14 @@ assetsenv.Install("${LIB_DIST_DIR}", assetslib)
 if assetsenv["IS_BASE_FIRMWARE"]:
     # External dolphin animations
     dolphin_external = assetsenv.DolphinExtBuilder(
-        Dir("#/assets/resources/dolphin"),
-        Dir("#/assets/dolphin"),
+        assetsenv.Dir("#/assets/resources/dolphin"),
+        assetsenv.Dir("#/assets/dolphin"),
         DOLPHIN_RES_TYPE="external",
     )
-    NoClean(dolphin_external)
+    assetsenv.NoClean(dolphin_external)
     if assetsenv["FORCE"]:
-        AlwaysBuild(dolphin_external)
-    Alias("dolphin_ext", dolphin_external)
+        assetsenv.AlwaysBuild(dolphin_external)
+    assetsenv.Alias("dolphin_ext", dolphin_external)
 
     # Resources manifest
 
@@ -101,13 +101,13 @@ if assetsenv["IS_BASE_FIRMWARE"]:
             "${RESMANIFESTCOMSTR}",
         ),
     )
-    Precious(resources)
-    NoClean(resources)
+    assetsenv.Precious(resources)
+    assetsenv.NoClean(resources)
     if assetsenv["FORCE"]:
-        AlwaysBuild(resources)
+        assetsenv.AlwaysBuild(resources)
 
     # Exporting resources node to external environment
     env["FW_RESOURCES"] = resources
-    Alias("resources", resources)
+    assetsenv.Alias("resources", resources)
 
 Return("assetslib")

+ 6 - 5
documentation/fbt.md

@@ -1,11 +1,12 @@
 # Flipper Build Tool
 
-FBT is the entry point for most firmware-related commands and utilities.
+FBT is the entry point for firmware-related commands and utilities.
 It is invoked by `./fbt` in firmware project root directory. Internally, it is a wrapper around [scons](https://scons.org/) build system.
 
 ## Requirements
 
 Please install Python packages required by assets build scripts: `pip3 install -r scripts/requirements.txt`
+Make sure that `gcc-arm-none-eabi` toolchain & OpenOCD executables are in system's PATH.
 
 ## NB
 
@@ -17,22 +18,22 @@ To build with FBT, call it specifying configuration options & targets to build.
 
 `./fbt --with-updater COMPACT=1 DEBUG=0 VERBOSE=1 updater_package copro_dist`
 
-To run cleanup (think of `make clean`) for specified targets, all `-c` option.
+To run cleanup (think of `make clean`) for specified targets, add `-c` option.
 
 ## FBT targets
 
-FBT keeps track of internal dependencies, so you only need to build the highest-level target you need, and FBT will make sure everything it needs is up-to-date.
+FBT keeps track of internal dependencies, so you only need to build the highest-level target you need, and FBT will make sure everything they depend on is up-to-date.
 
 ### High-level (what you most likely need)
 
-- `fw_dist` - build & publish firmware to `dist` folder
+- `fw_dist` - build & publish firmware to `dist` folder. This is a default target, when no other are specified
 - `updater_package` - build self-update package. _Requires `--with-updater` option_
 - `copro_dist` - bundle Core2 FUS+stack binaries for qFlipper
 - `flash` - flash attached device with OpenOCD over ST-Link
 - `flash_usb` - build, upload and install update package to device over USB.  _Requires `--with-updater` option_
 - `debug` - build and flash firmware, then attach with gdb with firmware's .elf loaded
 - `debug_updater` - attach gdb with updater's .elf loaded. _Requires `--with-updater` option_
-- `debug_other` - attach gdb without loading built elf. Allows to manually add external elf files with `add-symbol-file` in gdb.
+- `debug_other` - attach gdb without loading any .elf. Allows to manually add external elf files with `add-symbol-file` in gdb.
 - `openocd` - just start OpenOCD
 
 ### Firmware targets

+ 10 - 4
fbt

@@ -2,11 +2,17 @@
 
 set -e
 
+SCRIPTDIR="$( dirname -- "$0"; )";
+SCONS_EP=${SCRIPTDIR}/lib/scons/scripts/scons.py
+
 if [[ -d .git ]]; then
-	echo "Updating git submodules"
-	git submodule update --init
+    echo Updating git submodules
+    git submodule update --init
+else # Not in a git repo
+    echo Not in a git repo, please clone with git clone --recursive
+    # Return error code 1 to indicate failure
+    exit 1
 fi
 
-SCRIPTDIR="$( dirname -- "$0"; )";
 SCONS_DEFAULT_FLAGS="-Q --warn=target-not-built"
-python3 ${SCRIPTDIR}/lib/scons/scripts/scons.py ${SCONS_DEFAULT_FLAGS} "$@"
+python3 ${SCONS_EP} ${SCONS_DEFAULT_FLAGS} "$@"

+ 6 - 3
fbt.cmd

@@ -1,8 +1,11 @@
 @echo off
+
+set SCONS_EP=%~dp0\lib\scons\scripts\scons.py
+
 if exist ".git" (
-	echo Prepairing git submodules
-	git submodule update --init
+	echo Updating git submodules
+	git submodule update --init	
 )
 
 set "SCONS_DEFAULT_FLAGS=-Q --warn=target-not-built"
-python lib/scons/scripts/scons.py %SCONS_DEFAULT_FLAGS% %*
+python %SCONS_EP% %SCONS_DEFAULT_FLAGS% %*

+ 4 - 4
fbt_options.py

@@ -46,7 +46,7 @@ OPENOCD_OPTS = '-f interface/stlink.cfg -c "transport select hla_swd" -f debug/s
 SVD_FILE = "debug/STM32WB55_CM4.svd"
 
 FIRMWARE_APPS = {
-    "default": (
+    "default": [
         "crypto_start",
         # Svc
         "basic_services",
@@ -62,11 +62,11 @@ FIRMWARE_APPS = {
         "basic_plugins",
         # Debug
         "debug_apps",
-    ),
-    "unit_tests": (
+    ],
+    "unit_tests": [
         "basic_services",
         "unit_tests",
-    ),
+    ],
 }
 
 FIRMWARE_APP_SET = "default"

+ 52 - 10
firmware.scons

@@ -2,6 +2,7 @@ Import("ENV", "fw_build_meta")
 
 import os
 
+from fbt.util import link_dir
 
 # Building initial C environment for libs
 env = ENV.Clone(
@@ -72,7 +73,7 @@ if not env["VERBOSE"]:
         HEXCOMSTR="\tHEX\t${TARGET}",
         BINCOMSTR="\tBIN\t${TARGET}",
         DFUCOMSTR="\tDFU\t${TARGET}",
-        OOCDCOMSTR="\tFLASH\t${SOURCE}",
+        OPENOCDCOMSTR="\tFLASH\t${SOURCE}",
     )
 
 
@@ -116,8 +117,7 @@ else:
     fwenv.Append(APPS=["updater"])
 
 if extra_int_apps := GetOption("extra_int_apps"):
-    for extra_int_app in extra_int_apps.split(","):
-        fwenv.Append(APPS=[extra_int_app])
+    fwenv.Append(APPS=extra_int_apps.split(","))
 
 fwenv.LoadApplicationManifests()
 fwenv.PrepareApplicationsBuild()
@@ -198,41 +198,83 @@ fwelf = fwenv["FW_ELF"] = fwenv.Program(
     ],
 )
 
+
+def link_elf_dir_as_latest(env, elf_target):
+    # Ugly way to check if updater-related targets were requested
+    elf_dir = elf_target.Dir(".")
+    explicitly_building_updater = False
+    # print("BUILD_TARGETS:", ','.join(BUILD_TARGETS))
+    for build_target in BUILD_TARGETS:
+        # print(">>> ", str(build_target))
+        if "updater" in str(build_target):
+            explicitly_building_updater = True
+
+    latest_dir = env.Dir("#build/latest")
+
+    link_this_dir = True
+    if explicitly_building_updater:
+        # If updater is explicitly requested, link to the latest updater
+        # Otherwise, link to the latest firmware
+        link_this_dir = not env["IS_BASE_FIRMWARE"]
+
+    if link_this_dir:
+        print(f"Setting {elf_dir} as latest built dir")
+        return link_dir(latest_dir.abspath, elf_dir.abspath, env["PLATFORM"] == "win32")
+
+
+def link_latest_dir(env, target, source):
+    return link_elf_dir_as_latest(env, target[0])
+
+
 # Make it depend on everything child builders returned
 Depends(fwelf, lib_targets)
 AddPostAction(fwelf, fwenv["APPBUILD_DUMP"])
 AddPostAction(fwelf, Action("@$SIZECOM"))
+AddPostAction(fwelf, Action(link_latest_dir, None))
+
+link_dir_command = fwenv["LINK_DIR_CMD"] = fwenv.PhonyTarget(
+    "${FIRMWARE_BUILD_CFG}" + "_latest",
+    Action(lambda target, source, env: link_elf_dir_as_latest(env, source[0]), None),
+    source=fwelf,
+)
 
 
 fwhex = fwenv["FW_HEX"] = fwenv.HEXBuilder("${FIRMWARE_BUILD_CFG}")
 fwbin = fwenv["FW_BIN"] = fwenv.BINBuilder("${FIRMWARE_BUILD_CFG}")
 fwdfu = fwenv["FW_DFU"] = fwenv.DFUBuilder("${FIRMWARE_BUILD_CFG}")
-# Default(dfu)
 Alias(fwenv["FIRMWARE_BUILD_CFG"] + "_dfu", fwdfu)
 
 fwdump = fwenv.ObjDump("${FIRMWARE_BUILD_CFG}")
 Alias(fwenv["FIRMWARE_BUILD_CFG"] + "_list", fwdump)
 
 # Additional FW-related pseudotargets
-flash = fwenv["FW_FLASH"] = fwenv.OOCDFlashCommand(
+flash = fwenv["FW_FLASH"] = fwenv.OpenOCDFlash(
+    "#build/oocd-${FIRMWARE_BUILD_CFG}-flash.flag",
     "${FIRMWARE_BUILD_CFG}",
     OPENOCD_COMMAND='-c "program ${SOURCE.posix} reset exit ${IMAGE_BASE_ADDRESS}"',
 )
 if fwenv["FORCE"]:
-    AlwaysBuild(flash)
-Alias(fwenv["FIRMWARE_BUILD_CFG"] + "_flash", flash)
+    fwenv.AlwaysBuild(flash)
+fwenv.Alias(fwenv["FIRMWARE_BUILD_CFG"] + "_flash", flash)
 if fwenv["IS_BASE_FIRMWARE"]:
-    Alias("flash", flash)
+    fwenv.Alias("flash", flash)
 
 
 # Compile DB generation
 fwcdb = fwenv["FW_CDB"] = fwenv.CompilationDatabase("compile_commands.json")
-Alias(fwenv["FIRMWARE_BUILD_CFG"] + "_cdb", fwcdb)
+fwenv.Alias(fwenv["FIRMWARE_BUILD_CFG"] + "_cdb", fwcdb)
 
 
-artifacts = [fwhex, fwbin, fwdfu, env["FW_VERSION_JSON"]]
+artifacts = [
+    fwhex,
+    fwbin,
+    fwdfu,
+    env["FW_VERSION_JSON"],
+    fwcdb,
+]
 fwenv["FW_ARTIFACTS"] = artifacts
 
 Alias(fwenv["FIRMWARE_BUILD_CFG"] + "_all", artifacts)
 
+
 Return("fwenv")

+ 0 - 1
scripts/flipper/app.py

@@ -1,7 +1,6 @@
 import logging
 import argparse
 import sys
-import os
 
 
 class App:

+ 17 - 0
scripts/flipper/utils/cdc.py

@@ -0,0 +1,17 @@
+import serial.tools.list_ports as list_ports
+
+# Returns a valid port or None, if it cannot be found
+def resolve_port(logger, portname: str = "auto"):
+    if portname != "auto":
+        return portname
+    # Try guessing
+    flippers = list(list_ports.grep("flip"))
+    if len(flippers) == 1:
+        flipper = flippers[0]
+        logger.info(f"Using {flipper.serial_number} on {flipper.device}")
+        return flipper.device
+    elif len(flippers) == 0:
+        logger.error("Failed to find connected Flipper")
+    elif len(flippers) > 1:
+        logger.error("More than one Flipper is attached")
+    logger.error("Failed to guess which port to use. Specify --port")

+ 45 - 79
scripts/selfupdate.py

@@ -1,20 +1,18 @@
 #!/usr/bin/env python3
 
+from typing import final
+from flipper.app import App
 from flipper.storage import FlipperStorage
+from flipper.utils.cdc import resolve_port
 
 import logging
-import argparse
 import os
-import sys
 import pathlib
 import serial.tools.list_ports as list_ports
 
 
-class Main:
-    def __init__(self):
-        # command args
-        self.parser = argparse.ArgumentParser()
-        self.parser.add_argument("-d", "--debug", action="store_true", help="Debug")
+class Main(App):
+    def init(self):
         self.parser.add_argument("-p", "--port", help="CDC Port", default="auto")
         self.parser.add_argument(
             "-b",
@@ -25,35 +23,15 @@ class Main:
             type=int,
         )
 
-        self.subparsers = self.parser.add_subparsers(help="sub-command help")
-
-        self.parser_install = self.subparsers.add_parser(
-            "install", help="Install OTA package"
-        )
-        self.parser_install.add_argument("manifest_path", help="Manifest path")
-        self.parser_install.add_argument(
+        self.parser.add_argument("manifest_path", help="Manifest path")
+        self.parser.add_argument(
             "--pkg_dir_name", help="Update dir name", default="pcbundle", required=False
         )
-        self.parser_install.set_defaults(func=self.install)
+        self.parser.set_defaults(func=self.install)
 
         # logging
         self.logger = logging.getLogger()
 
-    def __call__(self):
-        self.args = self.parser.parse_args()
-        if "func" not in self.args:
-            self.parser.error("Choose something to do")
-        # configure log output
-        self.log_level = logging.DEBUG if self.args.debug else logging.INFO
-        self.logger.setLevel(self.log_level)
-        self.handler = logging.StreamHandler(sys.stdout)
-        self.handler.setLevel(self.log_level)
-        self.formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
-        self.handler.setFormatter(self.formatter)
-        self.logger.addHandler(self.handler)
-        # execute requested function
-        self.args.func()
-
     # make directory with exist check
     def mkdir_on_storage(self, storage, flipper_dir_path):
         if not storage.exist_dir(flipper_dir_path):
@@ -82,61 +60,49 @@ class Main:
                 return False
         return True
 
-    def _get_port(self):
-        if self.args.port != "auto":
-            return self.args.port
-        # Try guessing
-        flippers = list(list_ports.grep("flip"))
-        if len(flippers) == 1:
-            flipper = flippers[0]
-            self.logger.info(f"Using {flipper.serial_number} on {flipper.device}")
-            return flipper.device
-        elif len(flippers) == 0:
-            self.logger.error("Failed to find connected Flipper")
-        elif len(flippers) > 1:
-            self.logger.error("More than one Flipper is attached")
-        self.logger.error("Failed to guess which port to use. Specify --port")
-
     def install(self):
-        if not (port := self._get_port()):
+        if not (port := resolve_port(self.logger, self.args.port)):
             return 1
 
         storage = FlipperStorage(port, self.args.baud)
         storage.start()
 
-        if not os.path.isfile(self.args.manifest_path):
-            self.logger.error("Error: manifest not found")
-            return 2
-
-        manifest_path = pathlib.Path(os.path.abspath(self.args.manifest_path))
-        manifest_name, pkg_name = manifest_path.parts[-1], manifest_path.parts[-2]
-
-        pkg_dir_name = self.args.pkg_dir_name or pkg_name
-        flipper_update_path = f"/ext/update/{pkg_dir_name}"
-
-        self.logger.info(f'Installing "{pkg_name}" from {flipper_update_path}')
-        # if not os.path.exists(self.args.manifest_path):
-        # self.logger.error("Error: package not found")
-        if not self.mkdir_on_storage(storage, flipper_update_path):
-            self.logger.error(f"Error: cannot create {storage.last_error}")
-            return -2
-
-        for dirpath, dirnames, filenames in os.walk(manifest_path.parents[0]):
-            for fname in filenames:
-                self.logger.debug(f"Uploading {fname}")
-                local_file_path = os.path.join(dirpath, fname)
-                flipper_file_path = f"{flipper_update_path}/{fname}"
-                if not self.send_file_to_storage(
-                    storage, flipper_file_path, local_file_path, False
-                ):
-                    self.logger.error(f"Error: {storage.last_error}")
-                    return -3
-
-            storage.send_and_wait_eol(
-                f"update install {flipper_update_path}/{manifest_name}\r"
-            )
-            break
-        storage.stop()
+        try:
+            if not os.path.isfile(self.args.manifest_path):
+                self.logger.error("Error: manifest not found")
+                return 2
+
+            manifest_path = pathlib.Path(os.path.abspath(self.args.manifest_path))
+            manifest_name, pkg_name = manifest_path.parts[-1], manifest_path.parts[-2]
+
+            pkg_dir_name = self.args.pkg_dir_name or pkg_name
+            flipper_update_path = f"/ext/update/{pkg_dir_name}"
+
+            self.logger.info(f'Installing "{pkg_name}" from {flipper_update_path}')
+            # if not os.path.exists(self.args.manifest_path):
+            # self.logger.error("Error: package not found")
+            if not self.mkdir_on_storage(storage, flipper_update_path):
+                self.logger.error(f"Error: cannot create {storage.last_error}")
+                return -2
+
+            for dirpath, dirnames, filenames in os.walk(manifest_path.parents[0]):
+                for fname in filenames:
+                    self.logger.debug(f"Uploading {fname}")
+                    local_file_path = os.path.join(dirpath, fname)
+                    flipper_file_path = f"{flipper_update_path}/{fname}"
+                    if not self.send_file_to_storage(
+                        storage, flipper_file_path, local_file_path, False
+                    ):
+                        self.logger.error(f"Error: {storage.last_error}")
+                        return -3
+
+                storage.send_and_wait_eol(
+                    f"update install {flipper_update_path}/{manifest_name}\r"
+                )
+                break
+            return 0
+        finally:
+            storage.stop()
 
 
 if __name__ == "__main__":

+ 47 - 44
scripts/storage.py

@@ -1,23 +1,19 @@
 #!/usr/bin/env python3
 
+from flipper.app import App
 from flipper.storage import FlipperStorage
+from flipper.utils.cdc import resolve_port
 
 import logging
-import argparse
 import os
-import sys
 import binascii
-import posixpath
 import filecmp
 import tempfile
 
 
-class Main:
-    def __init__(self):
-        # command args
-        self.parser = argparse.ArgumentParser()
-        self.parser.add_argument("-d", "--debug", action="store_true", help="Debug")
-        self.parser.add_argument("-p", "--port", help="CDC Port", required=True)
+class Main(App):
+    def init(self):
+        self.parser.add_argument("-p", "--port", help="CDC Port", default="auto")
         self.parser.add_argument(
             "-b",
             "--baud",
@@ -77,43 +73,37 @@ class Main:
         )
         self.parser_stress.set_defaults(func=self.stress)
 
-        # logging
-        self.logger = logging.getLogger()
-
-    def __call__(self):
-        self.args = self.parser.parse_args()
-        if "func" not in self.args:
-            self.parser.error("Choose something to do")
-        # configure log output
-        self.log_level = logging.DEBUG if self.args.debug else logging.INFO
-        self.logger.setLevel(self.log_level)
-        self.handler = logging.StreamHandler(sys.stdout)
-        self.handler.setLevel(self.log_level)
-        self.formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
-        self.handler.setFormatter(self.formatter)
-        self.logger.addHandler(self.handler)
-        # execute requested function
-        self.args.func()
+    def _get_storage(self):
+        if not (port := resolve_port(self.logger, self.args.port)):
+            return None
 
-    def mkdir(self):
-        storage = FlipperStorage(self.args.port)
+        storage = FlipperStorage(port, self.args.baud)
         storage.start()
+        return storage
+
+    def mkdir(self):
+        if not (storage := self._get_storage()):
+            return 1
+
         self.logger.debug(f'Creating "{self.args.flipper_path}"')
         if not storage.mkdir(self.args.flipper_path):
             self.logger.error(f"Error: {storage.last_error}")
         storage.stop()
+        return 0
 
     def remove(self):
-        storage = FlipperStorage(self.args.port)
-        storage.start()
+        if not (storage := self._get_storage()):
+            return 1
+
         self.logger.debug(f'Removing "{self.args.flipper_path}"')
         if not storage.remove(self.args.flipper_path):
             self.logger.error(f"Error: {storage.last_error}")
         storage.stop()
+        return 0
 
     def receive(self):
-        storage = FlipperStorage(self.args.port)
-        storage.start()
+        if not (storage := self._get_storage()):
+            return 1
 
         if storage.exist_dir(self.args.flipper_path):
             for dirpath, dirnames, filenames in storage.walk(self.args.flipper_path):
@@ -155,14 +145,17 @@ class Main:
             if not storage.receive_file(self.args.flipper_path, self.args.local_path):
                 self.logger.error(f"Error: {storage.last_error}")
         storage.stop()
+        return 0
 
     def send(self):
-        storage = FlipperStorage(self.args.port)
-        storage.start()
+        if not (storage := self._get_storage()):
+            return 1
+
         self.send_to_storage(
             storage, self.args.flipper_path, self.args.local_path, self.args.force
         )
         storage.stop()
+        return 0
 
     # send file or folder recursively
     def send_to_storage(self, storage, flipper_path, local_path, force):
@@ -250,8 +243,9 @@ class Main:
                     self.logger.error(f"Error: {storage.last_error}")
 
     def read(self):
-        storage = FlipperStorage(self.args.port, self.args.baud)
-        storage.start()
+        if not (storage := self._get_storage()):
+            return 1
+
         self.logger.debug(f'Reading "{self.args.flipper_path}"')
         data = storage.read_file(self.args.flipper_path)
         if not data:
@@ -264,10 +258,12 @@ class Main:
                 print("Binary hexadecimal data:")
                 print(binascii.hexlify(data).decode())
         storage.stop()
+        return 0
 
     def size(self):
-        storage = FlipperStorage(self.args.port)
-        storage.start()
+        if not (storage := self._get_storage()):
+            return 1
+
         self.logger.debug(f'Getting size of "{self.args.flipper_path}"')
         size = storage.size(self.args.flipper_path)
         if size < 0:
@@ -275,13 +271,16 @@ class Main:
         else:
             print(size)
         storage.stop()
+        return 0
 
     def list(self):
-        storage = FlipperStorage(self.args.port)
-        storage.start()
+        if not (storage := self._get_storage()):
+            return 1
+
         self.logger.debug(f'Listing "{self.args.flipper_path}"')
         storage.list_tree(self.args.flipper_path)
         storage.stop()
+        return 0
 
     def stress(self):
         self.logger.error("This test is wearing out flash memory.")
@@ -293,18 +292,21 @@ class Main:
             self.logger.error("Stop at this point or device warranty will be void")
             say = input("Anything to say? ").strip().lower()
             if say != "void":
-                return
+                return 2
             say = input("Why, Mr. Anderson? ").strip().lower()
             if say != "because":
-                return
+                return 3
 
         with tempfile.TemporaryDirectory() as tmpdirname:
             send_file_name = os.path.join(tmpdirname, "send")
             receive_file_name = os.path.join(tmpdirname, "receive")
             with open(send_file_name, "w") as fout:
                 fout.write("A" * self.args.file_size)
-            storage = FlipperStorage(self.args.port)
-            storage.start()
+
+            storage = self._get_storage()
+            if not storage:
+                return 1
+
             if storage.exist_file(self.args.flipper_path):
                 self.logger.error("File exists, remove it first")
                 return
@@ -318,6 +320,7 @@ class Main:
                 os.unlink(receive_file_name)
                 self.args.count -= 1
             storage.stop()
+            return 0
 
 
 if __name__ == "__main__":

+ 26 - 1
site_scons/fbt/util.py

@@ -2,7 +2,9 @@ import SCons
 from SCons.Subst import quote_spaces
 
 import re
-
+import os
+import random
+import string
 
 WINPATHSEP_RE = re.compile(r"\\([^\"'\\]|$)")
 
@@ -17,3 +19,26 @@ def tempfile_arg_esc_func(arg):
 
 def wrap_tempfile(env, command):
     env[command] = '${TEMPFILE("' + env[command] + '","$' + command + 'STR")}'
+
+
+def link_dir(target_path, source_path, is_windows):
+    # print(f"link_dir: {target_path} -> {source_path}")
+    if os.path.lexists(target_path) or os.path.exists(target_path):
+        os.unlink(target_path)
+    if is_windows:
+        # Crete junction
+        import _winapi
+
+        if not os.path.isdir(source_path):
+            raise Exception(f"Source directory {source_path} is not a directory")
+
+        if not os.path.exists(target_path):
+            _winapi.CreateJunction(source_path, target_path)
+    else:
+        os.symlink(source_path, target_path)
+
+
+def random_alnum(length):
+    return "".join(
+        random.choice(string.ascii_letters + string.digits) for _ in range(length)
+    )

+ 4 - 1
site_scons/site_tools/fbt_apps.py

@@ -51,7 +51,10 @@ def generate(env):
     env.Append(
         BUILDERS={
             "ApplicationsC": Builder(
-                action=Action(build_apps_c, "${APPSCOMSTR}"),
+                action=Action(
+                    build_apps_c,
+                    "${APPSCOMSTR}",
+                ),
             ),
         }
     )

+ 24 - 13
site_scons/site_tools/fbt_dist.py

@@ -46,17 +46,19 @@ def AddFwProject(env, base_env, fw_type, fw_env_key):
         ],
         DIST_DEPENDS=[
             project_env["FW_ARTIFACTS"],
+            project_env["LINK_DIR_CMD"],
         ],
     )
+
     env.Replace(DIST_DIR=get_variant_dirname(env))
     return project_env
 
 
-def AddDebugTarget(env, targetenv, force_flash=True):
-    pseudo_name = f"debug.{targetenv.subst('$FIRMWARE_BUILD_CFG')}.pseudo"
-    debug_target = env.GDBPy(
-        pseudo_name,
-        targetenv["FW_ELF"],
+def AddDebugTarget(env, alias, targetenv, force_flash=True):
+    debug_target = env.PhonyTarget(
+        alias,
+        "$GDBPYCOM",
+        source=targetenv["FW_ELF"],
         GDBPYOPTS='-ex "source debug/FreeRTOS/FreeRTOS.py" '
         '-ex "source debug/PyCortexMDebug/PyCortexMDebug.py" '
         '-ex "svd_load ${SVD_FILE}" '
@@ -64,28 +66,37 @@ def AddDebugTarget(env, targetenv, force_flash=True):
     )
     if force_flash:
         env.Depends(debug_target, targetenv["FW_FLASH"])
-    env.Pseudo(pseudo_name)
-    env.AlwaysBuild(debug_target)
+
     return debug_target
 
 
+def DistCommand(env, name, source, **kw):
+    target = f"dist_{name}"
+    command = env.Command(
+        target,
+        source,
+        '@${PYTHON3} ${ROOT_DIR.abspath}/scripts/sconsdist.py copy -p ${DIST_PROJECTS} -s "${DIST_SUFFIX}" ${DIST_EXTRA}',
+        **kw,
+    )
+    env.Pseudo(target)
+    env.Alias(name, command)
+    return command
+
+
 def generate(env):
     env.AddMethod(AddFwProject)
     env.AddMethod(AddDebugTarget)
+    env.AddMethod(DistCommand)
     env.SetDefault(
         COPRO_MCU_FAMILY="STM32WB5x",
     )
+
     env.Append(
         BUILDERS={
-            "DistBuilder": Builder(
-                action=Action(
-                    '@${PYTHON3} ${ROOT_DIR.abspath}/scripts/sconsdist.py copy -p ${DIST_PROJECTS} -s "${DIST_SUFFIX}" ${DIST_EXTRA}',
-                ),
-            ),
             "UsbInstall": Builder(
                 action=[
                     Action(
-                        "${PYTHON3} ${ROOT_DIR.abspath}/scripts/selfupdate.py install dist/${DIST_DIR}/f${TARGET_HW}-update-${DIST_SUFFIX}/update.fuf"
+                        "${PYTHON3} ${ROOT_DIR.abspath}/scripts/selfupdate.py dist/${DIST_DIR}/f${TARGET_HW}-update-${DIST_SUFFIX}/update.fuf"
                     ),
                     Touch("${TARGET}"),
                 ]

+ 0 - 16
site_scons/site_tools/gdb.py

@@ -11,22 +11,6 @@ def generate(env):
         GDBCOM="$GDB $GDBOPTS $SOURCES",  # no $TARGET
         GDBPYCOM="$GDBPY $GDBOPTS $GDBPYOPTS $SOURCES",  # no $TARGET
     )
-    env.Append(
-        BUILDERS={
-            "GDB": Builder(
-                action=Action(
-                    "${GDBCOM}",
-                    "${GDBCOMSTR}",
-                ),
-            ),
-            "GDBPy": Builder(
-                action=Action(
-                    "${GDBPYCOM}",
-                    "${GDBPYCOMSTR}",
-                ),
-            ),
-        }
-    )
 
 
 def exists(env):

+ 4 - 6
site_scons/site_tools/openocd.py

@@ -7,7 +7,7 @@ __OPENOCD_BIN = "openocd"
 
 _oocd_action = Action(
     "${OPENOCD} ${OPENOCD_OPTS} ${OPENOCD_COMMAND}",
-    "${OOCDCOMSTR}",
+    "${OPENOCDCOMSTR}",
 )
 
 
@@ -16,12 +16,13 @@ def generate(env):
         OPENOCD=__OPENOCD_BIN,
         OPENOCD_OPTS="",
         OPENOCD_COMMAND="",
-        OOCDCOMSTR="",
+        OPENOCDCOM="${OPENOCD} ${OPENOCD_OPTS} ${OPENOCD_COMMAND}",
+        OPENOCDCOMSTR="",
     )
 
     env.Append(
         BUILDERS={
-            "OOCDFlashCommand": Builder(
+            "OpenOCDFlash": Builder(
                 action=[
                     _oocd_action,
                     Touch("${TARGET}"),
@@ -29,9 +30,6 @@ def generate(env):
                 suffix=".flash",
                 src_suffix=".bin",
             ),
-            "OOCDCommand": Builder(
-                action=_oocd_action,
-            ),
         }
     )
 

+ 11 - 0
site_scons/site_tools/sconsmodular.py

@@ -29,9 +29,20 @@ def BuildModules(env, modules):
     return result
 
 
+def PhonyTarget(env, name, action, source=None, **kw):
+    if not source:
+        source = []
+    phony_name = "phony_" + name
+    env.Pseudo(phony_name)
+    return env.AlwaysBuild(
+        env.Alias(name, env.Command(phony_name, source, action, **kw))
+    )
+
+
 def generate(env):
     env.AddMethod(BuildModule)
     env.AddMethod(BuildModules)
+    env.AddMethod(PhonyTarget)
 
 
 def exists(env):