appmanifest.py 9.4 KB

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