appmanifest.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. from dataclasses import dataclass, field
  2. from typing import List, Optional
  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] = None
  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. _appdir: Optional[str] = None
  33. class AppManager:
  34. def __init__(self):
  35. self.known_apps = {}
  36. def get(self, appname: str):
  37. try:
  38. return self.known_apps[appname]
  39. except KeyError as _:
  40. raise FlipperManifestException(
  41. f"Missing application manifest for '{appname}'"
  42. )
  43. def load_manifest(self, app_manifest_path: str, app_dir_name: str):
  44. if not os.path.exists(app_manifest_path):
  45. raise FlipperManifestException(
  46. f"App manifest not found at path {app_manifest_path}"
  47. )
  48. # print("Loading", app_manifest_path)
  49. app_manifests = []
  50. def App(*args, **kw):
  51. nonlocal app_manifests
  52. app_manifests.append(FlipperApplication(*args, **kw, _appdir=app_dir_name))
  53. with open(app_manifest_path, "rt") as manifest_file:
  54. exec(manifest_file.read())
  55. if len(app_manifests) == 0:
  56. raise FlipperManifestException(
  57. f"App manifest '{app_manifest_path}' is malformed"
  58. )
  59. # print("Built", app_manifests)
  60. for app in app_manifests:
  61. self._add_known_app(app)
  62. def _add_known_app(self, app: FlipperApplication):
  63. if self.known_apps.get(app.appid, None):
  64. raise FlipperManifestException(f"Duplicate app declaration: {app.appid}")
  65. self.known_apps[app.appid] = app
  66. def filter_apps(self, applist: List[str]):
  67. return AppBuildset(self, applist)
  68. class AppBuilderException(Exception):
  69. pass
  70. class AppBuildset:
  71. BUILTIN_APP_TYPES = (
  72. FlipperAppType.SERVICE,
  73. FlipperAppType.SYSTEM,
  74. FlipperAppType.APP,
  75. FlipperAppType.PLUGIN,
  76. FlipperAppType.DEBUG,
  77. FlipperAppType.ARCHIVE,
  78. FlipperAppType.SETTINGS,
  79. FlipperAppType.STARTUP,
  80. )
  81. def __init__(self, appmgr: AppManager, appnames: List[str]):
  82. self.appmgr = appmgr
  83. self.appnames = set(appnames)
  84. self._orig_appnames = appnames
  85. self._process_deps()
  86. self._check_conflicts()
  87. self._check_unsatisfied() # unneeded?
  88. self.apps = sorted(
  89. list(map(self.appmgr.get, self.appnames)),
  90. key=lambda app: app.appid,
  91. )
  92. def _is_missing_dep(self, dep_name: str):
  93. return dep_name not in self.appnames
  94. def _process_deps(self):
  95. while True:
  96. provided = []
  97. for app in self.appnames:
  98. # print(app)
  99. provided.extend(
  100. filter(
  101. self._is_missing_dep,
  102. self.appmgr.get(app).provides + self.appmgr.get(app).requires,
  103. )
  104. )
  105. # print("provides round", provided)
  106. if len(provided) == 0:
  107. break
  108. self.appnames.update(provided)
  109. def _check_conflicts(self):
  110. conflicts = []
  111. for app in self.appnames:
  112. # print(app)
  113. if conflict_app_name := list(
  114. filter(
  115. lambda dep_name: dep_name in self.appnames,
  116. self.appmgr.get(app).conflicts,
  117. )
  118. ):
  119. conflicts.append((app, conflict_app_name))
  120. if len(conflicts):
  121. raise AppBuilderException(
  122. f"App conflicts for {', '.join(f'{conflict_dep[0]}: {conflict_dep[1]}' for conflict_dep in conflicts)}"
  123. )
  124. def _check_unsatisfied(self):
  125. unsatisfied = []
  126. for app in self.appnames:
  127. if missing_dep := list(
  128. filter(self._is_missing_dep, self.appmgr.get(app).requires)
  129. ):
  130. unsatisfied.append((app, missing_dep))
  131. if len(unsatisfied):
  132. raise AppBuilderException(
  133. f"Unsatisfied dependencies for {', '.join(f'{missing_dep[0]}: {missing_dep[1]}' for missing_dep in unsatisfied)}"
  134. )
  135. def get_apps_cdefs(self):
  136. cdefs = set()
  137. for app in self.apps:
  138. cdefs.update(app.cdefines)
  139. return sorted(list(cdefs))
  140. def get_apps_of_type(self, apptype: FlipperAppType):
  141. return sorted(
  142. filter(lambda app: app.apptype == apptype, self.apps),
  143. key=lambda app: app.order,
  144. )
  145. def get_builtin_app_folders(self):
  146. return sorted(
  147. set(
  148. app._appdir
  149. for app in filter(
  150. lambda app: app.apptype in self.BUILTIN_APP_TYPES, self.apps
  151. )
  152. )
  153. )
  154. class ApplicationsCGenerator:
  155. APP_TYPE_MAP = {
  156. FlipperAppType.SERVICE: ("FlipperApplication", "FLIPPER_SERVICES"),
  157. FlipperAppType.SYSTEM: ("FlipperApplication", "FLIPPER_SYSTEM_APPS"),
  158. FlipperAppType.APP: ("FlipperApplication", "FLIPPER_APPS"),
  159. FlipperAppType.PLUGIN: ("FlipperApplication", "FLIPPER_PLUGINS"),
  160. FlipperAppType.DEBUG: ("FlipperApplication", "FLIPPER_DEBUG_APPS"),
  161. FlipperAppType.SETTINGS: ("FlipperApplication", "FLIPPER_SETTINGS_APPS"),
  162. FlipperAppType.STARTUP: ("FlipperOnStartHook", "FLIPPER_ON_SYSTEM_START"),
  163. }
  164. def __init__(self, buildset: AppBuildset):
  165. self.buildset = buildset
  166. def get_app_ep_forward(self, app: FlipperApplication):
  167. if app.apptype == FlipperAppType.STARTUP:
  168. return f"extern void {app.entry_point}();"
  169. return f"extern int32_t {app.entry_point}(void* p);"
  170. def get_app_descr(self, app: FlipperApplication):
  171. if app.apptype == FlipperAppType.STARTUP:
  172. return app.entry_point
  173. return f"""
  174. {{.app = {app.entry_point},
  175. .name = "{app.name}",
  176. .stack_size = {app.stack_size},
  177. .icon = {f"&{app.icon}" if app.icon else "NULL"},
  178. .flags = {'|'.join(f"FlipperApplicationFlag{flag}" for flag in app.flags)} }}"""
  179. def generate(self):
  180. contents = ['#include "applications.h"', "#include <assets_icons.h>"]
  181. for apptype in self.APP_TYPE_MAP:
  182. contents.extend(
  183. map(self.get_app_ep_forward, self.buildset.get_apps_of_type(apptype))
  184. )
  185. entry_type, entry_block = self.APP_TYPE_MAP[apptype]
  186. contents.append(f"const {entry_type} {entry_block}[] = {{")
  187. contents.append(
  188. ",\n".join(
  189. map(self.get_app_descr, self.buildset.get_apps_of_type(apptype))
  190. )
  191. )
  192. contents.append("};")
  193. contents.append(
  194. f"const size_t {entry_block}_COUNT = COUNT_OF({entry_block});"
  195. )
  196. archive_app = self.buildset.get_apps_of_type(FlipperAppType.ARCHIVE)
  197. if archive_app:
  198. contents.extend(
  199. [
  200. self.get_app_ep_forward(archive_app[0]),
  201. f"const FlipperApplication FLIPPER_ARCHIVE = {self.get_app_descr(archive_app[0])};",
  202. ]
  203. )
  204. return "\n".join(contents)