fbt_extapps.py 9.2 KB

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