fbt_extapps.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. from dataclasses import dataclass, field
  2. from typing import Optional
  3. from SCons.Builder import Builder
  4. from SCons.Action import Action
  5. from SCons.Errors import UserError
  6. from SCons.Node import NodeList
  7. import SCons.Warnings
  8. from fbt.elfmanifest import assemble_manifest_data
  9. from fbt.appmanifest import FlipperApplication, FlipperManifestException, FlipperAppType
  10. from fbt.sdk.cache import SdkCache
  11. from fbt.util import extract_abs_dir_path
  12. import os
  13. import pathlib
  14. import itertools
  15. import shutil
  16. from ansi.color import fg
  17. @dataclass
  18. class FlipperExternalAppInfo:
  19. app: FlipperApplication
  20. compact: NodeList = field(default_factory=NodeList)
  21. debug: NodeList = field(default_factory=NodeList)
  22. validator: NodeList = field(default_factory=NodeList)
  23. installer: NodeList = field(default_factory=NodeList)
  24. def BuildAppElf(env, app):
  25. ext_apps_work_dir = env.subst("$EXT_APPS_WORK_DIR")
  26. app_work_dir = os.path.join(ext_apps_work_dir, app.appid)
  27. env.SetDefault(_APP_ICONS=[])
  28. env.VariantDir(app_work_dir, app._appdir, duplicate=False)
  29. app_env = env.Clone(FAP_SRC_DIR=app._appdir, FAP_WORK_DIR=app_work_dir)
  30. app_alias = f"fap_{app.appid}"
  31. app_artifacts = FlipperExternalAppInfo(app)
  32. externally_built_files = []
  33. if app.fap_extbuild:
  34. for external_file_def in app.fap_extbuild:
  35. externally_built_files.append(external_file_def.path)
  36. app_env.Alias(app_alias, external_file_def.path)
  37. app_env.AlwaysBuild(
  38. app_env.Command(
  39. external_file_def.path,
  40. None,
  41. Action(
  42. external_file_def.command,
  43. "" if app_env["VERBOSE"] else "\tEXTCMD\t${TARGET}",
  44. ),
  45. )
  46. )
  47. if app.fap_icon_assets:
  48. fap_icons = app_env.CompileIcons(
  49. app_env.Dir(app_work_dir),
  50. app._appdir.Dir(app.fap_icon_assets),
  51. icon_bundle_name=f"{app.fap_icon_assets_symbol if app.fap_icon_assets_symbol else app.appid }_icons",
  52. )
  53. app_env.Alias("_fap_icons", fap_icons)
  54. env.Append(_APP_ICONS=[fap_icons])
  55. private_libs = []
  56. for lib_def in app.fap_private_libs:
  57. lib_src_root_path = os.path.join(app_work_dir, "lib", lib_def.name)
  58. app_env.AppendUnique(
  59. CPPPATH=list(
  60. app_env.Dir(lib_src_root_path).Dir(incpath).srcnode().rfile().abspath
  61. for incpath in lib_def.fap_include_paths
  62. ),
  63. )
  64. lib_sources = list(
  65. itertools.chain.from_iterable(
  66. app_env.GlobRecursive(source_type, lib_src_root_path)
  67. for source_type in lib_def.sources
  68. )
  69. )
  70. if len(lib_sources) == 0:
  71. raise UserError(f"No sources gathered for private library {lib_def}")
  72. private_lib_env = app_env.Clone()
  73. private_lib_env.AppendUnique(
  74. CCFLAGS=[
  75. *lib_def.cflags,
  76. ],
  77. CPPDEFINES=lib_def.cdefines,
  78. CPPPATH=list(
  79. map(
  80. lambda cpath: extract_abs_dir_path(app._appdir.Dir(cpath)),
  81. lib_def.cincludes,
  82. )
  83. ),
  84. )
  85. lib = private_lib_env.StaticLibrary(
  86. os.path.join(app_work_dir, lib_def.name),
  87. lib_sources,
  88. )
  89. private_libs.append(lib)
  90. app_sources = list(
  91. itertools.chain.from_iterable(
  92. app_env.GlobRecursive(
  93. source_type,
  94. app_work_dir,
  95. exclude="lib",
  96. )
  97. for source_type in app.sources
  98. )
  99. )
  100. app_env.Append(
  101. LIBS=[*app.fap_libs, *private_libs],
  102. CPPPATH=env.Dir(app_work_dir),
  103. )
  104. app_artifacts.debug = app_env.Program(
  105. os.path.join(ext_apps_work_dir, f"{app.appid}_d"),
  106. app_sources,
  107. APP_ENTRY=app.entry_point,
  108. )
  109. app_env.Clean(
  110. app_artifacts.debug, [*externally_built_files, app_env.Dir(app_work_dir)]
  111. )
  112. app_elf_dump = app_env.ObjDump(app_artifacts.debug)
  113. app_env.Alias(f"{app_alias}_list", app_elf_dump)
  114. app_artifacts.compact = app_env.EmbedAppMetadata(
  115. os.path.join(ext_apps_work_dir, app.appid),
  116. app_artifacts.debug,
  117. APP=app,
  118. )
  119. manifest_vals = {
  120. k: v
  121. for k, v in vars(app).items()
  122. if not k.startswith(FlipperApplication.PRIVATE_FIELD_PREFIX)
  123. }
  124. app_env.Depends(
  125. app_artifacts.compact,
  126. [app_env["SDK_DEFINITION"], app_env.Value(manifest_vals)],
  127. )
  128. if app.fap_icon:
  129. app_env.Depends(
  130. app_artifacts.compact,
  131. app_env.File(f"{app._apppath}/{app.fap_icon}"),
  132. )
  133. app_artifacts.validator = app_env.ValidateAppImports(app_artifacts.compact)
  134. app_env.AlwaysBuild(app_artifacts.validator)
  135. app_env.Alias(app_alias, app_artifacts.validator)
  136. env["EXT_APPS"][app.appid] = app_artifacts
  137. return app_artifacts
  138. def prepare_app_metadata(target, source, env):
  139. sdk_cache = SdkCache(env["SDK_DEFINITION"].path, load_version_only=True)
  140. if not sdk_cache.is_buildable():
  141. raise UserError(
  142. "SDK version is not finalized, please review changes and re-run operation"
  143. )
  144. app = env["APP"]
  145. meta_file_name = source[0].path + ".meta"
  146. with open(meta_file_name, "wb") as f:
  147. f.write(
  148. assemble_manifest_data(
  149. app_manifest=app,
  150. hardware_target=int(env.subst("$TARGET_HW")),
  151. sdk_version=sdk_cache.version.as_int(),
  152. )
  153. )
  154. def validate_app_imports(target, source, env):
  155. sdk_cache = SdkCache(env["SDK_DEFINITION"].path, load_version_only=False)
  156. app_syms = set()
  157. with open(target[0].path, "rt") as f:
  158. for line in f:
  159. app_syms.add(line.split()[0])
  160. unresolved_syms = app_syms - sdk_cache.get_valid_names()
  161. if unresolved_syms:
  162. warning_msg = fg.brightyellow(
  163. f"{source[0].path}: app won't run. Unresolved symbols: "
  164. ) + fg.brightmagenta(f"{unresolved_syms}")
  165. disabled_api_syms = unresolved_syms.intersection(sdk_cache.get_disabled_names())
  166. if disabled_api_syms:
  167. warning_msg += (
  168. fg.brightyellow(" (in API, but disabled: ")
  169. + fg.brightmagenta(f"{disabled_api_syms}")
  170. + fg.brightyellow(")")
  171. )
  172. SCons.Warnings.warn(SCons.Warnings.LinkWarning, warning_msg),
  173. def GetExtAppFromPath(env, app_dir):
  174. if not app_dir:
  175. raise UserError("APPSRC= not set")
  176. appmgr = env["APPMGR"]
  177. app = None
  178. try:
  179. # Maybe used passed an appid?
  180. app = appmgr.get(app_dir)
  181. except FlipperManifestException as _:
  182. # Look up path components in known app dits
  183. for dir_part in reversed(pathlib.Path(app_dir).parts):
  184. if app := appmgr.find_by_appdir(dir_part):
  185. break
  186. if not app:
  187. raise UserError(f"Failed to resolve application for given APPSRC={app_dir}")
  188. app_artifacts = env["EXT_APPS"].get(app.appid, None)
  189. if not app_artifacts:
  190. raise UserError(
  191. f"Application {app.appid} is not configured for building as external"
  192. )
  193. return app_artifacts
  194. def resources_fap_dist_emitter(target, source, env):
  195. target_dir = target[0]
  196. target = []
  197. for _, app_artifacts in env["EXT_APPS"].items():
  198. # We don't deploy example apps & debug tools with SD card resources
  199. if (
  200. app_artifacts.app.apptype == FlipperAppType.DEBUG
  201. or app_artifacts.app.fap_category == "Examples"
  202. ):
  203. continue
  204. source.extend(app_artifacts.compact)
  205. target.append(
  206. target_dir.Dir(app_artifacts.app.fap_category).File(
  207. app_artifacts.compact[0].name
  208. )
  209. )
  210. return (target, source)
  211. def resources_fap_dist_action(target, source, env):
  212. # FIXME
  213. target_dir = env.Dir("#/assets/resources/apps")
  214. shutil.rmtree(target_dir.path, ignore_errors=True)
  215. for src, target in zip(source, target):
  216. os.makedirs(os.path.dirname(target.path), exist_ok=True)
  217. shutil.copy(src.path, target.path)
  218. def generate(env, **kw):
  219. env.SetDefault(
  220. EXT_APPS_WORK_DIR="${FBT_FAP_DEBUG_ELF_ROOT}",
  221. APP_RUN_SCRIPT="${FBT_SCRIPT_DIR}/runfap.py",
  222. )
  223. if not env["VERBOSE"]:
  224. env.SetDefault(
  225. FAPDISTCOMSTR="\tFAPDIST\t${TARGET}",
  226. APPMETA_COMSTR="\tAPPMETA\t${TARGET}",
  227. APPMETAEMBED_COMSTR="\tFAP\t${TARGET}",
  228. APPCHECK_COMSTR="\tAPPCHK\t${SOURCE}",
  229. )
  230. env.SetDefault(
  231. EXT_APPS={}, # appid -> FlipperExternalAppInfo
  232. )
  233. env.AddMethod(BuildAppElf)
  234. env.AddMethod(GetExtAppFromPath)
  235. env.Append(
  236. BUILDERS={
  237. "FapDist": Builder(
  238. action=Action(
  239. resources_fap_dist_action,
  240. "$FAPDISTCOMSTR",
  241. ),
  242. emitter=resources_fap_dist_emitter,
  243. ),
  244. "EmbedAppMetadata": Builder(
  245. action=[
  246. Action(prepare_app_metadata, "$APPMETA_COMSTR"),
  247. Action(
  248. "${OBJCOPY} "
  249. "--remove-section .ARM.attributes "
  250. "--add-section .fapmeta=${SOURCE}.meta "
  251. "--set-section-flags .fapmeta=contents,noload,readonly,data "
  252. "--strip-debug --strip-unneeded "
  253. "--add-gnu-debuglink=${SOURCE} "
  254. "${SOURCES} ${TARGET}",
  255. "$APPMETAEMBED_COMSTR",
  256. ),
  257. ],
  258. suffix=".fap",
  259. src_suffix=".elf",
  260. ),
  261. "ValidateAppImports": Builder(
  262. action=[
  263. Action(
  264. "@${NM} -P -u ${SOURCE} > ${TARGET}",
  265. None, # "$APPDUMP_COMSTR",
  266. ),
  267. Action(
  268. validate_app_imports,
  269. "$APPCHECK_COMSTR",
  270. ),
  271. ],
  272. suffix=".impsyms",
  273. src_suffix=".fap",
  274. ),
  275. }
  276. )
  277. def exists(env):
  278. return True