appmanifest.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. from dataclasses import dataclass, field
  2. from typing import List, Optional, Tuple
  3. from enum import Enum
  4. import os
  5. class FlipperManifestException(Exception):
  6. pass
  7. class FlipperAppType(Enum):
  8. SERVICE = "Service"
  9. SYSTEM = "System"
  10. APP = "App"
  11. PLUGIN = "Plugin"
  12. DEBUG = "Debug"
  13. ARCHIVE = "Archive"
  14. SETTINGS = "Settings"
  15. STARTUP = "StartupHook"
  16. EXTERNAL = "External"
  17. METAPACKAGE = "Package"
  18. @dataclass
  19. class FlipperApplication:
  20. @dataclass
  21. class ExternallyBuiltFile:
  22. path: str
  23. command: str
  24. @dataclass
  25. class Library:
  26. name: str
  27. fap_include_paths: List[str] = field(default_factory=lambda: ["."])
  28. sources: List[str] = field(default_factory=lambda: ["*.c*"])
  29. cflags: List[str] = field(default_factory=list)
  30. cdefines: List[str] = field(default_factory=list)
  31. cincludes: List[str] = field(default_factory=list)
  32. PRIVATE_FIELD_PREFIX = "_"
  33. appid: str
  34. apptype: FlipperAppType
  35. name: Optional[str] = ""
  36. entry_point: Optional[str] = None
  37. flags: List[str] = field(default_factory=lambda: ["Default"])
  38. cdefines: List[str] = field(default_factory=list)
  39. requires: List[str] = field(default_factory=list)
  40. conflicts: List[str] = field(default_factory=list)
  41. provides: List[str] = field(default_factory=list)
  42. stack_size: int = 2048
  43. icon: Optional[str] = None
  44. order: int = 0
  45. sdk_headers: List[str] = field(default_factory=list)
  46. targets: List[str] = field(default_factory=lambda: ["all"])
  47. # .fap-specific
  48. sources: List[str] = field(default_factory=lambda: ["*.c*"])
  49. fap_version: Tuple[int] = field(default_factory=lambda: (0, 1))
  50. fap_icon: Optional[str] = None
  51. fap_libs: List[str] = field(default_factory=list)
  52. fap_category: str = ""
  53. fap_description: str = ""
  54. fap_author: str = ""
  55. fap_weburl: str = ""
  56. fap_icon_assets: Optional[str] = None
  57. fap_extbuild: List[ExternallyBuiltFile] = field(default_factory=list)
  58. fap_private_libs: List[Library] = field(default_factory=list)
  59. # Internally used by fbt
  60. _appdir: Optional[object] = None
  61. _apppath: Optional[str] = None
  62. class AppManager:
  63. def __init__(self):
  64. self.known_apps = {}
  65. def get(self, appname: str):
  66. try:
  67. return self.known_apps[appname]
  68. except KeyError as _:
  69. raise FlipperManifestException(
  70. f"Missing application manifest for '{appname}'"
  71. )
  72. def find_by_appdir(self, appdir: str):
  73. for app in self.known_apps.values():
  74. if app._appdir.name == appdir:
  75. return app
  76. return None
  77. def load_manifest(self, app_manifest_path: str, app_dir_node: object):
  78. if not os.path.exists(app_manifest_path):
  79. raise FlipperManifestException(
  80. f"App manifest not found at path {app_manifest_path}"
  81. )
  82. # print("Loading", app_manifest_path)
  83. app_manifests = []
  84. def App(*args, **kw):
  85. nonlocal app_manifests
  86. app_manifests.append(
  87. FlipperApplication(
  88. *args,
  89. **kw,
  90. _appdir=app_dir_node,
  91. _apppath=os.path.dirname(app_manifest_path),
  92. ),
  93. )
  94. def ExtFile(*args, **kw):
  95. return FlipperApplication.ExternallyBuiltFile(*args, **kw)
  96. def Lib(*args, **kw):
  97. return FlipperApplication.Library(*args, **kw)
  98. try:
  99. with open(app_manifest_path, "rt") as manifest_file:
  100. exec(manifest_file.read())
  101. except Exception as e:
  102. raise FlipperManifestException(
  103. f"Failed parsing manifest '{app_manifest_path}' : {e}"
  104. )
  105. if len(app_manifests) == 0:
  106. raise FlipperManifestException(
  107. f"App manifest '{app_manifest_path}' is malformed"
  108. )
  109. # print("Built", app_manifests)
  110. for app in app_manifests:
  111. self._add_known_app(app)
  112. def _add_known_app(self, app: FlipperApplication):
  113. if self.known_apps.get(app.appid, None):
  114. raise FlipperManifestException(f"Duplicate app declaration: {app.appid}")
  115. self.known_apps[app.appid] = app
  116. def filter_apps(self, applist: List[str], hw_target: str):
  117. return AppBuildset(self, applist, hw_target)
  118. class AppBuilderException(Exception):
  119. pass
  120. class AppBuildset:
  121. BUILTIN_APP_TYPES = (
  122. FlipperAppType.SERVICE,
  123. FlipperAppType.SYSTEM,
  124. FlipperAppType.APP,
  125. FlipperAppType.PLUGIN,
  126. FlipperAppType.DEBUG,
  127. FlipperAppType.ARCHIVE,
  128. FlipperAppType.SETTINGS,
  129. FlipperAppType.STARTUP,
  130. )
  131. def __init__(self, appmgr: AppManager, appnames: List[str], hw_target: str):
  132. self.appmgr = appmgr
  133. self.appnames = set(appnames)
  134. self.hw_target = hw_target
  135. self._orig_appnames = appnames
  136. self._process_deps()
  137. self._filter_by_target()
  138. self._check_conflicts()
  139. self._check_unsatisfied() # unneeded?
  140. self.apps = sorted(
  141. list(map(self.appmgr.get, self.appnames)),
  142. key=lambda app: app.appid,
  143. )
  144. def _is_missing_dep(self, dep_name: str):
  145. return dep_name not in self.appnames
  146. def _filter_by_target(self):
  147. for appname in self.appnames.copy():
  148. app = self.appmgr.get(appname)
  149. # if app.apptype not in self.BUILTIN_APP_TYPES:
  150. if not any(map(lambda t: t in app.targets, ["all", self.hw_target])):
  151. print(
  152. f"Removing {appname} due to target mismatch (building for {self.hw_target}, app supports {app.targets}"
  153. )
  154. self.appnames.remove(appname)
  155. def _process_deps(self):
  156. while True:
  157. provided = []
  158. for app in self.appnames:
  159. # print(app)
  160. provided.extend(
  161. filter(
  162. self._is_missing_dep,
  163. self.appmgr.get(app).provides + self.appmgr.get(app).requires,
  164. )
  165. )
  166. # print("provides round", provided)
  167. if len(provided) == 0:
  168. break
  169. self.appnames.update(provided)
  170. def _check_conflicts(self):
  171. conflicts = []
  172. for app in self.appnames:
  173. # print(app)
  174. if conflict_app_name := list(
  175. filter(
  176. lambda dep_name: dep_name in self.appnames,
  177. self.appmgr.get(app).conflicts,
  178. )
  179. ):
  180. conflicts.append((app, conflict_app_name))
  181. if len(conflicts):
  182. raise AppBuilderException(
  183. f"App conflicts for {', '.join(f'{conflict_dep[0]}: {conflict_dep[1]}' for conflict_dep in conflicts)}"
  184. )
  185. def _check_unsatisfied(self):
  186. unsatisfied = []
  187. for app in self.appnames:
  188. if missing_dep := list(
  189. filter(self._is_missing_dep, self.appmgr.get(app).requires)
  190. ):
  191. unsatisfied.append((app, missing_dep))
  192. if len(unsatisfied):
  193. raise AppBuilderException(
  194. f"Unsatisfied dependencies for {', '.join(f'{missing_dep[0]}: {missing_dep[1]}' for missing_dep in unsatisfied)}"
  195. )
  196. def get_apps_cdefs(self):
  197. cdefs = set()
  198. for app in self.apps:
  199. cdefs.update(app.cdefines)
  200. return sorted(list(cdefs))
  201. def get_sdk_headers(self):
  202. sdk_headers = []
  203. for app in self.apps:
  204. sdk_headers.extend([app._appdir.File(header) for header in app.sdk_headers])
  205. return sdk_headers
  206. def get_apps_of_type(self, apptype: FlipperAppType, all_known: bool = False):
  207. return sorted(
  208. filter(
  209. lambda app: app.apptype == apptype,
  210. self.appmgr.known_apps.values() if all_known else self.apps,
  211. ),
  212. key=lambda app: app.order,
  213. )
  214. def get_builtin_apps(self):
  215. return list(
  216. filter(lambda app: app.apptype in self.BUILTIN_APP_TYPES, self.apps)
  217. )
  218. def get_builtin_app_folders(self):
  219. return sorted(
  220. set(
  221. (app._appdir, source_type)
  222. for app in self.get_builtin_apps()
  223. for source_type in app.sources
  224. )
  225. )
  226. class ApplicationsCGenerator:
  227. APP_TYPE_MAP = {
  228. FlipperAppType.SERVICE: ("FlipperApplication", "FLIPPER_SERVICES"),
  229. FlipperAppType.SYSTEM: ("FlipperApplication", "FLIPPER_SYSTEM_APPS"),
  230. FlipperAppType.APP: ("FlipperApplication", "FLIPPER_APPS"),
  231. FlipperAppType.PLUGIN: ("FlipperApplication", "FLIPPER_PLUGINS"),
  232. FlipperAppType.DEBUG: ("FlipperApplication", "FLIPPER_DEBUG_APPS"),
  233. FlipperAppType.SETTINGS: ("FlipperApplication", "FLIPPER_SETTINGS_APPS"),
  234. FlipperAppType.STARTUP: ("FlipperOnStartHook", "FLIPPER_ON_SYSTEM_START"),
  235. }
  236. def __init__(self, buildset: AppBuildset, autorun_app: str = ""):
  237. self.buildset = buildset
  238. self.autorun = autorun_app
  239. def get_app_ep_forward(self, app: FlipperApplication):
  240. if app.apptype == FlipperAppType.STARTUP:
  241. return f"extern void {app.entry_point}();"
  242. return f"extern int32_t {app.entry_point}(void* p);"
  243. def get_app_descr(self, app: FlipperApplication):
  244. if app.apptype == FlipperAppType.STARTUP:
  245. return app.entry_point
  246. return f"""
  247. {{.app = {app.entry_point},
  248. .name = "{app.name}",
  249. .stack_size = {app.stack_size},
  250. .icon = {f"&{app.icon}" if app.icon else "NULL"},
  251. .flags = {'|'.join(f"FlipperApplicationFlag{flag}" for flag in app.flags)} }}"""
  252. def generate(self):
  253. contents = [
  254. '#include "applications.h"',
  255. "#include <assets_icons.h>",
  256. f'const char* FLIPPER_AUTORUN_APP_NAME = "{self.autorun}";',
  257. ]
  258. for apptype in self.APP_TYPE_MAP:
  259. contents.extend(
  260. map(self.get_app_ep_forward, self.buildset.get_apps_of_type(apptype))
  261. )
  262. entry_type, entry_block = self.APP_TYPE_MAP[apptype]
  263. contents.append(f"const {entry_type} {entry_block}[] = {{")
  264. contents.append(
  265. ",\n".join(
  266. map(self.get_app_descr, self.buildset.get_apps_of_type(apptype))
  267. )
  268. )
  269. contents.append("};")
  270. contents.append(
  271. f"const size_t {entry_block}_COUNT = COUNT_OF({entry_block});"
  272. )
  273. archive_app = self.buildset.get_apps_of_type(FlipperAppType.ARCHIVE)
  274. if archive_app:
  275. contents.extend(
  276. [
  277. self.get_app_ep_forward(archive_app[0]),
  278. f"const FlipperApplication FLIPPER_ARCHIVE = {self.get_app_descr(archive_app[0])};",
  279. ]
  280. )
  281. return "\n".join(contents)