diff --git a/ReadMe.md b/ReadMe.md index 6a3eed377..e8b4daabc 100644 --- a/ReadMe.md +++ b/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 diff --git a/SConstruct b/SConstruct index 312b23a64..7e8fc8962 100644 --- a/SConstruct +++ b/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) diff --git a/assets/SConscript b/assets/SConscript index e70208ab9..d5465534d 100644 --- a/assets/SConscript +++ b/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") diff --git a/documentation/fbt.md b/documentation/fbt.md index 061339da4..9658ff6a2 100644 --- a/documentation/fbt.md +++ b/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 diff --git a/fbt b/fbt index 81193a468..c95b1371b 100755 --- a/fbt +++ b/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} "$@" diff --git a/fbt.cmd b/fbt.cmd index 7711e44b5..67d42132a 100644 --- a/fbt.cmd +++ b/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% %* diff --git a/fbt_options.py b/fbt_options.py index ddeff0481..3cad7e58c 100644 --- a/fbt_options.py +++ b/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" diff --git a/firmware.scons b/firmware.scons index 890be5f87..4c9988ba8 100644 --- a/firmware.scons +++ b/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") diff --git a/scripts/flipper/app.py b/scripts/flipper/app.py index a75a8a9e8..958356021 100644 --- a/scripts/flipper/app.py +++ b/scripts/flipper/app.py @@ -1,7 +1,6 @@ import logging import argparse import sys -import os class App: diff --git a/scripts/flipper/utils/cdc.py b/scripts/flipper/utils/cdc.py new file mode 100644 index 000000000..081705cc2 --- /dev/null +++ b/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") diff --git a/scripts/selfupdate.py b/scripts/selfupdate.py index 538ecdb98..a22ca1f59 100644 --- a/scripts/selfupdate.py +++ b/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 + 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] + 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}" + 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 + 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 + 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() + storage.send_and_wait_eol( + f"update install {flipper_update_path}/{manifest_name}\r" + ) + break + return 0 + finally: + storage.stop() if __name__ == "__main__": diff --git a/scripts/storage.py b/scripts/storage.py index 1281253bc..0ddc2fc0c 100755 --- a/scripts/storage.py +++ b/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 _get_storage(self): + if not (port := resolve_port(self.logger, self.args.port)): + return None - 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() + storage = FlipperStorage(port, self.args.baud) + storage.start() + return storage def mkdir(self): - storage = FlipperStorage(self.args.port) - storage.start() + 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__": diff --git a/site_scons/fbt/util.py b/site_scons/fbt/util.py index 9de24a9aa..11509b2d1 100644 --- a/site_scons/fbt/util.py +++ b/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) + ) diff --git a/site_scons/site_tools/fbt_apps.py b/site_scons/site_tools/fbt_apps.py index cbeae2d0a..bdccccf7b 100644 --- a/site_scons/site_tools/fbt_apps.py +++ b/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}", + ), ), } ) diff --git a/site_scons/site_tools/fbt_dist.py b/site_scons/site_tools/fbt_dist.py index 8bfb40684..fcfecbcbf 100644 --- a/site_scons/site_tools/fbt_dist.py +++ b/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}"), ] diff --git a/site_scons/site_tools/gdb.py b/site_scons/site_tools/gdb.py index e7b6bdd68..94ea9fbe9 100644 --- a/site_scons/site_tools/gdb.py +++ b/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): diff --git a/site_scons/site_tools/openocd.py b/site_scons/site_tools/openocd.py index 135e1100a..dcf0bf925 100644 --- a/site_scons/site_tools/openocd.py +++ b/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, - ), } ) diff --git a/site_scons/site_tools/sconsmodular.py b/site_scons/site_tools/sconsmodular.py index eeb900229..a4bb9f65a 100644 --- a/site_scons/site_tools/sconsmodular.py +++ b/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):