diff --git a/.github/workflow_data/devbuild.py b/.github/workflow_data/devbuild.py index fd409bce2..3a2d8facc 100644 --- a/.github/workflow_data/devbuild.py +++ b/.github/workflow_data/devbuild.py @@ -12,6 +12,12 @@ if __name__ == "__main__": event = json.load(f) client = nextcloud_client.Client(os.environ["NC_HOST"]) + _session = requests.session + def session(*args, **kwargs): + s = _session(*args, **kwargs) + s.headers["User-Agent"] = os.environ["NC_USERAGENT"] + return s + requests.session = session client.login(os.environ["NC_USER"], os.environ["NC_PASS"]) for file in ( diff --git a/.github/workflow_data/webhook.py b/.github/workflow_data/webhook.py index d1f9c08bb..de0617276 100644 --- a/.github/workflow_data/webhook.py +++ b/.github/workflow_data/webhook.py @@ -36,7 +36,7 @@ if __name__ == "__main__": for i, commit in enumerate(event["commits"]): msg = commit['message'].splitlines()[0].replace("`", "") msg = msg[:50] + ("..." if len(msg) > 50 else "") - desc += f"\n[`{commit['id'][:7]}`]({commit['url']}): {msg} - [__{commit['author']['username']}__](https://github.com/{commit['author']['username']})" + desc += f"\n[`{commit['id'][:7]}`]({commit['url']}): {msg} - [__{commit['author'].get('username')}__](https://github.com/{commit['author'].get('username')})" if len(desc) > 2020: desc = desc.rsplit("\n", 1)[0] + f"\n+ {count - i} more commits" break diff --git a/.github/workflow_data/webupdater.py b/.github/workflow_data/webupdater.py index ccf131567..e9a861aaa 100644 --- a/.github/workflow_data/webupdater.py +++ b/.github/workflow_data/webupdater.py @@ -1,9 +1,16 @@ import nextcloud_client +import requests import json import os if __name__ == "__main__": client = nextcloud_client.Client(os.environ["NC_HOST"]) + _session = requests.session + def session(*args, **kwargs): + s = _session(*args, **kwargs) + s.headers["User-Agent"] = os.environ["NC_USERAGENT"] + return s + requests.session = session client.login(os.environ["NC_USER"], os.environ["NC_PASS"]) file = os.environ["ARTIFACT_TGZ"] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f118e279d..a9d90cf6b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,7 @@ concurrency: env: TARGETS: f7 DEFAULT_TARGET: f7 + FBT_GIT_SUBMODULE_SHALLOW: 1 jobs: build: @@ -57,6 +58,7 @@ jobs: if: "github.event_name == 'push' && github.ref_name == 'dev' && !contains(github.event.head_commit.message, '--nobuild')" env: NC_HOST: "https://cloud.cynthialabs.net/" + NC_USERAGENT: "${{ secrets.NC_USERAGENT }}" NC_USER: "${{ secrets.NC_USER }}" NC_PASS: "${{ secrets.NC_PASS }}" BUILD_WEBHOOK: ${{ secrets.BUILD_WEBHOOK }} diff --git a/.github/workflows/hotfix.yml b/.github/workflows/hotfix.yml index 38076ae18..7b7f15132 100644 --- a/.github/workflows/hotfix.yml +++ b/.github/workflows/hotfix.yml @@ -59,6 +59,7 @@ jobs: - name: "Upload to webupdater" env: NC_HOST: "https://cloud.cynthialabs.net/" + NC_USERAGENT: "${{ secrets.NC_USERAGENT }}" NC_USER: "${{ secrets.NC_USER }}" NC_PASS: "${{ secrets.NC_PASS }}" run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1352871e0..00a5c8dab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,6 +55,7 @@ jobs: - name: "Upload to webupdater" env: NC_HOST: "https://cloud.cynthialabs.net/" + NC_USERAGENT: "${{ secrets.NC_USERAGENT }}" NC_USER: "${{ secrets.NC_USER }}" NC_PASS: "${{ secrets.NC_PASS }}" run: | diff --git a/applications/settings/power_settings_app/scenes/power_settings_scene_power_off.c b/applications/settings/power_settings_app/scenes/power_settings_scene_power_off.c index 573c4c4f8..65f10762f 100644 --- a/applications/settings/power_settings_app/scenes/power_settings_scene_power_off.c +++ b/applications/settings/power_settings_app/scenes/power_settings_scene_power_off.c @@ -20,7 +20,7 @@ void power_settings_scene_power_off_on_enter(void* context) { dialog, " I will be\nwaiting for\n you here", 78, 16, AlignLeft, AlignTop); } dialog_ex_set_icon(dialog, 21, 13, &I_Cry_dolph_55x52); - dialog_ex_set_left_button_text(dialog, "Back"); + dialog_ex_set_left_button_text(dialog, "Battery"); dialog_ex_set_right_button_text(dialog, "OFF"); dialog_ex_set_result_callback(dialog, power_settings_scene_power_off_dialog_callback); dialog_ex_set_context(dialog, app); @@ -34,10 +34,7 @@ bool power_settings_scene_power_off_on_event(void* context, SceneManagerEvent ev if(event.type == SceneManagerEventTypeCustom) { if(event.event == DialogExResultLeft) { - if(!scene_manager_previous_scene(app->scene_manager)) { - scene_manager_stop(app->scene_manager); - view_dispatcher_stop(app->view_dispatcher); - } + scene_manager_next_scene(app->scene_manager, PowerSettingsAppSceneBatteryInfo); } else if(event.event == DialogExResultRight) { power_off(app->power); } diff --git a/applications/settings/power_settings_app/views/battery_info.c b/applications/settings/power_settings_app/views/battery_info.c index dd539bd6c..ec94f65ad 100644 --- a/applications/settings/power_settings_app/views/battery_info.c +++ b/applications/settings/power_settings_app/views/battery_info.c @@ -151,7 +151,10 @@ static bool battery_info_input_callback(InputEvent* event, void* context) { BatteryInfo* battery_info = context; - if(event->type == InputTypeShort) { + bool about_battery; + with_view_model( + battery_info->view, BatteryInfoModel * model, { about_battery = model->alt; }, false); + if(about_battery && event->type == InputTypeShort) { if(event->key == InputKeyLeft) { event->key = InputKeyBack; } else if(event->key == InputKeyRight) { diff --git a/applications/system/hid_app/views/hid_mouse.c b/applications/system/hid_app/views/hid_mouse.c index 956ad6b24..5e2dab476 100644 --- a/applications/system/hid_app/views/hid_mouse.c +++ b/applications/system/hid_app/views/hid_mouse.c @@ -22,6 +22,7 @@ typedef struct { bool left_mouse_held; bool right_mouse_pressed; bool connected; + uint8_t acceleration; HidTransport transport; } HidMouseModel; @@ -119,6 +120,11 @@ static void hid_mouse_process(HidMouse* hid_mouse, InputEvent* event) { hid_mouse->view, HidMouseModel * model, { + model->acceleration = (event->type == InputTypePress) ? 1 : + (event->type == InputTypeRelease) ? 0 : + (model->acceleration >= 20) ? 20 : + model->acceleration + 1; + if(event->key == InputKeyBack) { if(event->type == InputTypeShort) { hid_hal_mouse_press(hid_mouse->hid, HID_MOUSE_BTN_RIGHT); @@ -150,7 +156,8 @@ static void hid_mouse_process(HidMouse* hid_mouse, InputEvent* event) { model->right_pressed = true; hid_hal_mouse_move(hid_mouse->hid, MOUSE_MOVE_SHORT, 0); } else if(event->type == InputTypeRepeat) { - hid_hal_mouse_move(hid_mouse->hid, MOUSE_MOVE_LONG, 0); + for(uint8_t i = model->acceleration; i > 1; i -= 2) + hid_hal_mouse_move(hid_mouse->hid, MOUSE_MOVE_LONG, 0); } else if(event->type == InputTypeRelease) { model->right_pressed = false; } @@ -159,7 +166,8 @@ static void hid_mouse_process(HidMouse* hid_mouse, InputEvent* event) { model->left_pressed = true; hid_hal_mouse_move(hid_mouse->hid, -MOUSE_MOVE_SHORT, 0); } else if(event->type == InputTypeRepeat) { - hid_hal_mouse_move(hid_mouse->hid, -MOUSE_MOVE_LONG, 0); + for(uint8_t i = model->acceleration; i > 1; i -= 2) + hid_hal_mouse_move(hid_mouse->hid, -MOUSE_MOVE_LONG, 0); } else if(event->type == InputTypeRelease) { model->left_pressed = false; } @@ -168,7 +176,9 @@ static void hid_mouse_process(HidMouse* hid_mouse, InputEvent* event) { model->down_pressed = true; hid_hal_mouse_move(hid_mouse->hid, 0, MOUSE_MOVE_SHORT); } else if(event->type == InputTypeRepeat) { - hid_hal_mouse_move(hid_mouse->hid, 0, MOUSE_MOVE_LONG); + for(uint8_t i = model->acceleration; i > 1; i -= 2) + hid_hal_mouse_move(hid_mouse->hid, 0, MOUSE_MOVE_LONG); + } else if(event->type == InputTypeRelease) { model->down_pressed = false; } @@ -177,7 +187,8 @@ static void hid_mouse_process(HidMouse* hid_mouse, InputEvent* event) { model->up_pressed = true; hid_hal_mouse_move(hid_mouse->hid, 0, -MOUSE_MOVE_SHORT); } else if(event->type == InputTypeRepeat) { - hid_hal_mouse_move(hid_mouse->hid, 0, -MOUSE_MOVE_LONG); + for(uint8_t i = model->acceleration; i > 1; i -= 2) + hid_hal_mouse_move(hid_mouse->hid, 0, -MOUSE_MOVE_LONG); } else if(event->type == InputTypeRelease) { model->up_pressed = false; } diff --git a/applications/system/subghz_remote/views/remote.c b/applications/system/subghz_remote/views/remote.c index fc7608624..bdf4b89c3 100644 --- a/applications/system/subghz_remote/views/remote.c +++ b/applications/system/subghz_remote/views/remote.c @@ -201,21 +201,30 @@ bool subrem_view_remote_input(InputEvent* event, void* context) { furi_assert(context); SubRemViewRemote* subrem_view_remote = context; - if(event->key == InputKeyBack && event->type == InputTypeLong) { - subrem_view_remote->callback(SubRemCustomEventViewRemoteBack, subrem_view_remote->context); - return true; - } else if(event->key == InputKeyBack && event->type == InputTypeShort) { + if(event->key == InputKeyBack && event->type == InputTypePress) { + bool is_stopping = false; with_view_model( subrem_view_remote->view, SubRemViewRemoteModel * model, - { model->pressed_btn = 0; }, + { + if(model->state == SubRemViewRemoteStateSending) { + is_stopping = true; + model->pressed_btn = 0; + } + }, true); - subrem_view_remote->callback( - SubRemCustomEventViewRemoteForcedStop, subrem_view_remote->context); - return true; - } else if(event->key == InputKeyBack) { + + //Cant send exit the app inside that with_model,locks the model and the app will hang and not unload! + if(is_stopping) + subrem_view_remote->callback( + SubRemCustomEventViewRemoteForcedStop, subrem_view_remote->context); + else + subrem_view_remote->callback( + SubRemCustomEventViewRemoteBack, subrem_view_remote->context); + return true; } + // BACK button processing end if(event->key == InputKeyUp && event->type == InputTypePress) { diff --git a/fbt b/fbt index a4f953c3d..549f7c0ae 100755 --- a/fbt +++ b/fbt @@ -5,7 +5,8 @@ set -eu; # private variables -N_GIT_THREADS="$(getconf _NPROCESSORS_ONLN)"; +N_CORES="$(getconf _NPROCESSORS_ONLN)"; +N_GIT_THREADS="$(($N_CORES * 2))"; SCRIPT_PATH="$(cd "$(dirname "$0")" && pwd -P)"; SCONS_DEFAULT_FLAGS="--warn=target-not-built"; SCONS_EP="python3 -m SCons"; @@ -15,6 +16,7 @@ FBT_NOENV="${FBT_NOENV:-""}"; FBT_NO_SYNC="${FBT_NO_SYNC:-""}"; FBT_TOOLCHAIN_PATH="${FBT_TOOLCHAIN_PATH:-$SCRIPT_PATH}"; FBT_VERBOSE="${FBT_VERBOSE:-""}"; +FBT_GIT_SUBMODULE_SHALLOW="${FBT_GIT_SUBMODULE_SHALLOW:-""}"; if [ -z "$FBT_NOENV" ]; then FBT_VERBOSE="$FBT_VERBOSE" . "$SCRIPT_PATH/scripts/toolchain/fbtenv.sh"; @@ -29,7 +31,12 @@ if [ -z "$FBT_NO_SYNC" ]; then echo "\".git\" directory not found, please clone repo via \"git clone\""; exit 1; fi - git submodule update --init --recursive --jobs "$N_GIT_THREADS"; + _FBT_CLONE_FLAGS="--jobs $N_GIT_THREADS"; + if [ ! -z "$FBT_GIT_SUBMODULE_SHALLOW" ]; then + _FBT_CLONE_FLAGS="$_FBT_CLONE_FLAGS --depth 1"; + fi + + git submodule update --init --recursive --recursive $_FBT_CLONE_FLAGS; fi $SCONS_EP $SCONS_DEFAULT_FLAGS "$@" diff --git a/fbt.cmd b/fbt.cmd index 61f55f5c1..078cc69a0 100644 --- a/fbt.cmd +++ b/fbt.cmd @@ -4,10 +4,18 @@ call "%~dp0scripts\toolchain\fbtenv.cmd" env set SCONS_EP=python -m SCons if [%FBT_NO_SYNC%] == [] ( + set _FBT_CLONE_FLAGS=--jobs %NUMBER_OF_PROCESSORS% + if not [%FBT_GIT_SUBMODULE_SHALLOW%] == [] ( + set _FBT_CLONE_FLAGS=%_FBT_CLONE_FLAGS% --depth 1 + ) if exist ".git" ( - git submodule update --init --recursive --depth 1 --jobs %NUMBER_OF_PROCESSORS% + git submodule update --init --recursive --recursive %_FBT_CLONE_FLAGS% + if %ERRORLEVEL% neq 0 ( + echo Failed to update submodules, set FBT_NO_SYNC to skip + exit /b 1 + ) ) else ( - echo Not in a git repo, please clone with "git clone" + echo .git not found, please clone repo with "git clone" exit /b 1 ) ) diff --git a/lib/flipper_application/elf/elf_file.c b/lib/flipper_application/elf/elf_file.c index 9b8b4c8f5..b2c9445ff 100644 --- a/lib/flipper_application/elf/elf_file.c +++ b/lib/flipper_application/elf/elf_file.c @@ -613,10 +613,31 @@ static Elf32_Addr elf_address_of_by_hash(ELFFile* elf, uint32_t hash) { return ELF_INVALID_ADDRESS; } +static bool elf_file_find_string_by_hash(ELFFile* elf, uint32_t hash, FuriString* out) { + bool result = false; + + FuriString* symbol_name = furi_string_alloc(); + Elf32_Sym sym; + for(size_t i = 0; i < elf->symbol_count; i++) { + furi_string_reset(symbol_name); + if(elf_read_symbol(elf, i, &sym, symbol_name)) { + if(elf_symbolname_hash(furi_string_get_cstr(symbol_name)) == hash) { + furi_string_set(out, symbol_name); + result = true; + break; + } + } + } + furi_string_free(symbol_name); + + return result; +} + static bool elf_relocate_fast(ELFFile* elf, ELFSection* s) { UNUSED(elf); const uint8_t* start = s->fast_rel->data; const uint8_t version = *start; + bool no_errors = true; if(version != FAST_RELOCATION_VERSION) { FURI_LOG_E(TAG, "Unsupported fast relocation version %d", version); @@ -664,16 +685,30 @@ static bool elf_relocate_fast(ELFFile* elf, ELFSection* s) { } if(address == ELF_INVALID_ADDRESS) { - FURI_LOG_E(TAG, "Failed to resolve address for hash %lX", hash_or_section_index); - return false; - } + FuriString* symbol_name = furi_string_alloc(); + if(elf_file_find_string_by_hash(elf, hash_or_section_index, symbol_name)) { + FURI_LOG_E( + TAG, + "Failed to resolve address for symbol %s (hash %lX)", + furi_string_get_cstr(symbol_name), + hash_or_section_index); + } else { + FURI_LOG_E( + TAG, + "Failed to resolve address for hash %lX (string not found)", + hash_or_section_index); + } + furi_string_free(symbol_name); - for(uint32_t j = 0; j < offsets_count; j++) { - uint32_t offset = *((uint32_t*)start) & 0x00FFFFFF; - start += 3; - // FURI_LOG_I(TAG, " Fast relocation offset %ld: %ld", j, offset); - Elf32_Addr relAddr = ((Elf32_Addr)s->data) + offset; - elf_relocate_symbol(elf, relAddr, type, address); + no_errors = false; + start += 3 * offsets_count; + } else { + for(uint32_t j = 0; j < offsets_count; j++) { + uint32_t offset = *((uint32_t*)start) & 0x00FFFFFF; + start += 3; + Elf32_Addr relAddr = ((Elf32_Addr)s->data) + offset; + elf_relocate_symbol(elf, relAddr, type, address); + } } } @@ -681,7 +716,7 @@ static bool elf_relocate_fast(ELFFile* elf, ELFSection* s) { free(s->fast_rel); s->fast_rel = NULL; - return true; + return no_errors; } static bool elf_relocate_section(ELFFile* elf, ELFSection* section) { diff --git a/scripts/fbt/appmanifest.py b/scripts/fbt/appmanifest.py index 24076632e..11828d32e 100644 --- a/scripts/fbt/appmanifest.py +++ b/scripts/fbt/appmanifest.py @@ -100,6 +100,10 @@ class FlipperApplication: def is_default_deployable(self): return self.apptype != FlipperAppType.DEBUG and self.fap_category != "Examples" + @property + def do_strict_import_checks(self): + return self.apptype != FlipperAppType.PLUGIN + def __post_init__(self): if self.apptype == FlipperAppType.PLUGIN: self.stack_size = 0 diff --git a/scripts/fbt_tools/fbt_extapps.py b/scripts/fbt_tools/fbt_extapps.py index bc8f9d5df..963429f24 100644 --- a/scripts/fbt_tools/fbt_extapps.py +++ b/scripts/fbt_tools/fbt_extapps.py @@ -42,6 +42,7 @@ class AppBuilder: self.ext_apps_work_dir = env["EXT_APPS_WORK_DIR"] self.app_work_dir = self.get_app_work_dir(env, app) self.app_alias = f"fap_{self.app.appid}" + self.icons_src = None self.externally_built_files = [] self.private_libs = [] @@ -93,6 +94,7 @@ class AppBuilder: ) self.app_env.Alias("_fap_icons", fap_icons) self.fw_env.Append(_APP_ICONS=[fap_icons]) + self.icons_src = next(filter(lambda n: n.path.endswith(".c"), fap_icons)) def _build_private_libs(self): for lib_def in self.app.fap_private_libs: @@ -160,6 +162,10 @@ class AppBuilder: if not app_sources: raise UserError(f"No source files found for {self.app.appid}") + # Ensure that icons are included in the build, regardless of user-configured sources + if self.icons_src and not self.icons_src in app_sources: + app_sources.append(self.icons_src) + ## Uncomment for debug # print(f"App sources for {self.app.appid}: {list(f.path for f in app_sources)}") @@ -180,7 +186,9 @@ class AppBuilder: self.app._assets_dirs.append(self.app_work_dir.Dir("assets")) app_artifacts.validator = self.app_env.ValidateAppImports( - app_artifacts.compact + app_artifacts.compact, + _CHECK_APP=self.app.do_strict_import_checks + and self.app_env.get("STRICT_FAP_IMPORT_CHECK"), )[0] if self.app.apptype == FlipperAppType.PLUGIN: @@ -300,7 +308,10 @@ def validate_app_imports(target, source, env): + fg.brightmagenta(f"{disabled_api_syms}") + fg.brightyellow(")") ) - SCons.Warnings.warn(SCons.Warnings.LinkWarning, warning_msg), + if env.get("_CHECK_APP"): + raise UserError(warning_msg) + else: + SCons.Warnings.warn(SCons.Warnings.LinkWarning, warning_msg), def GetExtAppByIdOrPath(env, app_dir): diff --git a/scripts/fbt_tools/sconsrecursiveglob.py b/scripts/fbt_tools/sconsrecursiveglob.py index 38aa9a839..e7eb8fb72 100644 --- a/scripts/fbt_tools/sconsrecursiveglob.py +++ b/scripts/fbt_tools/sconsrecursiveglob.py @@ -20,10 +20,9 @@ def GlobRecursive(env, pattern, node=".", exclude=[]): source=True, exclude=exclude, ) - # Otherwise, just check if that's an existing file path - # NB: still creates "virtual" nodes as part of existence check - elif (file_node := node.File(pattern)).exists() or file_node.rexists(): - results.append(file_node) + # Otherwise, just assume that file at path exists + else: + results.append(node.File(pattern)) # print(f"Glob result for {pattern} from {node}: {results}") return results diff --git a/scripts/ufbt/commandline.scons b/scripts/ufbt/commandline.scons index 99c34c35d..a3ba7e08d 100644 --- a/scripts/ufbt/commandline.scons +++ b/scripts/ufbt/commandline.scons @@ -88,6 +88,11 @@ vars.AddVariables( "CDC Port of Flipper to use, if multiple are connected", "auto", ), + BoolVariable( + "STRICT_FAP_IMPORT_CHECK", + help="Enable strict import check for .faps", + default=True, + ), ) Return("vars") diff --git a/site_scons/commandline.scons b/site_scons/commandline.scons index 0b4f03da0..93c43405a 100644 --- a/site_scons/commandline.scons +++ b/site_scons/commandline.scons @@ -270,6 +270,11 @@ vars.AddVariables( "clangd", ], ), + BoolVariable( + "STRICT_FAP_IMPORT_CHECK", + help="Enable strict import check for .faps", + default=True, + ), ) Return("vars")