SConstruct 11 KB

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