fbt_extapps.py 9.0 KB

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