[FL-2627] Flipper applications: SDK, build and debug system (#1387)
* Added support for running applications from SD card (FAPs - Flipper Application Packages) * Added plugin_dist target for fbt to build FAPs * All apps of type FlipperAppType.EXTERNAL and FlipperAppType.PLUGIN are built as FAPs by default * Updated VSCode configuration for new fbt features - re-deploy stock configuration to use them * Added debugging support for FAPs with fbt debug & VSCode * Added public firmware API with automated versioning Co-authored-by: hedger <hedger@users.noreply.github.com> Co-authored-by: SG <who.just.the.doctor@gmail.com> Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
@@ -63,7 +63,7 @@ class BlackmagicResolver:
|
||||
if probe := self.get_serial() or self.get_networked():
|
||||
return probe
|
||||
|
||||
raise Exception("Please specify BLACKMAGIC=...")
|
||||
raise StopError("Please specify BLACKMAGIC=...")
|
||||
|
||||
|
||||
def generate(env):
|
||||
|
@@ -18,18 +18,26 @@ from fbt.appmanifest import (
|
||||
|
||||
def LoadApplicationManifests(env):
|
||||
appmgr = env["APPMGR"] = AppManager()
|
||||
for entry in env.Glob("#/applications/*", ondisk=True, source=True):
|
||||
if isinstance(entry, SCons.Node.FS.Dir) and not str(entry).startswith("."):
|
||||
try:
|
||||
app_manifest_file_path = os.path.join(entry.abspath, "application.fam")
|
||||
appmgr.load_manifest(app_manifest_file_path, entry.name)
|
||||
env.Append(PY_LINT_SOURCES=[app_manifest_file_path])
|
||||
except FlipperManifestException as e:
|
||||
warn(WarningOnByDefault, str(e))
|
||||
for app_dir, _ in env["APPDIRS"]:
|
||||
app_dir_node = env.Dir("#").Dir(app_dir)
|
||||
|
||||
for entry in app_dir_node.glob("*", ondisk=True, source=True):
|
||||
if isinstance(entry, SCons.Node.FS.Dir) and not str(entry).startswith("."):
|
||||
try:
|
||||
app_manifest_file_path = os.path.join(
|
||||
entry.abspath, "application.fam"
|
||||
)
|
||||
appmgr.load_manifest(app_manifest_file_path, entry)
|
||||
env.Append(PY_LINT_SOURCES=[app_manifest_file_path])
|
||||
except FlipperManifestException as e:
|
||||
warn(WarningOnByDefault, str(e))
|
||||
|
||||
|
||||
def PrepareApplicationsBuild(env):
|
||||
env["APPBUILD"] = env["APPMGR"].filter_apps(env["APPS"])
|
||||
appbuild = env["APPBUILD"] = env["APPMGR"].filter_apps(env["APPS"])
|
||||
env.Append(
|
||||
SDK_HEADERS=appbuild.get_sdk_headers(),
|
||||
)
|
||||
env["APPBUILD_DUMP"] = env.Action(
|
||||
DumpApplicationConfig,
|
||||
"\tINFO\t",
|
||||
|
@@ -13,11 +13,11 @@ def icons_emitter(target, source, env):
|
||||
"compiled/assets_icons.c",
|
||||
"compiled/assets_icons.h",
|
||||
]
|
||||
source = env.GlobRecursive("*.*", env["ICON_SRC_DIR"])
|
||||
return target, source
|
||||
|
||||
|
||||
def proto_emitter(target, source, env):
|
||||
out_path = target[0].path
|
||||
target = []
|
||||
for src in source:
|
||||
basename = os.path.splitext(src.name)[0]
|
||||
@@ -109,7 +109,7 @@ def generate(env):
|
||||
BUILDERS={
|
||||
"IconBuilder": Builder(
|
||||
action=Action(
|
||||
'${PYTHON3} "${ASSETS_COMPILER}" icons ${SOURCE.posix} ${TARGET.dir.posix}',
|
||||
'${PYTHON3} "${ASSETS_COMPILER}" icons ${ICON_SRC_DIR} ${TARGET.dir}',
|
||||
"${ICONSCOMSTR}",
|
||||
),
|
||||
emitter=icons_emitter,
|
||||
|
@@ -4,7 +4,7 @@ from SCons.Script import Mkdir
|
||||
from SCons.Defaults import Touch
|
||||
|
||||
|
||||
def get_variant_dirname(env, project=None):
|
||||
def GetProjetDirName(env, project=None):
|
||||
parts = [f"f{env['TARGET_HW']}"]
|
||||
if project:
|
||||
parts.append(project)
|
||||
@@ -21,7 +21,7 @@ def get_variant_dirname(env, project=None):
|
||||
|
||||
|
||||
def create_fw_build_targets(env, configuration_name):
|
||||
flavor = get_variant_dirname(env, configuration_name)
|
||||
flavor = GetProjetDirName(env, configuration_name)
|
||||
build_dir = env.Dir("build").Dir(flavor).abspath
|
||||
return env.SConscript(
|
||||
"firmware.scons",
|
||||
@@ -49,7 +49,7 @@ def AddFwProject(env, base_env, fw_type, fw_env_key):
|
||||
],
|
||||
)
|
||||
|
||||
env.Replace(DIST_DIR=get_variant_dirname(env))
|
||||
env.Replace(DIST_DIR=env.GetProjetDirName())
|
||||
return project_env
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ def generate(env):
|
||||
env.AddMethod(AddFwProject)
|
||||
env.AddMethod(DistCommand)
|
||||
env.AddMethod(AddOpenOCDFlashTarget)
|
||||
env.AddMethod(GetProjetDirName)
|
||||
env.AddMethod(AddJFlashTarget)
|
||||
env.AddMethod(AddUsbFlashTarget)
|
||||
|
||||
|
@@ -1,29 +1,151 @@
|
||||
from SCons.Builder import Builder
|
||||
from SCons.Action import Action
|
||||
from SCons.Errors import UserError
|
||||
import SCons.Warnings
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
from fbt.elfmanifest import assemble_manifest_data
|
||||
from fbt.sdk import SdkCache
|
||||
import itertools
|
||||
|
||||
|
||||
def BuildAppElf(env, app):
|
||||
work_dir = env.subst("$EXT_APPS_WORK_DIR")
|
||||
app_target_name = os.path.join(work_dir, app.appid)
|
||||
|
||||
app_alias = f"{env['FIRMWARE_BUILD_CFG']}_{app.appid}"
|
||||
app_elf = env.Program(
|
||||
app_target_name,
|
||||
env.GlobRecursive("*.c*", os.path.join(work_dir, app._appdir)),
|
||||
APP_ENTRY=app.entry_point,
|
||||
app_original_elf = os.path.join(work_dir, f"{app.appid}_d")
|
||||
app_sources = list(
|
||||
itertools.chain.from_iterable(
|
||||
env.GlobRecursive(source_type, os.path.join(work_dir, app._appdir.relpath))
|
||||
for source_type in app.sources
|
||||
)
|
||||
)
|
||||
app_elf_dump = env.ObjDump(app_target_name)
|
||||
app_elf_raw = env.Program(
|
||||
app_original_elf,
|
||||
app_sources,
|
||||
APP_ENTRY=app.entry_point,
|
||||
LIBS=env["LIBS"] + app.fap_libs,
|
||||
)
|
||||
|
||||
app_elf_dump = env.ObjDump(app_elf_raw)
|
||||
env.Alias(f"{app_alias}_list", app_elf_dump)
|
||||
|
||||
app_stripped_elf = env.ELFStripper(
|
||||
os.path.join(env.subst("$PLUGIN_ELF_DIR"), app.appid), app_elf
|
||||
app_elf_augmented = env.EmbedAppMetadata(
|
||||
os.path.join(env.subst("$PLUGIN_ELF_DIR"), app.appid),
|
||||
app_elf_raw,
|
||||
APP=app,
|
||||
)
|
||||
env.Alias(app_alias, app_stripped_elf)
|
||||
return app_stripped_elf
|
||||
|
||||
env.Depends(app_elf_augmented, [env["SDK_DEFINITION"], env.Value(app)])
|
||||
if app.fap_icon:
|
||||
env.Depends(
|
||||
app_elf_augmented,
|
||||
env.File(f"{app._apppath}/{app.fap_icon}"),
|
||||
)
|
||||
env.Alias(app_alias, app_elf_augmented)
|
||||
|
||||
app_elf_import_validator = env.ValidateAppImports(app_elf_augmented)
|
||||
env.AlwaysBuild(app_elf_import_validator)
|
||||
return (app_elf_augmented, app_elf_raw, app_elf_import_validator)
|
||||
|
||||
|
||||
def prepare_app_metadata(target, source, env):
|
||||
sdk_cache = SdkCache(env.subst("$SDK_DEFINITION"), load_version_only=True)
|
||||
|
||||
if not sdk_cache.is_buildable():
|
||||
raise UserError(
|
||||
"SDK version is not finalized, please review changes and re-run operation"
|
||||
)
|
||||
|
||||
app = env["APP"]
|
||||
meta_file_name = source[0].path + ".meta"
|
||||
with open(meta_file_name, "wb") as f:
|
||||
# f.write(f"hello this is {app}")
|
||||
f.write(
|
||||
assemble_manifest_data(
|
||||
app_manifest=app,
|
||||
hardware_target=int(env.subst("$TARGET_HW")),
|
||||
sdk_version=sdk_cache.version.as_int(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def validate_app_imports(target, source, env):
|
||||
sdk_cache = SdkCache(env.subst("$SDK_DEFINITION"), load_version_only=False)
|
||||
app_syms = set()
|
||||
with open(target[0].path, "rt") as f:
|
||||
for line in f:
|
||||
app_syms.add(line.split()[0])
|
||||
unresolved_syms = app_syms - sdk_cache.get_valid_names()
|
||||
if unresolved_syms:
|
||||
SCons.Warnings.warn(
|
||||
SCons.Warnings.LinkWarning,
|
||||
f"{source[0].path}: app won't run. Unresolved symbols: {unresolved_syms}",
|
||||
)
|
||||
|
||||
|
||||
def GetExtAppFromPath(env, app_dir):
|
||||
if not app_dir:
|
||||
raise UserError("APPSRC= not set")
|
||||
|
||||
appmgr = env["APPMGR"]
|
||||
|
||||
app = None
|
||||
for dir_part in reversed(pathlib.Path(app_dir).parts):
|
||||
if app := appmgr.find_by_appdir(dir_part):
|
||||
break
|
||||
if not app:
|
||||
raise UserError(f"Failed to resolve application for given APPSRC={app_dir}")
|
||||
|
||||
app_elf = env["_extapps"]["compact"].get(app.appid, None)
|
||||
if not app_elf:
|
||||
raise UserError(f"No external app found for {app.appid}")
|
||||
|
||||
return (app, app_elf[0])
|
||||
|
||||
|
||||
def generate(env, **kw):
|
||||
env.SetDefault(EXT_APPS_WORK_DIR=kw.get("EXT_APPS_WORK_DIR", ".extapps"))
|
||||
env.VariantDir(env.subst("$EXT_APPS_WORK_DIR"), ".", duplicate=False)
|
||||
env.SetDefault(EXT_APPS_WORK_DIR=kw.get("EXT_APPS_WORK_DIR"))
|
||||
env.VariantDir(env.subst("$EXT_APPS_WORK_DIR"), env.Dir("#"), duplicate=False)
|
||||
|
||||
env.AddMethod(BuildAppElf)
|
||||
env.AddMethod(GetExtAppFromPath)
|
||||
env.Append(
|
||||
BUILDERS={
|
||||
"EmbedAppMetadata": Builder(
|
||||
action=[
|
||||
Action(prepare_app_metadata, "$APPMETA_COMSTR"),
|
||||
Action(
|
||||
"${OBJCOPY} "
|
||||
"--remove-section .ARM.attributes "
|
||||
"--add-section .fapmeta=${SOURCE}.meta "
|
||||
"--set-section-flags .fapmeta=contents,noload,readonly,data "
|
||||
"--strip-debug --strip-unneeded "
|
||||
"--add-gnu-debuglink=${SOURCE} "
|
||||
"${SOURCES} ${TARGET}",
|
||||
"$APPMETAEMBED_COMSTR",
|
||||
),
|
||||
],
|
||||
suffix=".fap",
|
||||
src_suffix=".elf",
|
||||
),
|
||||
"ValidateAppImports": Builder(
|
||||
action=[
|
||||
Action(
|
||||
"@${NM} -P -u ${SOURCE} > ${TARGET}",
|
||||
None, # "$APPDUMP_COMSTR",
|
||||
),
|
||||
Action(
|
||||
validate_app_imports,
|
||||
None, # "$APPCHECK_COMSTR",
|
||||
),
|
||||
],
|
||||
suffix=".impsyms",
|
||||
src_suffix=".fap",
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def exists(env):
|
||||
|
208
site_scons/site_tools/fbt_sdk.py
Normal file
208
site_scons/site_tools/fbt_sdk.py
Normal file
@@ -0,0 +1,208 @@
|
||||
from SCons.Builder import Builder
|
||||
from SCons.Action import Action
|
||||
from SCons.Errors import UserError
|
||||
|
||||
# from SCons.Scanner import C
|
||||
from SCons.Script import Mkdir, Copy, Delete, Entry
|
||||
from SCons.Util import LogicalLines
|
||||
|
||||
import os.path
|
||||
import posixpath
|
||||
import pathlib
|
||||
|
||||
from fbt.sdk import SdkCollector, SdkCache
|
||||
|
||||
|
||||
def prebuild_sdk_emitter(target, source, env):
|
||||
target.append(env.ChangeFileExtension(target[0], ".d"))
|
||||
return target, source
|
||||
|
||||
|
||||
def prebuild_sdk_create_origin_file(target, source, env):
|
||||
mega_file = env.subst("${TARGET}.c", target=target[0])
|
||||
with open(mega_file, "wt") as sdk_c:
|
||||
sdk_c.write("\n".join(f"#include <{h.path}>" for h in env["SDK_HEADERS"]))
|
||||
|
||||
|
||||
class SdkTreeBuilder:
|
||||
def __init__(self, env, target, source) -> None:
|
||||
self.env = env
|
||||
self.target = target
|
||||
self.source = source
|
||||
|
||||
self.header_depends = []
|
||||
self.header_dirs = []
|
||||
|
||||
self.target_sdk_dir = env.subst("f${TARGET_HW}_sdk")
|
||||
self.sdk_deploy_dir = target[0].Dir(self.target_sdk_dir)
|
||||
|
||||
def _parse_sdk_depends(self):
|
||||
deps_file = self.source[0]
|
||||
with open(deps_file.path, "rt") as deps_f:
|
||||
lines = LogicalLines(deps_f).readlines()
|
||||
_, depends = lines[0].split(":", 1)
|
||||
self.header_depends = list(
|
||||
filter(lambda fname: fname.endswith(".h"), depends.split()),
|
||||
)
|
||||
self.header_dirs = sorted(
|
||||
set(map(os.path.normpath, map(os.path.dirname, self.header_depends)))
|
||||
)
|
||||
|
||||
def _generate_sdk_meta(self):
|
||||
filtered_paths = [self.target_sdk_dir]
|
||||
full_fw_paths = list(
|
||||
map(
|
||||
os.path.normpath,
|
||||
(self.env.Dir(inc_dir).relpath for inc_dir in self.env["CPPPATH"]),
|
||||
)
|
||||
)
|
||||
|
||||
sdk_dirs = ", ".join(f"'{dir}'" for dir in self.header_dirs)
|
||||
for dir in full_fw_paths:
|
||||
if dir in sdk_dirs:
|
||||
filtered_paths.append(
|
||||
posixpath.normpath(posixpath.join(self.target_sdk_dir, dir))
|
||||
)
|
||||
|
||||
sdk_env = self.env.Clone()
|
||||
sdk_env.Replace(CPPPATH=filtered_paths)
|
||||
with open(self.target[0].path, "wt") as f:
|
||||
cmdline_options = sdk_env.subst(
|
||||
"$CCFLAGS $_CCCOMCOM", target=Entry("dummy")
|
||||
)
|
||||
f.write(cmdline_options.replace("\\", "/"))
|
||||
f.write("\n")
|
||||
|
||||
def _create_deploy_commands(self):
|
||||
dirs_to_create = set(
|
||||
self.sdk_deploy_dir.Dir(dirpath) for dirpath in self.header_dirs
|
||||
)
|
||||
actions = [
|
||||
Delete(self.sdk_deploy_dir),
|
||||
Mkdir(self.sdk_deploy_dir),
|
||||
]
|
||||
actions += [Mkdir(d) for d in dirs_to_create]
|
||||
|
||||
actions += [
|
||||
Copy(
|
||||
self.sdk_deploy_dir.File(h).path,
|
||||
h,
|
||||
)
|
||||
for h in self.header_depends
|
||||
]
|
||||
return actions
|
||||
|
||||
def generate_actions(self):
|
||||
self._parse_sdk_depends()
|
||||
self._generate_sdk_meta()
|
||||
|
||||
return self._create_deploy_commands()
|
||||
|
||||
|
||||
def deploy_sdk_tree(target, source, env, for_signature):
|
||||
if for_signature:
|
||||
return []
|
||||
|
||||
sdk_tree = SdkTreeBuilder(env, target, source)
|
||||
return sdk_tree.generate_actions()
|
||||
|
||||
|
||||
def gen_sdk_data(sdk_cache: SdkCache):
|
||||
api_def = []
|
||||
api_def.extend(
|
||||
(f"#include <{h.name}>" for h in sdk_cache.get_headers()),
|
||||
)
|
||||
|
||||
api_def.append(f"const int elf_api_version = {sdk_cache.version.as_int()};")
|
||||
|
||||
api_def.append(
|
||||
"static constexpr auto elf_api_table = sort(create_array_t<sym_entry>("
|
||||
)
|
||||
|
||||
api_lines = []
|
||||
for fun_def in sdk_cache.get_functions():
|
||||
api_lines.append(
|
||||
f"API_METHOD({fun_def.name}, {fun_def.returns}, ({fun_def.params}))"
|
||||
)
|
||||
|
||||
for var_def in sdk_cache.get_variables():
|
||||
api_lines.append(f"API_VARIABLE({var_def.name}, {var_def.var_type })")
|
||||
|
||||
api_def.append(",\n".join(api_lines))
|
||||
|
||||
api_def.append("));")
|
||||
return api_def
|
||||
|
||||
|
||||
def _check_sdk_is_up2date(sdk_cache: SdkCache):
|
||||
if not sdk_cache.is_buildable():
|
||||
raise UserError(
|
||||
"SDK version is not finalized, please review changes and re-run operation"
|
||||
)
|
||||
|
||||
|
||||
def validate_sdk_cache(source, target, env):
|
||||
# print(f"Generating SDK for {source[0]} to {target[0]}")
|
||||
current_sdk = SdkCollector()
|
||||
current_sdk.process_source_file_for_sdk(source[0].path)
|
||||
for h in env["SDK_HEADERS"]:
|
||||
current_sdk.add_header_to_sdk(pathlib.Path(h.path).as_posix())
|
||||
|
||||
sdk_cache = SdkCache(target[0].path)
|
||||
sdk_cache.validate_api(current_sdk.get_api())
|
||||
sdk_cache.save()
|
||||
_check_sdk_is_up2date(sdk_cache)
|
||||
|
||||
|
||||
def generate_sdk_symbols(source, target, env):
|
||||
sdk_cache = SdkCache(source[0].path)
|
||||
_check_sdk_is_up2date(sdk_cache)
|
||||
|
||||
api_def = gen_sdk_data(sdk_cache)
|
||||
with open(target[0].path, "wt") as f:
|
||||
f.write("\n".join(api_def))
|
||||
|
||||
|
||||
def generate(env, **kw):
|
||||
env.Append(
|
||||
BUILDERS={
|
||||
"SDKPrebuilder": Builder(
|
||||
emitter=prebuild_sdk_emitter,
|
||||
action=[
|
||||
Action(
|
||||
prebuild_sdk_create_origin_file,
|
||||
"$SDK_PREGEN_COMSTR",
|
||||
),
|
||||
Action(
|
||||
"$CC -o $TARGET -E -P $CCFLAGS $_CCCOMCOM $SDK_PP_FLAGS -MMD ${TARGET}.c",
|
||||
"$SDK_COMSTR",
|
||||
),
|
||||
],
|
||||
suffix=".i",
|
||||
),
|
||||
"SDKTree": Builder(
|
||||
generator=deploy_sdk_tree,
|
||||
src_suffix=".d",
|
||||
),
|
||||
"SDKSymUpdater": Builder(
|
||||
action=Action(
|
||||
validate_sdk_cache,
|
||||
"$SDKSYM_UPDATER_COMSTR",
|
||||
),
|
||||
suffix=".csv",
|
||||
src_suffix=".i",
|
||||
),
|
||||
"SDKSymGenerator": Builder(
|
||||
action=Action(
|
||||
generate_sdk_symbols,
|
||||
"$SDKSYM_GENERATOR_COMSTR",
|
||||
),
|
||||
suffix=".h",
|
||||
src_suffix=".csv",
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def exists(env):
|
||||
return True
|
@@ -3,12 +3,14 @@ from SCons.Action import Action
|
||||
import SCons
|
||||
|
||||
__OBJCOPY_ARM_BIN = "arm-none-eabi-objcopy"
|
||||
__NM_ARM_BIN = "arm-none-eabi-nm"
|
||||
|
||||
|
||||
def generate(env):
|
||||
env.SetDefault(
|
||||
BIN2DFU="${ROOT_DIR.abspath}/scripts/bin2dfu.py",
|
||||
OBJCOPY=__OBJCOPY_ARM_BIN, # FIXME
|
||||
NM=__NM_ARM_BIN, # FIXME
|
||||
)
|
||||
env.Append(
|
||||
BUILDERS={
|
||||
|
@@ -40,10 +40,15 @@ def PhonyTarget(env, name, action, source=None, **kw):
|
||||
return command
|
||||
|
||||
|
||||
def ChangeFileExtension(env, fnode, ext):
|
||||
return env.File(f"#{os.path.splitext(fnode.path)[0]}{ext}")
|
||||
|
||||
|
||||
def generate(env):
|
||||
env.AddMethod(BuildModule)
|
||||
env.AddMethod(BuildModules)
|
||||
env.AddMethod(PhonyTarget)
|
||||
env.AddMethod(ChangeFileExtension)
|
||||
|
||||
|
||||
def exists(env):
|
||||
|
Reference in New Issue
Block a user