appmanifest.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. import os
  2. from dataclasses import dataclass, field
  3. from enum import Enum
  4. from typing import Callable, List, Optional, Tuple
  5. class FlipperManifestException(Exception):
  6. pass
  7. class FlipperAppType(Enum):
  8. SERVICE = "Service"
  9. SYSTEM = "System"
  10. APP = "App"
  11. DEBUG = "Debug"
  12. ARCHIVE = "Archive"
  13. SETTINGS = "Settings"
  14. STARTUP = "StartupHook"
  15. EXTERNAL = "External"
  16. METAPACKAGE = "Package"
  17. PLUGIN = "Plugin"
  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: str | Tuple[int] = "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_icon_assets_symbol: Optional[str] = None
  58. fap_extbuild: List[ExternallyBuiltFile] = field(default_factory=list)
  59. fap_private_libs: List[Library] = field(default_factory=list)
  60. fap_file_assets: Optional[str] = None
  61. # Internally used by fbt
  62. _appmanager: Optional["AppManager"] = None
  63. _appdir: Optional[object] = None
  64. _apppath: Optional[str] = None
  65. _plugins: List["FlipperApplication"] = field(default_factory=list)
  66. def supports_hardware_target(self, target: str):
  67. return target in self.targets or "all" in self.targets
  68. @property
  69. def is_default_deployable(self):
  70. return self.apptype != FlipperAppType.DEBUG and self.fap_category != "Examples"
  71. def __post_init__(self):
  72. if self.apptype == FlipperAppType.PLUGIN:
  73. self.stack_size = 0
  74. if isinstance(self.fap_version, str):
  75. try:
  76. self.fap_version = tuple(int(v) for v in self.fap_version.split("."))
  77. except ValueError:
  78. raise FlipperManifestException(
  79. f"Invalid version string '{self.fap_version}'. Must be in the form 'major.minor'"
  80. )
  81. class AppManager:
  82. def __init__(self):
  83. self.known_apps = {}
  84. def get(self, appname: str):
  85. try:
  86. return self.known_apps[appname]
  87. except KeyError:
  88. raise FlipperManifestException(
  89. f"Missing application manifest for '{appname}'"
  90. )
  91. def find_by_appdir(self, appdir: str):
  92. for app in self.known_apps.values():
  93. if app._appdir.name == appdir:
  94. return app
  95. return None
  96. def _validate_app_params(self, *args, **kw):
  97. apptype = kw.get("apptype")
  98. if apptype == FlipperAppType.PLUGIN:
  99. if kw.get("stack_size"):
  100. raise FlipperManifestException(
  101. f"Plugin {kw.get('appid')} cannot have stack (did you mean FlipperAppType.EXTERNAL?)"
  102. )
  103. if not kw.get("requires"):
  104. raise FlipperManifestException(
  105. f"Plugin {kw.get('appid')} must have 'requires' in manifest"
  106. )
  107. # Harmless - cdefines for external apps are meaningless
  108. # if apptype == FlipperAppType.EXTERNAL and kw.get("cdefines"):
  109. # raise FlipperManifestException(
  110. # f"External app {kw.get('appid')} must not have 'cdefines' in manifest"
  111. # )
  112. def load_manifest(self, app_manifest_path: str, app_dir_node: object):
  113. if not os.path.exists(app_manifest_path):
  114. raise FlipperManifestException(
  115. f"App manifest not found at path {app_manifest_path}"
  116. )
  117. # print("Loading", app_manifest_path)
  118. app_manifests = []
  119. def App(*args, **kw):
  120. nonlocal app_manifests
  121. self._validate_app_params(*args, **kw)
  122. app_manifests.append(
  123. FlipperApplication(
  124. *args,
  125. **kw,
  126. _appdir=app_dir_node,
  127. _apppath=os.path.dirname(app_manifest_path),
  128. _appmanager=self,
  129. ),
  130. )
  131. def ExtFile(*args, **kw):
  132. return FlipperApplication.ExternallyBuiltFile(*args, **kw)
  133. def Lib(*args, **kw):
  134. return FlipperApplication.Library(*args, **kw)
  135. try:
  136. with open(app_manifest_path, "rt") as manifest_file:
  137. exec(manifest_file.read())
  138. except Exception as e:
  139. raise FlipperManifestException(
  140. f"Failed parsing manifest '{app_manifest_path}' : {e}"
  141. )
  142. if len(app_manifests) == 0:
  143. raise FlipperManifestException(
  144. f"App manifest '{app_manifest_path}' is malformed"
  145. )
  146. # print("Built", app_manifests)
  147. for app in app_manifests:
  148. self._add_known_app(app)
  149. def _add_known_app(self, app: FlipperApplication):
  150. if self.known_apps.get(app.appid, None):
  151. raise FlipperManifestException(f"Duplicate app declaration: {app.appid}")
  152. self.known_apps[app.appid] = app
  153. def filter_apps(self, applist: List[str], hw_target: str):
  154. return AppBuildset(self, applist, hw_target)
  155. class AppBuilderException(Exception):
  156. pass
  157. class AppBuildset:
  158. BUILTIN_APP_TYPES = (
  159. FlipperAppType.SERVICE,
  160. FlipperAppType.SYSTEM,
  161. FlipperAppType.APP,
  162. FlipperAppType.DEBUG,
  163. FlipperAppType.ARCHIVE,
  164. FlipperAppType.SETTINGS,
  165. FlipperAppType.STARTUP,
  166. )
  167. @staticmethod
  168. def print_writer(message):
  169. print(message)
  170. def __init__(
  171. self,
  172. appmgr: AppManager,
  173. appnames: List[str],
  174. hw_target: str,
  175. message_writer: Callable = None,
  176. ):
  177. self.appmgr = appmgr
  178. self.appnames = set(appnames)
  179. self._orig_appnames = appnames
  180. self.hw_target = hw_target
  181. self._writer = message_writer if message_writer else self.print_writer
  182. self._process_deps()
  183. self._check_conflicts()
  184. self._check_unsatisfied() # unneeded?
  185. self._check_target_match()
  186. self._group_plugins()
  187. self.apps = sorted(
  188. list(map(self.appmgr.get, self.appnames)),
  189. key=lambda app: app.appid,
  190. )
  191. def _is_missing_dep(self, dep_name: str):
  192. return dep_name not in self.appnames
  193. def _check_if_app_target_supported(self, app_name: str):
  194. return self.appmgr.get(app_name).supports_hardware_target(self.hw_target)
  195. def _get_app_depends(self, app_name: str) -> List[str]:
  196. app_def = self.appmgr.get(app_name)
  197. # Skip app if its target is not supported by the target we are building for
  198. if not self._check_if_app_target_supported(app_name):
  199. self._writer(
  200. f"Skipping {app_name} due to target mismatch (building for {self.hw_target}, app supports {app_def.targets}"
  201. )
  202. return []
  203. return list(
  204. filter(
  205. self._check_if_app_target_supported,
  206. filter(self._is_missing_dep, app_def.provides + app_def.requires),
  207. )
  208. )
  209. def _process_deps(self):
  210. while True:
  211. provided = []
  212. for app_name in self.appnames:
  213. provided.extend(self._get_app_depends(app_name))
  214. # print("provides round: ", provided)
  215. if len(provided) == 0:
  216. break
  217. self.appnames.update(provided)
  218. def _check_conflicts(self):
  219. conflicts = []
  220. for app in self.appnames:
  221. if conflict_app_name := list(
  222. filter(
  223. lambda dep_name: dep_name in self.appnames,
  224. self.appmgr.get(app).conflicts,
  225. )
  226. ):
  227. conflicts.append((app, conflict_app_name))
  228. if len(conflicts):
  229. raise AppBuilderException(
  230. f"App conflicts for {', '.join(f'{conflict_dep[0]}: {conflict_dep[1]}' for conflict_dep in conflicts)}"
  231. )
  232. def _check_unsatisfied(self):
  233. unsatisfied = []
  234. for app in self.appnames:
  235. if missing_dep := list(
  236. filter(self._is_missing_dep, self.appmgr.get(app).requires)
  237. ):
  238. unsatisfied.append((app, missing_dep))
  239. if len(unsatisfied):
  240. raise AppBuilderException(
  241. f"Unsatisfied dependencies for {', '.join(f'{missing_dep[0]}: {missing_dep[1]}' for missing_dep in unsatisfied)}"
  242. )
  243. def _check_target_match(self):
  244. incompatible = []
  245. for app in self.appnames:
  246. if not self.appmgr.get(app).supports_hardware_target(self.hw_target):
  247. incompatible.append(app)
  248. if len(incompatible):
  249. raise AppBuilderException(
  250. f"Apps incompatible with target {self.hw_target}: {', '.join(incompatible)}"
  251. )
  252. def _group_plugins(self):
  253. known_extensions = self.get_apps_of_type(FlipperAppType.PLUGIN, all_known=True)
  254. for extension_app in known_extensions:
  255. for parent_app_id in extension_app.requires:
  256. try:
  257. parent_app = self.appmgr.get(parent_app_id)
  258. parent_app._plugins.append(extension_app)
  259. except FlipperManifestException:
  260. self._writer(
  261. f"Module {extension_app.appid} has unknown parent {parent_app_id}"
  262. )
  263. def get_apps_cdefs(self):
  264. cdefs = set()
  265. for app in self.apps:
  266. cdefs.update(app.cdefines)
  267. return sorted(list(cdefs))
  268. def get_sdk_headers(self):
  269. sdk_headers = []
  270. for app in self.apps:
  271. sdk_headers.extend([app._appdir.File(header) for header in app.sdk_headers])
  272. return sdk_headers
  273. def get_apps_of_type(self, apptype: FlipperAppType, all_known: bool = False):
  274. return sorted(
  275. filter(
  276. lambda app: app.apptype == apptype,
  277. self.appmgr.known_apps.values() if all_known else self.apps,
  278. ),
  279. key=lambda app: app.order,
  280. )
  281. def get_builtin_apps(self):
  282. return list(
  283. filter(lambda app: app.apptype in self.BUILTIN_APP_TYPES, self.apps)
  284. )
  285. def get_builtin_app_folders(self):
  286. return sorted(
  287. set(
  288. (app._appdir, source_type)
  289. for app in self.get_builtin_apps()
  290. for source_type in app.sources
  291. )
  292. )
  293. class ApplicationsCGenerator:
  294. APP_TYPE_MAP = {
  295. FlipperAppType.SERVICE: ("FlipperApplication", "FLIPPER_SERVICES"),
  296. FlipperAppType.SYSTEM: ("FlipperApplication", "FLIPPER_SYSTEM_APPS"),
  297. FlipperAppType.APP: ("FlipperApplication", "FLIPPER_APPS"),
  298. FlipperAppType.DEBUG: ("FlipperApplication", "FLIPPER_DEBUG_APPS"),
  299. FlipperAppType.SETTINGS: ("FlipperApplication", "FLIPPER_SETTINGS_APPS"),
  300. FlipperAppType.STARTUP: ("FlipperOnStartHook", "FLIPPER_ON_SYSTEM_START"),
  301. }
  302. def __init__(self, buildset: AppBuildset, autorun_app: str = ""):
  303. self.buildset = buildset
  304. self.autorun = autorun_app
  305. def get_app_ep_forward(self, app: FlipperApplication):
  306. if app.apptype == FlipperAppType.STARTUP:
  307. return f"extern void {app.entry_point}();"
  308. return f"extern int32_t {app.entry_point}(void* p);"
  309. def get_app_descr(self, app: FlipperApplication):
  310. if app.apptype == FlipperAppType.STARTUP:
  311. return app.entry_point
  312. return f"""
  313. {{.app = {app.entry_point},
  314. .name = "{app.name}",
  315. .appid = "{app.appid}",
  316. .stack_size = {app.stack_size},
  317. .icon = {f"&{app.icon}" if app.icon else "NULL"},
  318. .flags = {'|'.join(f"FlipperApplicationFlag{flag}" for flag in app.flags)} }}"""
  319. def generate(self):
  320. contents = [
  321. '#include "applications.h"',
  322. "#include <assets_icons.h>",
  323. f'const char* FLIPPER_AUTORUN_APP_NAME = "{self.autorun}";',
  324. ]
  325. for apptype in self.APP_TYPE_MAP:
  326. contents.extend(
  327. map(self.get_app_ep_forward, self.buildset.get_apps_of_type(apptype))
  328. )
  329. entry_type, entry_block = self.APP_TYPE_MAP[apptype]
  330. contents.append(f"const {entry_type} {entry_block}[] = {{")
  331. contents.append(
  332. ",\n".join(
  333. map(self.get_app_descr, self.buildset.get_apps_of_type(apptype))
  334. )
  335. )
  336. contents.append("};")
  337. contents.append(
  338. f"const size_t {entry_block}_COUNT = COUNT_OF({entry_block});"
  339. )
  340. archive_app = self.buildset.get_apps_of_type(FlipperAppType.ARCHIVE)
  341. if archive_app:
  342. contents.extend(
  343. [
  344. self.get_app_ep_forward(archive_app[0]),
  345. f"const FlipperApplication FLIPPER_ARCHIVE = {self.get_app_descr(archive_app[0])};",
  346. ]
  347. )
  348. return "\n".join(contents)