SConstruct 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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_debug = dist_env.PhonyTarget(
  145. "debug",
  146. "${GDBPYCOM}",
  147. source=dist_env["FW_ELF"],
  148. GDBOPTS="${GDBOPTS_BASE}",
  149. GDBREMOTE="${OPENOCD_GDB_PIPE}",
  150. FBT_FAP_DEBUG_ELF_ROOT=path_as_posix(dist_env.subst("$FBT_FAP_DEBUG_ELF_ROOT")),
  151. )
  152. dist_env.PhonyTarget(
  153. "blackmagic",
  154. "${GDBPYCOM}",
  155. source=dist_env["FW_ELF"],
  156. GDBOPTS="${GDBOPTS_BASE} ${GDBOPTS_BLACKMAGIC}",
  157. GDBREMOTE="${BLACKMAGIC_ADDR}",
  158. FBT_FAP_DEBUG_ELF_ROOT=path_as_posix(dist_env.subst("$FBT_FAP_DEBUG_ELF_ROOT")),
  159. )
  160. dist_env.PhonyTarget(
  161. "flash_blackmagic",
  162. "$GDB $GDBOPTS $SOURCES $GDBFLASH",
  163. source=dist_env["FW_ELF"],
  164. GDBOPTS="${GDBOPTS_BASE} ${GDBOPTS_BLACKMAGIC}",
  165. GDBREMOTE="${BLACKMAGIC_ADDR}",
  166. GDBFLASH=[
  167. "-ex",
  168. "load",
  169. "-ex",
  170. "quit",
  171. ],
  172. )
  173. flash_usb_full = dist_env.UsbInstall(
  174. dist_env["UFBT_STATE_DIR"].File("usbinstall"),
  175. [],
  176. )
  177. dist_env.AlwaysBuild(flash_usb_full)
  178. dist_env.Alias("flash_usb", flash_usb_full)
  179. dist_env.Alias("flash_usb_full", flash_usb_full)
  180. # App build environment
  181. appenv = env.Clone(
  182. CCCOM=env["CCCOM"].replace("$CFLAGS", "$CFLAGS_APP $CFLAGS"),
  183. CXXCOM=env["CXXCOM"].replace("$CXXFLAGS", "$CXXFLAGS_APP $CXXFLAGS"),
  184. LINKCOM=env["LINKCOM"].replace("$LINKFLAGS", "$LINKFLAGS_APP $LINKFLAGS"),
  185. COMPILATIONDB_USE_ABSPATH=True,
  186. )
  187. original_app_dir = Dir(appenv.subst("$UFBT_APP_DIR"))
  188. app_mount_point = Dir("#/app/")
  189. app_mount_point.addRepository(original_app_dir)
  190. appenv.LoadAppManifest(app_mount_point)
  191. appenv.PrepareApplicationsBuild()
  192. #######################
  193. apps_artifacts = appenv["EXT_APPS"]
  194. apps_to_build_as_faps = [
  195. FlipperAppType.PLUGIN,
  196. FlipperAppType.EXTERNAL,
  197. ]
  198. known_extapps = [
  199. app
  200. for apptype in apps_to_build_as_faps
  201. for app in appenv["APPBUILD"].get_apps_of_type(apptype, True)
  202. ]
  203. for app in known_extapps:
  204. app_artifacts = appenv.BuildAppElf(app)
  205. app_src_dir = extract_abs_dir(app_artifacts.app._appdir)
  206. app_artifacts.installer = [
  207. appenv.Install(app_src_dir.Dir("dist"), app_artifacts.compact),
  208. appenv.Install(app_src_dir.Dir("dist").Dir("debug"), app_artifacts.debug),
  209. ]
  210. if appenv["FORCE"]:
  211. appenv.AlwaysBuild([extapp.compact for extapp in apps_artifacts.values()])
  212. # Final steps - target aliases
  213. install_and_check = [
  214. (extapp.installer, extapp.validator) for extapp in apps_artifacts.values()
  215. ]
  216. Alias(
  217. "faps",
  218. install_and_check,
  219. )
  220. Default(install_and_check)
  221. # Compilation database
  222. fwcdb = appenv.CompilationDatabase(
  223. original_app_dir.Dir(".vscode").File("compile_commands.json")
  224. )
  225. AlwaysBuild(fwcdb)
  226. Precious(fwcdb)
  227. NoClean(fwcdb)
  228. if len(apps_artifacts):
  229. Default(fwcdb)
  230. # launch handler
  231. runnable_apps = appenv["APPBUILD"].get_apps_of_type(FlipperAppType.EXTERNAL, True)
  232. app_to_launch = None
  233. if len(runnable_apps) == 1:
  234. app_to_launch = runnable_apps[0].appid
  235. elif len(runnable_apps) > 1:
  236. # more than 1 app - try to find one with matching id
  237. app_to_launch = appenv.subst("$APPID")
  238. def ambiguous_app_call(**kw):
  239. raise UserError(
  240. f"More than one app is runnable: {', '.join(app.appid for app in runnable_apps)}. Please specify an app with APPID=..."
  241. )
  242. if app_to_launch:
  243. appenv.AddAppLaunchTarget(app_to_launch, "launch")
  244. else:
  245. dist_env.PhonyTarget("launch", Action(ambiguous_app_call, None))
  246. # cli handler
  247. appenv.PhonyTarget(
  248. "cli",
  249. '${PYTHON3} "${FBT_SCRIPT_DIR}/serial_cli.py"',
  250. )
  251. # Linter
  252. dist_env.PhonyTarget(
  253. "lint",
  254. "${PYTHON3} ${FBT_SCRIPT_DIR}/lint.py check ${LINT_SOURCES}",
  255. source=original_app_dir.File(".clang-format"),
  256. LINT_SOURCES=[original_app_dir],
  257. )
  258. dist_env.PhonyTarget(
  259. "format",
  260. "${PYTHON3} ${FBT_SCRIPT_DIR}/lint.py format ${LINT_SOURCES}",
  261. source=original_app_dir.File(".clang-format"),
  262. LINT_SOURCES=[original_app_dir],
  263. )
  264. # Prepare vscode environment
  265. def _path_as_posix(path):
  266. return pathlib.Path(path).as_posix()
  267. vscode_dist = []
  268. project_template_dir = dist_env["UFBT_SCRIPT_ROOT"].Dir("project_template")
  269. for template_file in project_template_dir.Dir(".vscode").glob("*"):
  270. vscode_dist.append(
  271. dist_env.Substfile(
  272. original_app_dir.Dir(".vscode").File(template_file.name),
  273. template_file,
  274. SUBST_DICT={
  275. "@UFBT_VSCODE_PATH_SEP@": os.path.pathsep,
  276. "@UFBT_TOOLCHAIN_ARM_TOOLCHAIN_DIR@": pathlib.Path(
  277. dist_env.WhereIs("arm-none-eabi-gcc")
  278. ).parent.as_posix(),
  279. "@UFBT_TOOLCHAIN_GCC@": _path_as_posix(
  280. dist_env.WhereIs("arm-none-eabi-gcc")
  281. ),
  282. "@UFBT_TOOLCHAIN_GDB_PY@": _path_as_posix(
  283. dist_env.WhereIs("arm-none-eabi-gdb-py")
  284. ),
  285. "@UFBT_TOOLCHAIN_OPENOCD@": _path_as_posix(dist_env.WhereIs("openocd")),
  286. "@UFBT_APP_DIR@": _path_as_posix(original_app_dir.abspath),
  287. "@UFBT_ROOT_DIR@": _path_as_posix(Dir("#").abspath),
  288. "@UFBT_DEBUG_DIR@": dist_env["FBT_DEBUG_DIR"],
  289. "@UFBT_DEBUG_ELF_DIR@": _path_as_posix(
  290. dist_env["FBT_FAP_DEBUG_ELF_ROOT"].abspath
  291. ),
  292. "@UFBT_FIRMWARE_ELF@": _path_as_posix(dist_env["FW_ELF"].abspath),
  293. },
  294. )
  295. )
  296. for config_file in project_template_dir.glob(".*"):
  297. if isinstance(config_file, FS.Dir):
  298. continue
  299. vscode_dist.append(dist_env.Install(original_app_dir, config_file))
  300. dist_env.Precious(vscode_dist)
  301. dist_env.NoClean(vscode_dist)
  302. dist_env.Alias("vscode_dist", vscode_dist)
  303. # Creating app from base template
  304. dist_env.SetDefault(FBT_APPID=appenv.subst("$APPID") or "template")
  305. app_template_dist = []
  306. for template_file in project_template_dir.Dir("app_template").glob("*"):
  307. dist_file_name = dist_env.subst(template_file.name)
  308. if template_file.name.endswith(".png"):
  309. app_template_dist.append(
  310. dist_env.InstallAs(original_app_dir.File(dist_file_name), template_file)
  311. )
  312. else:
  313. app_template_dist.append(
  314. dist_env.Substfile(
  315. original_app_dir.File(dist_file_name),
  316. template_file,
  317. SUBST_DICT={
  318. "@FBT_APPID@": dist_env.subst("$FBT_APPID"),
  319. },
  320. )
  321. )
  322. AddPostAction(
  323. app_template_dist[-1],
  324. [
  325. Mkdir(original_app_dir.Dir("images")),
  326. Touch(original_app_dir.Dir("images").File(".gitkeep")),
  327. ],
  328. )
  329. dist_env.Precious(app_template_dist)
  330. dist_env.NoClean(app_template_dist)
  331. dist_env.Alias("create", app_template_dist)