SConstruct 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. from SCons.Platform import TempFileMunge
  2. from SCons.Node import FS
  3. from SCons.Errors import UserError
  4. import os
  5. import multiprocessing
  6. import pathlib
  7. SetOption("num_jobs", multiprocessing.cpu_count())
  8. SetOption("max_drift", 1)
  9. # SetOption("silent", False)
  10. ufbt_state_dir = Dir(os.environ.get("UFBT_STATE_DIR", "#.ufbt"))
  11. ufbt_script_dir = Dir(os.environ.get("UFBT_SCRIPT_DIR"))
  12. ufbt_build_dir = ufbt_state_dir.Dir("build")
  13. ufbt_current_sdk_dir = ufbt_state_dir.Dir("current")
  14. SConsignFile(ufbt_state_dir.File(".sconsign.dblite").abspath)
  15. ufbt_variables = SConscript("commandline.scons")
  16. forward_os_env = {
  17. # Import PATH from OS env - scons doesn't do that by default
  18. "PATH": os.environ["PATH"],
  19. }
  20. # Proxying environment to child processes & scripts
  21. variables_to_forward = [
  22. # CI/CD variables
  23. "WORKFLOW_BRANCH_OR_TAG",
  24. "DIST_SUFFIX",
  25. # Python & other tools
  26. "HOME",
  27. "APPDATA",
  28. "PYTHONHOME",
  29. "PYTHONNOUSERSITE",
  30. "TMP",
  31. "TEMP",
  32. # Colors for tools
  33. "TERM",
  34. ]
  35. if proxy_env := GetOption("proxy_env"):
  36. variables_to_forward.extend(proxy_env.split(","))
  37. for env_value_name in variables_to_forward:
  38. if environ_value := os.environ.get(env_value_name, None):
  39. forward_os_env[env_value_name] = environ_value
  40. # Core environment init - loads SDK state, sets up paths, etc.
  41. core_env = Environment(
  42. variables=ufbt_variables,
  43. ENV=forward_os_env,
  44. UFBT_STATE_DIR=ufbt_state_dir,
  45. UFBT_CURRENT_SDK_DIR=ufbt_current_sdk_dir,
  46. UFBT_SCRIPT_DIR=ufbt_script_dir,
  47. toolpath=[ufbt_current_sdk_dir.Dir("scripts/ufbt/site_tools")],
  48. tools=[
  49. "ufbt_state",
  50. ("ufbt_help", {"vars": ufbt_variables}),
  51. ],
  52. )
  53. core_env.Append(CPPDEFINES=GetOption("extra_defines"))
  54. # Now we can import stuff bundled with SDK - it was added to sys.path by ufbt_state
  55. from fbt.util import (
  56. tempfile_arg_esc_func,
  57. single_quote,
  58. extract_abs_dir,
  59. extract_abs_dir_path,
  60. wrap_tempfile,
  61. path_as_posix,
  62. )
  63. from fbt.appmanifest import FlipperAppType
  64. from fbt.sdk.cache import SdkCache
  65. # Base environment with all tools loaded from SDK
  66. env = core_env.Clone(
  67. toolpath=[core_env["FBT_SCRIPT_DIR"].Dir("fbt_tools")],
  68. tools=[
  69. "fbt_tweaks",
  70. (
  71. "crosscc",
  72. {
  73. "toolchain_prefix": "arm-none-eabi-",
  74. "versions": (" 10.3",),
  75. },
  76. ),
  77. "fwbin",
  78. "python3",
  79. "sconsrecursiveglob",
  80. "sconsmodular",
  81. "ccache",
  82. "fbt_apps",
  83. "fbt_extapps",
  84. "fbt_assets",
  85. ("compilation_db", {"COMPILATIONDB_COMSTR": "\tCDB\t${TARGET}"}),
  86. ],
  87. FBT_FAP_DEBUG_ELF_ROOT=ufbt_build_dir,
  88. TEMPFILE=TempFileMunge,
  89. MAXLINELENGTH=2048,
  90. PROGSUFFIX=".elf",
  91. TEMPFILEARGESCFUNC=tempfile_arg_esc_func,
  92. SINGLEQUOTEFUNC=single_quote,
  93. ABSPATHGETTERFUNC=extract_abs_dir_path,
  94. APPS=[],
  95. UFBT_API_VERSION=SdkCache(
  96. core_env.subst("$SDK_DEFINITION"), load_version_only=True
  97. ).version,
  98. APPCHECK_COMSTR="\tAPPCHK\t${SOURCE}\n\t\tTarget: ${TARGET_HW}, API: ${UFBT_API_VERSION}",
  99. )
  100. wrap_tempfile(env, "LINKCOM")
  101. wrap_tempfile(env, "ARCOM")
  102. # print(env.Dump())
  103. # Dist env
  104. dist_env = env.Clone(
  105. tools=[
  106. "fbt_dist",
  107. "fbt_debugopts",
  108. "openocd",
  109. "blackmagic",
  110. "jflash",
  111. "textfile",
  112. ],
  113. ENV=os.environ,
  114. OPENOCD_OPTS=[
  115. "-f",
  116. "interface/stlink.cfg",
  117. "-c",
  118. "transport select hla_swd",
  119. "-f",
  120. "${FBT_DEBUG_DIR}/stm32wbx.cfg",
  121. "-c",
  122. "stm32wbx.cpu configure -rtos auto",
  123. ],
  124. )
  125. openocd_target = dist_env.OpenOCDFlash(
  126. dist_env["UFBT_STATE_DIR"].File("flash"),
  127. dist_env["FW_BIN"],
  128. OPENOCD_COMMAND=[
  129. "-c",
  130. "program ${SOURCE.posix} reset exit 0x08000000",
  131. ],
  132. )
  133. dist_env.Alias("firmware_flash", openocd_target)
  134. dist_env.Alias("flash", openocd_target)
  135. if env["FORCE"]:
  136. env.AlwaysBuild(openocd_target)
  137. firmware_jflash = dist_env.JFlash(
  138. dist_env["UFBT_STATE_DIR"].File("jflash"),
  139. dist_env["FW_BIN"],
  140. JFLASHADDR="0x20000000",
  141. )
  142. dist_env.Alias("firmware_jflash", firmware_jflash)
  143. dist_env.Alias("jflash", firmware_jflash)
  144. if env["FORCE"]:
  145. env.AlwaysBuild(firmware_jflash)
  146. firmware_debug = dist_env.PhonyTarget(
  147. "debug",
  148. "${GDBPYCOM}",
  149. source=dist_env["FW_ELF"],
  150. GDBOPTS="${GDBOPTS_BASE}",
  151. GDBREMOTE="${OPENOCD_GDB_PIPE}",
  152. FBT_FAP_DEBUG_ELF_ROOT=path_as_posix(dist_env.subst("$FBT_FAP_DEBUG_ELF_ROOT")),
  153. )
  154. dist_env.PhonyTarget(
  155. "blackmagic",
  156. "${GDBPYCOM}",
  157. source=dist_env["FW_ELF"],
  158. GDBOPTS="${GDBOPTS_BASE} ${GDBOPTS_BLACKMAGIC}",
  159. GDBREMOTE="${BLACKMAGIC_ADDR}",
  160. FBT_FAP_DEBUG_ELF_ROOT=path_as_posix(dist_env.subst("$FBT_FAP_DEBUG_ELF_ROOT")),
  161. )
  162. dist_env.PhonyTarget(
  163. "flash_blackmagic",
  164. "$GDB $GDBOPTS $SOURCES $GDBFLASH",
  165. source=dist_env["FW_ELF"],
  166. GDBOPTS="${GDBOPTS_BASE} ${GDBOPTS_BLACKMAGIC}",
  167. GDBREMOTE="${BLACKMAGIC_ADDR}",
  168. GDBFLASH=[
  169. "-ex",
  170. "load",
  171. "-ex",
  172. "quit",
  173. ],
  174. )
  175. flash_usb_full = dist_env.UsbInstall(
  176. dist_env["UFBT_STATE_DIR"].File("usbinstall"),
  177. [],
  178. )
  179. dist_env.AlwaysBuild(flash_usb_full)
  180. dist_env.Alias("flash_usb", flash_usb_full)
  181. dist_env.Alias("flash_usb_full", flash_usb_full)
  182. # App build environment
  183. appenv = env.Clone(
  184. CCCOM=env["CCCOM"].replace("$CFLAGS", "$CFLAGS_APP $CFLAGS"),
  185. CXXCOM=env["CXXCOM"].replace("$CXXFLAGS", "$CXXFLAGS_APP $CXXFLAGS"),
  186. LINKCOM=env["LINKCOM"].replace("$LINKFLAGS", "$LINKFLAGS_APP $LINKFLAGS"),
  187. COMPILATIONDB_USE_ABSPATH=True,
  188. )
  189. original_app_dir = Dir(appenv.subst("$UFBT_APP_DIR"))
  190. app_mount_point = Dir("#/app/")
  191. app_mount_point.addRepository(original_app_dir)
  192. appenv.LoadAppManifest(app_mount_point)
  193. appenv.PrepareApplicationsBuild()
  194. #######################
  195. apps_artifacts = appenv["EXT_APPS"]
  196. apps_to_build_as_faps = [
  197. FlipperAppType.PLUGIN,
  198. FlipperAppType.EXTERNAL,
  199. ]
  200. known_extapps = [
  201. app
  202. for apptype in apps_to_build_as_faps
  203. for app in appenv["APPBUILD"].get_apps_of_type(apptype, True)
  204. ]
  205. incompatible_apps = []
  206. for app in known_extapps:
  207. if not app.supports_hardware_target(appenv.subst("f${TARGET_HW}")):
  208. incompatible_apps.append(app)
  209. continue
  210. app_artifacts = appenv.BuildAppElf(app)
  211. app_src_dir = extract_abs_dir(app_artifacts.app._appdir)
  212. app_artifacts.installer = [
  213. appenv.Install(app_src_dir.Dir("dist"), app_artifacts.compact),
  214. appenv.Install(app_src_dir.Dir("dist").Dir("debug"), app_artifacts.debug),
  215. ]
  216. if len(incompatible_apps):
  217. print(
  218. "WARNING: The following apps are not compatible with the current target hardware and will not be built: {}".format(
  219. ", ".join([app.name for app in incompatible_apps])
  220. )
  221. )
  222. if appenv["FORCE"]:
  223. appenv.AlwaysBuild([extapp.compact for extapp in apps_artifacts.values()])
  224. # Final steps - target aliases
  225. install_and_check = [
  226. (extapp.installer, extapp.validator) for extapp in apps_artifacts.values()
  227. ]
  228. Alias(
  229. "faps",
  230. install_and_check,
  231. )
  232. Default(install_and_check)
  233. # Compilation database
  234. fwcdb = appenv.CompilationDatabase(
  235. original_app_dir.Dir(".vscode").File("compile_commands.json")
  236. )
  237. AlwaysBuild(fwcdb)
  238. Precious(fwcdb)
  239. NoClean(fwcdb)
  240. if len(apps_artifacts):
  241. Default(fwcdb)
  242. # launch handler
  243. runnable_apps = appenv["APPBUILD"].get_apps_of_type(FlipperAppType.EXTERNAL, True)
  244. app_to_launch = None
  245. if len(runnable_apps) == 1:
  246. app_to_launch = runnable_apps[0].appid
  247. elif len(runnable_apps) > 1:
  248. # more than 1 app - try to find one with matching id
  249. app_to_launch = appenv.subst("$APPID")
  250. def ambiguous_app_call(**kw):
  251. raise UserError(
  252. f"More than one app is runnable: {', '.join(app.appid for app in runnable_apps)}. Please specify an app with APPID=..."
  253. )
  254. if app_to_launch:
  255. appenv.AddAppLaunchTarget(app_to_launch, "launch")
  256. else:
  257. dist_env.PhonyTarget("launch", Action(ambiguous_app_call, None))
  258. # cli handler
  259. appenv.PhonyTarget(
  260. "cli",
  261. '${PYTHON3} "${FBT_SCRIPT_DIR}/serial_cli.py"',
  262. )
  263. # Linter
  264. dist_env.PhonyTarget(
  265. "lint",
  266. "${PYTHON3} ${FBT_SCRIPT_DIR}/lint.py check ${LINT_SOURCES}",
  267. source=original_app_dir.File(".clang-format"),
  268. LINT_SOURCES=[original_app_dir],
  269. )
  270. dist_env.PhonyTarget(
  271. "format",
  272. "${PYTHON3} ${FBT_SCRIPT_DIR}/lint.py format ${LINT_SOURCES}",
  273. source=original_app_dir.File(".clang-format"),
  274. LINT_SOURCES=[original_app_dir],
  275. )
  276. # Prepare vscode environment
  277. def _path_as_posix(path):
  278. return pathlib.Path(path).as_posix()
  279. vscode_dist = []
  280. project_template_dir = dist_env["UFBT_SCRIPT_ROOT"].Dir("project_template")
  281. for template_file in project_template_dir.Dir(".vscode").glob("*"):
  282. vscode_dist.append(
  283. dist_env.Substfile(
  284. original_app_dir.Dir(".vscode").File(template_file.name),
  285. template_file,
  286. SUBST_DICT={
  287. "@UFBT_VSCODE_PATH_SEP@": os.path.pathsep,
  288. "@UFBT_TOOLCHAIN_ARM_TOOLCHAIN_DIR@": pathlib.Path(
  289. dist_env.WhereIs("arm-none-eabi-gcc")
  290. ).parent.as_posix(),
  291. "@UFBT_TOOLCHAIN_GCC@": _path_as_posix(
  292. dist_env.WhereIs("arm-none-eabi-gcc")
  293. ),
  294. "@UFBT_TOOLCHAIN_GDB_PY@": _path_as_posix(
  295. dist_env.WhereIs("arm-none-eabi-gdb-py")
  296. ),
  297. "@UFBT_TOOLCHAIN_OPENOCD@": _path_as_posix(dist_env.WhereIs("openocd")),
  298. "@UFBT_APP_DIR@": _path_as_posix(original_app_dir.abspath),
  299. "@UFBT_ROOT_DIR@": _path_as_posix(Dir("#").abspath),
  300. "@UFBT_DEBUG_DIR@": dist_env["FBT_DEBUG_DIR"],
  301. "@UFBT_DEBUG_ELF_DIR@": _path_as_posix(
  302. dist_env["FBT_FAP_DEBUG_ELF_ROOT"].abspath
  303. ),
  304. "@UFBT_FIRMWARE_ELF@": _path_as_posix(dist_env["FW_ELF"].abspath),
  305. },
  306. )
  307. )
  308. for config_file in project_template_dir.glob(".*"):
  309. if isinstance(config_file, FS.Dir):
  310. continue
  311. vscode_dist.append(dist_env.Install(original_app_dir, config_file))
  312. dist_env.Precious(vscode_dist)
  313. dist_env.NoClean(vscode_dist)
  314. dist_env.Alias("vscode_dist", vscode_dist)
  315. # Creating app from base template
  316. dist_env.SetDefault(FBT_APPID=appenv.subst("$APPID") or "template")
  317. app_template_dist = []
  318. for template_file in project_template_dir.Dir("app_template").glob("*"):
  319. dist_file_name = dist_env.subst(template_file.name)
  320. if template_file.name.endswith(".png"):
  321. app_template_dist.append(
  322. dist_env.InstallAs(original_app_dir.File(dist_file_name), template_file)
  323. )
  324. else:
  325. app_template_dist.append(
  326. dist_env.Substfile(
  327. original_app_dir.File(dist_file_name),
  328. template_file,
  329. SUBST_DICT={
  330. "@FBT_APPID@": dist_env.subst("$FBT_APPID"),
  331. },
  332. )
  333. )
  334. AddPostAction(
  335. app_template_dist[-1],
  336. [
  337. Mkdir(original_app_dir.Dir("images")),
  338. Touch(original_app_dir.Dir("images").File(".gitkeep")),
  339. ],
  340. )
  341. dist_env.Precious(app_template_dist)
  342. dist_env.NoClean(app_template_dist)
  343. dist_env.Alias("create", app_template_dist)
  344. dist_env.PhonyTarget(
  345. "get_blackmagic",
  346. "@echo $( ${BLACKMAGIC_ADDR} $)",
  347. )
  348. dist_env.PhonyTarget(
  349. "get_apiversion",
  350. "@echo $( ${UFBT_API_VERSION} $)",
  351. )
  352. # Dolphin animation builder. Expects "external" directory in current dir
  353. # with animation sources & manifests. Builds & uploads them to connected Flipper
  354. dolphin_src_dir = original_app_dir.Dir("external")
  355. if dolphin_src_dir.exists():
  356. dolphin_dir = ufbt_build_dir.Dir("dolphin")
  357. dolphin_external = dist_env.DolphinExtBuilder(
  358. ufbt_build_dir.Dir("dolphin"),
  359. original_app_dir,
  360. DOLPHIN_RES_TYPE="external",
  361. )
  362. dist_env.PhonyTarget(
  363. "dolphin_ext",
  364. '${PYTHON3} ${FBT_SCRIPT_DIR}/storage.py send "${SOURCE}" /ext/dolphin',
  365. source=ufbt_build_dir.Dir("dolphin"),
  366. )
  367. else:
  368. def missing_dolphin_folder(**kw):
  369. raise UserError(f"Dolphin folder not found: {dolphin_src_dir}")
  370. dist_env.PhonyTarget("dolphin_ext", Action(missing_dolphin_folder, None))