SConstruct 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  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. # Debug alien elf
  163. debug_other_opts = [
  164. "-ex",
  165. "source ${FBT_DEBUG_DIR}/PyCortexMDebug/PyCortexMDebug.py",
  166. "-ex",
  167. "source ${FBT_DEBUG_DIR}/flipperversion.py",
  168. "-ex",
  169. "fw-version",
  170. ]
  171. dist_env.PhonyTarget(
  172. "debug_other",
  173. "${GDBPYCOM}",
  174. GDBOPTS="${GDBOPTS_BASE}",
  175. GDBREMOTE="${OPENOCD_GDB_PIPE}",
  176. GDBPYOPTS=debug_other_opts,
  177. )
  178. dist_env.PhonyTarget(
  179. "debug_other_blackmagic",
  180. "${GDBPYCOM}",
  181. GDBOPTS="${GDBOPTS_BASE} ${GDBOPTS_BLACKMAGIC}",
  182. GDBREMOTE="${BLACKMAGIC_ADDR}",
  183. GDBPYOPTS=debug_other_opts,
  184. )
  185. dist_env.PhonyTarget(
  186. "flash_blackmagic",
  187. "$GDB $GDBOPTS $SOURCES $GDBFLASH",
  188. source=dist_env["FW_ELF"],
  189. GDBOPTS="${GDBOPTS_BASE} ${GDBOPTS_BLACKMAGIC}",
  190. GDBREMOTE="${BLACKMAGIC_ADDR}",
  191. GDBFLASH=[
  192. "-ex",
  193. "load",
  194. "-ex",
  195. "quit",
  196. ],
  197. )
  198. flash_usb_full = dist_env.UsbInstall(
  199. dist_env["UFBT_STATE_DIR"].File("usbinstall"),
  200. [],
  201. )
  202. dist_env.AlwaysBuild(flash_usb_full)
  203. dist_env.Alias("flash_usb", flash_usb_full)
  204. dist_env.Alias("flash_usb_full", flash_usb_full)
  205. # App build environment
  206. appenv = env.Clone(
  207. CCCOM=env["CCCOM"].replace("$CFLAGS", "$CFLAGS_APP $CFLAGS"),
  208. CXXCOM=env["CXXCOM"].replace("$CXXFLAGS", "$CXXFLAGS_APP $CXXFLAGS"),
  209. LINKCOM=env["LINKCOM"].replace("$LINKFLAGS", "$LINKFLAGS_APP $LINKFLAGS"),
  210. COMPILATIONDB_USE_ABSPATH=True,
  211. )
  212. original_app_dir = Dir(appenv.subst("$UFBT_APP_DIR"))
  213. app_mount_point = Dir("#/app/")
  214. app_mount_point.addRepository(original_app_dir)
  215. appenv.LoadAppManifest(app_mount_point)
  216. appenv.PrepareApplicationsBuild()
  217. #######################
  218. apps_artifacts = appenv["EXT_APPS"]
  219. apps_to_build_as_faps = [
  220. FlipperAppType.PLUGIN,
  221. FlipperAppType.EXTERNAL,
  222. ]
  223. known_extapps = [
  224. app
  225. for apptype in apps_to_build_as_faps
  226. for app in appenv["APPBUILD"].get_apps_of_type(apptype, True)
  227. ]
  228. incompatible_apps = []
  229. for app in known_extapps:
  230. if not app.supports_hardware_target(appenv.subst("f${TARGET_HW}")):
  231. incompatible_apps.append(app)
  232. continue
  233. app_artifacts = appenv.BuildAppElf(app)
  234. app_src_dir = extract_abs_dir(app_artifacts.app._appdir)
  235. app_artifacts.installer = [
  236. appenv.Install(app_src_dir.Dir("dist"), app_artifacts.compact),
  237. appenv.Install(app_src_dir.Dir("dist").Dir("debug"), app_artifacts.debug),
  238. ]
  239. if len(incompatible_apps):
  240. print(
  241. "WARNING: The following apps are not compatible with the current target hardware and will not be built: {}".format(
  242. ", ".join([app.name for app in incompatible_apps])
  243. )
  244. )
  245. if appenv["FORCE"]:
  246. appenv.AlwaysBuild([extapp.compact for extapp in apps_artifacts.values()])
  247. # Final steps - target aliases
  248. install_and_check = [
  249. (extapp.installer, extapp.validator) for extapp in apps_artifacts.values()
  250. ]
  251. Alias(
  252. "faps",
  253. install_and_check,
  254. )
  255. Default(install_and_check)
  256. # Compilation database
  257. fwcdb = appenv.CompilationDatabase(
  258. original_app_dir.Dir(".vscode").File("compile_commands.json")
  259. )
  260. AlwaysBuild(fwcdb)
  261. Precious(fwcdb)
  262. NoClean(fwcdb)
  263. if len(apps_artifacts):
  264. Default(fwcdb)
  265. # launch handler
  266. runnable_apps = appenv["APPBUILD"].get_apps_of_type(FlipperAppType.EXTERNAL, True)
  267. app_to_launch = None
  268. if len(runnable_apps) == 1:
  269. app_to_launch = runnable_apps[0].appid
  270. elif len(runnable_apps) > 1:
  271. # more than 1 app - try to find one with matching id
  272. app_to_launch = appenv.subst("$APPID")
  273. def ambiguous_app_call(**kw):
  274. raise UserError(
  275. f"More than one app is runnable: {', '.join(app.appid for app in runnable_apps)}. Please specify an app with APPID=..."
  276. )
  277. if app_to_launch:
  278. appenv.AddAppLaunchTarget(app_to_launch, "launch")
  279. else:
  280. dist_env.PhonyTarget("launch", Action(ambiguous_app_call, None))
  281. # cli handler
  282. appenv.PhonyTarget(
  283. "cli",
  284. '${PYTHON3} "${FBT_SCRIPT_DIR}/serial_cli.py"',
  285. )
  286. # Linter
  287. dist_env.PhonyTarget(
  288. "lint",
  289. "${PYTHON3} ${FBT_SCRIPT_DIR}/lint.py check ${LINT_SOURCES}",
  290. source=original_app_dir.File(".clang-format"),
  291. LINT_SOURCES=[original_app_dir],
  292. )
  293. dist_env.PhonyTarget(
  294. "format",
  295. "${PYTHON3} ${FBT_SCRIPT_DIR}/lint.py format ${LINT_SOURCES}",
  296. source=original_app_dir.File(".clang-format"),
  297. LINT_SOURCES=[original_app_dir],
  298. )
  299. # Prepare vscode environment
  300. def _path_as_posix(path):
  301. return pathlib.Path(path).as_posix()
  302. vscode_dist = []
  303. project_template_dir = dist_env["UFBT_SCRIPT_ROOT"].Dir("project_template")
  304. for template_file in project_template_dir.Dir(".vscode").glob("*"):
  305. vscode_dist.append(
  306. dist_env.Substfile(
  307. original_app_dir.Dir(".vscode").File(template_file.name),
  308. template_file,
  309. SUBST_DICT={
  310. "@UFBT_VSCODE_PATH_SEP@": os.path.pathsep,
  311. "@UFBT_TOOLCHAIN_ARM_TOOLCHAIN_DIR@": pathlib.Path(
  312. dist_env.WhereIs("arm-none-eabi-gcc")
  313. ).parent.as_posix(),
  314. "@UFBT_TOOLCHAIN_GCC@": _path_as_posix(
  315. dist_env.WhereIs("arm-none-eabi-gcc")
  316. ),
  317. "@UFBT_TOOLCHAIN_GDB_PY@": _path_as_posix(
  318. dist_env.WhereIs("arm-none-eabi-gdb-py")
  319. ),
  320. "@UFBT_TOOLCHAIN_OPENOCD@": _path_as_posix(dist_env.WhereIs("openocd")),
  321. "@UFBT_APP_DIR@": _path_as_posix(original_app_dir.abspath),
  322. "@UFBT_ROOT_DIR@": _path_as_posix(Dir("#").abspath),
  323. "@UFBT_DEBUG_DIR@": dist_env["FBT_DEBUG_DIR"],
  324. "@UFBT_DEBUG_ELF_DIR@": _path_as_posix(
  325. dist_env["FBT_FAP_DEBUG_ELF_ROOT"].abspath
  326. ),
  327. "@UFBT_FIRMWARE_ELF@": _path_as_posix(dist_env["FW_ELF"].abspath),
  328. },
  329. )
  330. )
  331. for config_file in project_template_dir.glob(".*"):
  332. if isinstance(config_file, FS.Dir):
  333. continue
  334. vscode_dist.append(dist_env.Install(original_app_dir, config_file))
  335. dist_env.Precious(vscode_dist)
  336. dist_env.NoClean(vscode_dist)
  337. dist_env.Alias("vscode_dist", vscode_dist)
  338. # Creating app from base template
  339. dist_env.SetDefault(FBT_APPID=appenv.subst("$APPID") or "template")
  340. app_template_dir = project_template_dir.Dir("app_template")
  341. app_template_dist = []
  342. for template_file in app_template_dir.glob("*"):
  343. dist_file_name = dist_env.subst(template_file.name)
  344. if template_file.name.endswith(".png"):
  345. app_template_dist.append(
  346. dist_env.InstallAs(original_app_dir.File(dist_file_name), template_file)
  347. )
  348. else:
  349. app_template_dist.append(
  350. dist_env.Substfile(
  351. original_app_dir.File(dist_file_name),
  352. template_file,
  353. SUBST_DICT={
  354. "@FBT_APPID@": dist_env.subst("$FBT_APPID"),
  355. },
  356. )
  357. )
  358. AddPostAction(
  359. app_template_dist[-1],
  360. [
  361. Mkdir(original_app_dir.Dir("images")),
  362. Touch(original_app_dir.Dir("images").File(".gitkeep")),
  363. # scons' glob ignores .dot directories, so we need to copy .github manually
  364. Copy(original_app_dir.Dir(".github"), app_template_dir.Dir(".github")),
  365. ],
  366. )
  367. dist_env.Precious(app_template_dist)
  368. dist_env.NoClean(app_template_dist)
  369. dist_env.Alias("create", app_template_dist)
  370. dist_env.PhonyTarget(
  371. "get_blackmagic",
  372. "@echo $( ${BLACKMAGIC_ADDR} $)",
  373. )
  374. dist_env.PhonyTarget(
  375. "get_apiversion",
  376. "@echo $( ${UFBT_API_VERSION} $)",
  377. )
  378. # Dolphin animation builder. Expects "external" directory in current dir
  379. # with animation sources & manifests. Builds & uploads them to connected Flipper
  380. dolphin_src_dir = original_app_dir.Dir("external")
  381. if dolphin_src_dir.exists():
  382. dolphin_dir = ufbt_build_dir.Dir("dolphin")
  383. dolphin_external = dist_env.DolphinExtBuilder(
  384. ufbt_build_dir.Dir("dolphin"),
  385. original_app_dir,
  386. DOLPHIN_RES_TYPE="external",
  387. )
  388. dist_env.PhonyTarget(
  389. "dolphin_ext",
  390. '${PYTHON3} ${FBT_SCRIPT_DIR}/storage.py send "${SOURCE}" /ext/dolphin',
  391. source=ufbt_build_dir.Dir("dolphin"),
  392. )
  393. else:
  394. def missing_dolphin_folder(**kw):
  395. raise UserError(f"Dolphin folder not found: {dolphin_src_dir}")
  396. dist_env.PhonyTarget("dolphin_ext", Action(missing_dolphin_folder, None))
  397. dist_env.PhonyTarget(
  398. "env",
  399. "@echo $( ${FBT_SCRIPT_DIR}/toolchain/fbtenv.sh $)",
  400. )