fbt_extapps.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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(
  69. os.path.join(app._appdir.path, cinclude)
  70. for cinclude in lib_def.cincludes
  71. ),
  72. )
  73. lib = private_lib_env.StaticLibrary(
  74. os.path.join(app_work_dir, lib_def.name),
  75. lib_sources,
  76. )
  77. private_libs.append(lib)
  78. app_sources = list(
  79. itertools.chain.from_iterable(
  80. app_env.GlobRecursive(
  81. source_type,
  82. app_work_dir,
  83. exclude="lib",
  84. )
  85. for source_type in app.sources
  86. )
  87. )
  88. app_env.Append(
  89. LIBS=[*app.fap_libs, *private_libs],
  90. CPPPATH=env.Dir(app_work_dir),
  91. )
  92. app_elf_raw = app_env.Program(
  93. os.path.join(ext_apps_work_dir, f"{app.appid}_d"),
  94. app_sources,
  95. APP_ENTRY=app.entry_point,
  96. )
  97. app_env.Clean(app_elf_raw, [*externally_built_files, app_env.Dir(app_work_dir)])
  98. app_elf_dump = app_env.ObjDump(app_elf_raw)
  99. app_env.Alias(f"{app_alias}_list", app_elf_dump)
  100. app_elf_augmented = app_env.EmbedAppMetadata(
  101. os.path.join(ext_apps_work_dir, app.appid),
  102. app_elf_raw,
  103. APP=app,
  104. )
  105. manifest_vals = {
  106. k: v
  107. for k, v in vars(app).items()
  108. if not k.startswith(FlipperApplication.PRIVATE_FIELD_PREFIX)
  109. }
  110. app_env.Depends(
  111. app_elf_augmented,
  112. [app_env["SDK_DEFINITION"], app_env.Value(manifest_vals)],
  113. )
  114. if app.fap_icon:
  115. app_env.Depends(
  116. app_elf_augmented,
  117. app_env.File(f"{app._apppath}/{app.fap_icon}"),
  118. )
  119. app_elf_import_validator = app_env.ValidateAppImports(app_elf_augmented)
  120. app_env.AlwaysBuild(app_elf_import_validator)
  121. app_env.Alias(app_alias, app_elf_import_validator)
  122. return (app_elf_augmented, app_elf_raw, app_elf_import_validator)
  123. def prepare_app_metadata(target, source, env):
  124. sdk_cache = SdkCache(env.subst("$SDK_DEFINITION"), load_version_only=True)
  125. if not sdk_cache.is_buildable():
  126. raise UserError(
  127. "SDK version is not finalized, please review changes and re-run operation"
  128. )
  129. app = env["APP"]
  130. meta_file_name = source[0].path + ".meta"
  131. with open(meta_file_name, "wb") as f:
  132. # f.write(f"hello this is {app}")
  133. f.write(
  134. assemble_manifest_data(
  135. app_manifest=app,
  136. hardware_target=int(env.subst("$TARGET_HW")),
  137. sdk_version=sdk_cache.version.as_int(),
  138. )
  139. )
  140. def validate_app_imports(target, source, env):
  141. sdk_cache = SdkCache(env.subst("$SDK_DEFINITION"), load_version_only=False)
  142. app_syms = set()
  143. with open(target[0].path, "rt") as f:
  144. for line in f:
  145. app_syms.add(line.split()[0])
  146. unresolved_syms = app_syms - sdk_cache.get_valid_names()
  147. if unresolved_syms:
  148. SCons.Warnings.warn(
  149. SCons.Warnings.LinkWarning,
  150. f"\033[93m{source[0].path}: app won't run. Unresolved symbols: \033[95m{unresolved_syms}\033[0m",
  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 generate(env, **kw):
  175. env.SetDefault(EXT_APPS_WORK_DIR=kw.get("EXT_APPS_WORK_DIR"))
  176. # env.VariantDir(env.subst("$EXT_APPS_WORK_DIR"), env.Dir("#"), duplicate=False)
  177. env.AddMethod(BuildAppElf)
  178. env.AddMethod(GetExtAppFromPath)
  179. env.Append(
  180. BUILDERS={
  181. "EmbedAppMetadata": Builder(
  182. action=[
  183. Action(prepare_app_metadata, "$APPMETA_COMSTR"),
  184. Action(
  185. "${OBJCOPY} "
  186. "--remove-section .ARM.attributes "
  187. "--add-section .fapmeta=${SOURCE}.meta "
  188. "--set-section-flags .fapmeta=contents,noload,readonly,data "
  189. "--strip-debug --strip-unneeded "
  190. "--add-gnu-debuglink=${SOURCE} "
  191. "${SOURCES} ${TARGET}",
  192. "$APPMETAEMBED_COMSTR",
  193. ),
  194. ],
  195. suffix=".fap",
  196. src_suffix=".elf",
  197. ),
  198. "ValidateAppImports": Builder(
  199. action=[
  200. Action(
  201. "@${NM} -P -u ${SOURCE} > ${TARGET}",
  202. None, # "$APPDUMP_COMSTR",
  203. ),
  204. Action(
  205. validate_app_imports,
  206. "$APPCHECK_COMSTR",
  207. ),
  208. ],
  209. suffix=".impsyms",
  210. src_suffix=".fap",
  211. ),
  212. }
  213. )
  214. def exists(env):
  215. return True