fbt_extapps.py 7.9 KB

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