SConstruct 11 KB

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