fbt_extapps.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. from dataclasses import dataclass, field
  2. from typing import Optional, TypedDict
  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. import struct
  17. import hashlib
  18. from ansi.color import fg
  19. @dataclass
  20. class FlipperExternalAppInfo:
  21. app: FlipperApplication
  22. compact: NodeList = field(default_factory=NodeList)
  23. debug: NodeList = field(default_factory=NodeList)
  24. validator: NodeList = field(default_factory=NodeList)
  25. installer: NodeList = field(default_factory=NodeList)
  26. def BuildAppElf(env, app):
  27. ext_apps_work_dir = env.subst("$EXT_APPS_WORK_DIR")
  28. app_work_dir = os.path.join(ext_apps_work_dir, app.appid)
  29. env.SetDefault(_APP_ICONS=[])
  30. env.VariantDir(app_work_dir, app._appdir, duplicate=False)
  31. app_env = env.Clone(FAP_SRC_DIR=app._appdir, FAP_WORK_DIR=app_work_dir)
  32. app_alias = f"fap_{app.appid}"
  33. app_artifacts = FlipperExternalAppInfo(app)
  34. externally_built_files = []
  35. if app.fap_extbuild:
  36. for external_file_def in app.fap_extbuild:
  37. externally_built_files.append(external_file_def.path)
  38. app_env.Alias(app_alias, external_file_def.path)
  39. app_env.AlwaysBuild(
  40. app_env.Command(
  41. external_file_def.path,
  42. None,
  43. Action(
  44. external_file_def.command,
  45. "" if app_env["VERBOSE"] else "\tEXTCMD\t${TARGET}",
  46. ),
  47. )
  48. )
  49. if app.fap_icon_assets:
  50. fap_icons = app_env.CompileIcons(
  51. app_env.Dir(app_work_dir),
  52. app._appdir.Dir(app.fap_icon_assets),
  53. icon_bundle_name=f"{app.fap_icon_assets_symbol if app.fap_icon_assets_symbol else app.appid }_icons",
  54. )
  55. app_env.Alias("_fap_icons", fap_icons)
  56. env.Append(_APP_ICONS=[fap_icons])
  57. private_libs = []
  58. for lib_def in app.fap_private_libs:
  59. lib_src_root_path = os.path.join(app_work_dir, "lib", lib_def.name)
  60. app_env.AppendUnique(
  61. CPPPATH=list(
  62. app_env.Dir(lib_src_root_path).Dir(incpath).srcnode().rfile().abspath
  63. for incpath in lib_def.fap_include_paths
  64. ),
  65. )
  66. lib_sources = list(
  67. itertools.chain.from_iterable(
  68. app_env.GlobRecursive(source_type, lib_src_root_path)
  69. for source_type in lib_def.sources
  70. )
  71. )
  72. if len(lib_sources) == 0:
  73. raise UserError(f"No sources gathered for private library {lib_def}")
  74. private_lib_env = app_env.Clone()
  75. private_lib_env.AppendUnique(
  76. CCFLAGS=[
  77. *lib_def.cflags,
  78. ],
  79. CPPDEFINES=lib_def.cdefines,
  80. CPPPATH=list(
  81. map(
  82. lambda cpath: extract_abs_dir_path(app._appdir.Dir(cpath)),
  83. lib_def.cincludes,
  84. )
  85. ),
  86. )
  87. lib = private_lib_env.StaticLibrary(
  88. os.path.join(app_work_dir, lib_def.name),
  89. lib_sources,
  90. )
  91. private_libs.append(lib)
  92. app_sources = list(
  93. itertools.chain.from_iterable(
  94. app_env.GlobRecursive(
  95. source_type,
  96. app_work_dir,
  97. exclude="lib",
  98. )
  99. for source_type in app.sources
  100. )
  101. )
  102. app_env.Append(
  103. LIBS=[*app.fap_libs, *private_libs],
  104. CPPPATH=env.Dir(app_work_dir),
  105. )
  106. app_artifacts.debug = app_env.Program(
  107. os.path.join(ext_apps_work_dir, f"{app.appid}_d"),
  108. app_sources,
  109. APP_ENTRY=app.entry_point,
  110. )
  111. app_env.Clean(
  112. app_artifacts.debug, [*externally_built_files, app_env.Dir(app_work_dir)]
  113. )
  114. app_elf_dump = app_env.ObjDump(app_artifacts.debug)
  115. app_env.Alias(f"{app_alias}_list", app_elf_dump)
  116. app_artifacts.compact = app_env.EmbedAppMetadata(
  117. os.path.join(ext_apps_work_dir, app.appid),
  118. app_artifacts.debug,
  119. APP=app,
  120. )
  121. manifest_vals = {
  122. k: v
  123. for k, v in vars(app).items()
  124. if not k.startswith(FlipperApplication.PRIVATE_FIELD_PREFIX)
  125. }
  126. app_env.Depends(
  127. app_artifacts.compact,
  128. [app_env["SDK_DEFINITION"], app_env.Value(manifest_vals)],
  129. )
  130. # Add dependencies on icon files
  131. if app.fap_icon:
  132. app_env.Depends(
  133. app_artifacts.compact,
  134. app_env.File(f"{app._apppath}/{app.fap_icon}"),
  135. )
  136. # Add dependencies on file assets
  137. if app.fap_file_assets:
  138. app_env.Depends(
  139. app_artifacts.compact,
  140. app_env.GlobRecursive(
  141. "*",
  142. app._appdir.Dir(app.fap_file_assets),
  143. ),
  144. )
  145. app_artifacts.validator = app_env.ValidateAppImports(app_artifacts.compact)
  146. app_env.AlwaysBuild(app_artifacts.validator)
  147. app_env.Alias(app_alias, app_artifacts.validator)
  148. env["EXT_APPS"][app.appid] = app_artifacts
  149. return app_artifacts
  150. def prepare_app_metadata(target, source, env):
  151. sdk_cache = SdkCache(env["SDK_DEFINITION"].path, load_version_only=True)
  152. if not sdk_cache.is_buildable():
  153. raise UserError(
  154. "SDK version is not finalized, please review changes and re-run operation"
  155. )
  156. app = env["APP"]
  157. meta_file_name = source[0].path + ".meta"
  158. with open(meta_file_name, "wb") as f:
  159. f.write(
  160. assemble_manifest_data(
  161. app_manifest=app,
  162. hardware_target=int(env.subst("$TARGET_HW")),
  163. sdk_version=sdk_cache.version.as_int(),
  164. )
  165. )
  166. def validate_app_imports(target, source, env):
  167. sdk_cache = SdkCache(env["SDK_DEFINITION"].path, load_version_only=False)
  168. app_syms = set()
  169. with open(target[0].path, "rt") as f:
  170. for line in f:
  171. app_syms.add(line.split()[0])
  172. unresolved_syms = app_syms - sdk_cache.get_valid_names()
  173. if unresolved_syms:
  174. warning_msg = fg.brightyellow(
  175. f"{source[0].path}: app won't run. Unresolved symbols: "
  176. ) + fg.brightmagenta(f"{unresolved_syms}")
  177. disabled_api_syms = unresolved_syms.intersection(sdk_cache.get_disabled_names())
  178. if disabled_api_syms:
  179. warning_msg += (
  180. fg.brightyellow(" (in API, but disabled: ")
  181. + fg.brightmagenta(f"{disabled_api_syms}")
  182. + fg.brightyellow(")")
  183. )
  184. SCons.Warnings.warn(SCons.Warnings.LinkWarning, warning_msg),
  185. def GetExtAppFromPath(env, app_dir):
  186. if not app_dir:
  187. raise UserError("APPSRC= not set")
  188. appmgr = env["APPMGR"]
  189. app = None
  190. try:
  191. # Maybe used passed an appid?
  192. app = appmgr.get(app_dir)
  193. except FlipperManifestException as _:
  194. # Look up path components in known app dits
  195. for dir_part in reversed(pathlib.Path(app_dir).parts):
  196. if app := appmgr.find_by_appdir(dir_part):
  197. break
  198. if not app:
  199. raise UserError(f"Failed to resolve application for given APPSRC={app_dir}")
  200. app_artifacts = env["EXT_APPS"].get(app.appid, None)
  201. if not app_artifacts:
  202. raise UserError(
  203. f"Application {app.appid} is not configured for building as external"
  204. )
  205. return app_artifacts
  206. def resources_fap_dist_emitter(target, source, env):
  207. target_dir = target[0]
  208. target = []
  209. for _, app_artifacts in env["EXT_APPS"].items():
  210. # We don't deploy example apps & debug tools with SD card resources
  211. if (
  212. app_artifacts.app.apptype == FlipperAppType.DEBUG
  213. or app_artifacts.app.fap_category == "Examples"
  214. ):
  215. continue
  216. source.extend(app_artifacts.compact)
  217. target.append(
  218. target_dir.Dir(app_artifacts.app.fap_category).File(
  219. app_artifacts.compact[0].name
  220. )
  221. )
  222. return (target, source)
  223. def resources_fap_dist_action(target, source, env):
  224. # FIXME
  225. target_dir = env.Dir("#/assets/resources/apps")
  226. shutil.rmtree(target_dir.path, ignore_errors=True)
  227. for src, target in zip(source, target):
  228. os.makedirs(os.path.dirname(target.path), exist_ok=True)
  229. shutil.copy(src.path, target.path)
  230. def generate_embed_app_metadata_emitter(target, source, env):
  231. app = env["APP"]
  232. meta_file_name = source[0].path + ".meta"
  233. target.append("#" + meta_file_name)
  234. if app.fap_file_assets:
  235. files_section = source[0].path + ".files.section"
  236. target.append("#" + files_section)
  237. return (target, source)
  238. class File(TypedDict):
  239. path: str
  240. size: int
  241. content_path: str
  242. class Dir(TypedDict):
  243. path: str
  244. def prepare_app_files(target, source, env):
  245. app = env["APP"]
  246. directory = app._appdir.Dir(app.fap_file_assets)
  247. directory_path = directory.abspath
  248. if not directory.exists():
  249. raise UserError(f"File asset directory {directory} does not exist")
  250. file_list: list[File] = []
  251. directory_list: list[Dir] = []
  252. for root, dirs, files in os.walk(directory_path):
  253. for file_info in files:
  254. file_path = os.path.join(root, file_info)
  255. file_size = os.path.getsize(file_path)
  256. file_list.append(
  257. {
  258. "path": os.path.relpath(file_path, directory_path),
  259. "size": file_size,
  260. "content_path": file_path,
  261. }
  262. )
  263. for dir_info in dirs:
  264. dir_path = os.path.join(root, dir_info)
  265. dir_size = sum(
  266. os.path.getsize(os.path.join(dir_path, f)) for f in os.listdir(dir_path)
  267. )
  268. directory_list.append(
  269. {
  270. "path": os.path.relpath(dir_path, directory_path),
  271. }
  272. )
  273. file_list.sort(key=lambda f: f["path"])
  274. directory_list.sort(key=lambda d: d["path"])
  275. files_section = source[0].path + ".files.section"
  276. with open(files_section, "wb") as f:
  277. # u32 magic
  278. # u32 version
  279. # u32 dirs_count
  280. # u32 files_count
  281. # u32 signature_size
  282. # u8[] signature
  283. # Dirs:
  284. # u32 dir_name length
  285. # u8[] dir_name
  286. # Files:
  287. # u32 file_name length
  288. # u8[] file_name
  289. # u32 file_content_size
  290. # u8[] file_content
  291. # Write header magic and version
  292. f.write(struct.pack("<II", 0x4F4C5A44, 0x01))
  293. # Write dirs count
  294. f.write(struct.pack("<I", len(directory_list)))
  295. # Write files count
  296. f.write(struct.pack("<I", len(file_list)))
  297. md5_hash = hashlib.md5()
  298. md5_hash_size = len(md5_hash.digest())
  299. # write signature size and null signature, we'll fill it in later
  300. f.write(struct.pack("<I", md5_hash_size))
  301. signature_offset = f.tell()
  302. f.write(b"\x00" * md5_hash_size)
  303. # Write dirs
  304. for dir_info in directory_list:
  305. f.write(struct.pack("<I", len(dir_info["path"]) + 1))
  306. f.write(dir_info["path"].encode("ascii") + b"\x00")
  307. md5_hash.update(dir_info["path"].encode("ascii") + b"\x00")
  308. # Write files
  309. for file_info in file_list:
  310. f.write(struct.pack("<I", len(file_info["path"]) + 1))
  311. f.write(file_info["path"].encode("ascii") + b"\x00")
  312. f.write(struct.pack("<I", file_info["size"]))
  313. md5_hash.update(file_info["path"].encode("ascii") + b"\x00")
  314. with open(file_info["content_path"], "rb") as content_file:
  315. content = content_file.read()
  316. f.write(content)
  317. md5_hash.update(content)
  318. # Write signature
  319. f.seek(signature_offset)
  320. f.write(md5_hash.digest())
  321. def generate_embed_app_metadata_actions(source, target, env, for_signature):
  322. app = env["APP"]
  323. actions = [
  324. Action(prepare_app_metadata, "$APPMETA_COMSTR"),
  325. ]
  326. objcopy_str = (
  327. "${OBJCOPY} "
  328. "--remove-section .ARM.attributes "
  329. "--add-section .fapmeta=${SOURCE}.meta "
  330. )
  331. if app.fap_file_assets:
  332. actions.append(Action(prepare_app_files, "$APPFILE_COMSTR"))
  333. objcopy_str += "--add-section .fapassets=${SOURCE}.files.section "
  334. objcopy_str += (
  335. "--set-section-flags .fapmeta=contents,noload,readonly,data "
  336. "--strip-debug --strip-unneeded "
  337. "--add-gnu-debuglink=${SOURCE} "
  338. "${SOURCES} ${TARGET}"
  339. )
  340. actions.append(
  341. Action(
  342. objcopy_str,
  343. "$APPMETAEMBED_COMSTR",
  344. )
  345. )
  346. return Action(actions)
  347. def generate(env, **kw):
  348. env.SetDefault(
  349. EXT_APPS_WORK_DIR="${FBT_FAP_DEBUG_ELF_ROOT}",
  350. APP_RUN_SCRIPT="${FBT_SCRIPT_DIR}/runfap.py",
  351. )
  352. if not env["VERBOSE"]:
  353. env.SetDefault(
  354. FAPDISTCOMSTR="\tFAPDIST\t${TARGET}",
  355. APPMETA_COMSTR="\tAPPMETA\t${TARGET}",
  356. APPFILE_COMSTR="\tAPPFILE\t${TARGET}",
  357. APPMETAEMBED_COMSTR="\tFAP\t${TARGET}",
  358. APPCHECK_COMSTR="\tAPPCHK\t${SOURCE}",
  359. )
  360. env.SetDefault(
  361. EXT_APPS={}, # appid -> FlipperExternalAppInfo
  362. )
  363. env.AddMethod(BuildAppElf)
  364. env.AddMethod(GetExtAppFromPath)
  365. env.Append(
  366. BUILDERS={
  367. "FapDist": Builder(
  368. action=Action(
  369. resources_fap_dist_action,
  370. "$FAPDISTCOMSTR",
  371. ),
  372. emitter=resources_fap_dist_emitter,
  373. ),
  374. "EmbedAppMetadata": Builder(
  375. generator=generate_embed_app_metadata_actions,
  376. suffix=".fap",
  377. src_suffix=".elf",
  378. emitter=generate_embed_app_metadata_emitter,
  379. ),
  380. "ValidateAppImports": Builder(
  381. action=[
  382. Action(
  383. "@${NM} -P -u ${SOURCE} > ${TARGET}",
  384. None, # "$APPDUMP_COMSTR",
  385. ),
  386. Action(
  387. validate_app_imports,
  388. "$APPCHECK_COMSTR",
  389. ),
  390. ],
  391. suffix=".impsyms",
  392. src_suffix=".fap",
  393. ),
  394. }
  395. )
  396. def exists(env):
  397. return True