core_ext.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. import fnmatch
  2. import os
  3. import shutil
  4. import subprocess
  5. import sys
  6. import click
  7. from idf_py_actions.constants import GENERATORS, SUPPORTED_TARGETS, PREVIEW_TARGETS
  8. from idf_py_actions.errors import FatalError
  9. from idf_py_actions.global_options import global_options
  10. from idf_py_actions.tools import ensure_build_directory, idf_version, merge_action_lists, realpath, run_target, TargetChoice
  11. def action_extensions(base_actions, project_path):
  12. def build_target(target_name, ctx, args):
  13. """
  14. Execute the target build system to build target 'target_name'
  15. Calls ensure_build_directory() which will run cmake to generate a build
  16. directory (with the specified generator) as needed.
  17. """
  18. ensure_build_directory(args, ctx.info_name)
  19. run_target(target_name, args)
  20. def list_build_system_targets(target_name, ctx, args):
  21. """Shows list of targets known to build sytem (make/ninja)"""
  22. build_target('help', ctx, args)
  23. def menuconfig(target_name, ctx, args, style):
  24. """
  25. Menuconfig target is build_target extended with the style argument for setting the value for the environment
  26. variable.
  27. """
  28. if sys.version_info[0] < 3:
  29. # The subprocess lib cannot accept environment variables as "unicode".
  30. # This encoding step is required only in Python 2.
  31. style = style.encode(sys.getfilesystemencoding() or 'utf-8')
  32. os.environ['MENUCONFIG_STYLE'] = style
  33. build_target(target_name, ctx, args)
  34. def fallback_target(target_name, ctx, args):
  35. """
  36. Execute targets that are not explicitly known to idf.py
  37. """
  38. ensure_build_directory(args, ctx.info_name)
  39. try:
  40. subprocess.check_output(GENERATORS[args.generator]["dry_run"] + [target_name], cwd=args.build_dir)
  41. except Exception:
  42. raise FatalError(
  43. 'command "%s" is not known to idf.py and is not a %s target' % (target_name, args.generator))
  44. run_target(target_name, args)
  45. def verbose_callback(ctx, param, value):
  46. if not value or ctx.resilient_parsing:
  47. return
  48. for line in ctx.command.verbose_output:
  49. print(line)
  50. return value
  51. def clean(action, ctx, args):
  52. if not os.path.isdir(args.build_dir):
  53. print("Build directory '%s' not found. Nothing to clean." % args.build_dir)
  54. return
  55. build_target("clean", ctx, args)
  56. def _delete_windows_symlinks(directory):
  57. """
  58. It deletes symlinks recursively on Windows. It is useful for Python 2 which doesn't detect symlinks on Windows.
  59. """
  60. deleted_paths = []
  61. if os.name == "nt":
  62. import ctypes
  63. for root, dirnames, _filenames in os.walk(directory):
  64. for d in dirnames:
  65. full_path = os.path.join(root, d)
  66. try:
  67. full_path = full_path.decode("utf-8")
  68. except Exception:
  69. pass
  70. if ctypes.windll.kernel32.GetFileAttributesW(full_path) & 0x0400:
  71. os.rmdir(full_path)
  72. deleted_paths.append(full_path)
  73. return deleted_paths
  74. def fullclean(action, ctx, args):
  75. build_dir = args.build_dir
  76. if not os.path.isdir(build_dir):
  77. print("Build directory '%s' not found. Nothing to clean." % build_dir)
  78. return
  79. if len(os.listdir(build_dir)) == 0:
  80. print("Build directory '%s' is empty. Nothing to clean." % build_dir)
  81. return
  82. if not os.path.exists(os.path.join(build_dir, "CMakeCache.txt")):
  83. raise FatalError(
  84. "Directory '%s' doesn't seem to be a CMake build directory. Refusing to automatically "
  85. "delete files in this directory. Delete the directory manually to 'clean' it." % build_dir)
  86. red_flags = ["CMakeLists.txt", ".git", ".svn"]
  87. for red in red_flags:
  88. red = os.path.join(build_dir, red)
  89. if os.path.exists(red):
  90. raise FatalError(
  91. "Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure."
  92. % red)
  93. # OK, delete everything in the build directory...
  94. # Note: Python 2.7 doesn't detect symlinks on Windows (it is supported form 3.2). Tools promising to not
  95. # follow symlinks will actually follow them. Deleting the build directory with symlinks deletes also items
  96. # outside of this directory.
  97. deleted_symlinks = _delete_windows_symlinks(build_dir)
  98. if args.verbose and len(deleted_symlinks) > 1:
  99. print("The following symlinks were identified and removed:\n%s" % "\n".join(deleted_symlinks))
  100. for f in os.listdir(build_dir): # TODO: once we are Python 3 only, this can be os.scandir()
  101. f = os.path.join(build_dir, f)
  102. if args.verbose:
  103. print("Removing: %s" % f)
  104. if os.path.isdir(f):
  105. shutil.rmtree(f)
  106. else:
  107. os.remove(f)
  108. def python_clean(action, ctx, args):
  109. for root, dirnames, filenames in os.walk(os.environ["IDF_PATH"]):
  110. for d in dirnames:
  111. if d == "__pycache__":
  112. dir_to_delete = os.path.join(root, d)
  113. if args.verbose:
  114. print("Removing: %s" % dir_to_delete)
  115. shutil.rmtree(dir_to_delete)
  116. for filename in fnmatch.filter(filenames, '*.py[co]'):
  117. file_to_delete = os.path.join(root, filename)
  118. if args.verbose:
  119. print("Removing: %s" % file_to_delete)
  120. os.remove(file_to_delete)
  121. def set_target(action, ctx, args, idf_target):
  122. if (not args["preview"] and idf_target in PREVIEW_TARGETS):
  123. raise FatalError(
  124. "%s is still in preview. You have to append '--preview' option after idf.py to use any preview feature."
  125. % idf_target)
  126. args.define_cache_entry.append("IDF_TARGET=" + idf_target)
  127. sdkconfig_path = os.path.join(args.project_dir, 'sdkconfig')
  128. sdkconfig_old = sdkconfig_path + ".old"
  129. if os.path.exists(sdkconfig_old):
  130. os.remove(sdkconfig_old)
  131. if os.path.exists(sdkconfig_path):
  132. os.rename(sdkconfig_path, sdkconfig_old)
  133. print("Set Target to: %s, new sdkconfig created. Existing sdkconfig renamed to sdkconfig.old." % idf_target)
  134. ensure_build_directory(args, ctx.info_name, True)
  135. def reconfigure(action, ctx, args):
  136. ensure_build_directory(args, ctx.info_name, True)
  137. def validate_root_options(ctx, args, tasks):
  138. args.project_dir = realpath(args.project_dir)
  139. if args.build_dir is not None and args.project_dir == realpath(args.build_dir):
  140. raise FatalError(
  141. "Setting the build directory to the project directory is not supported. Suggest dropping "
  142. "--build-dir option, the default is a 'build' subdirectory inside the project directory.")
  143. if args.build_dir is None:
  144. args.build_dir = os.path.join(args.project_dir, "build")
  145. args.build_dir = realpath(args.build_dir)
  146. def idf_version_callback(ctx, param, value):
  147. if not value or ctx.resilient_parsing:
  148. return
  149. version = idf_version()
  150. if not version:
  151. raise FatalError("ESP-IDF version cannot be determined")
  152. print("ESP-IDF %s" % version)
  153. sys.exit(0)
  154. def list_targets_callback(ctx, param, value):
  155. if not value or ctx.resilient_parsing:
  156. return
  157. for target in SUPPORTED_TARGETS:
  158. print(target)
  159. if "preview" in ctx.params:
  160. for target in PREVIEW_TARGETS:
  161. print(target)
  162. sys.exit(0)
  163. root_options = {
  164. "global_options": [
  165. {
  166. "names": ["--version"],
  167. "help": "Show IDF version and exit.",
  168. "is_flag": True,
  169. "expose_value": False,
  170. "callback": idf_version_callback,
  171. },
  172. {
  173. "names": ["--list-targets"],
  174. "help": "Print list of supported targets and exit.",
  175. "is_flag": True,
  176. "expose_value": False,
  177. "callback": list_targets_callback,
  178. },
  179. {
  180. "names": ["-C", "--project-dir"],
  181. "scope": "shared",
  182. "help": "Project directory.",
  183. "type": click.Path(),
  184. "default": os.getcwd(),
  185. },
  186. {
  187. "names": ["-B", "--build-dir"],
  188. "help": "Build directory.",
  189. "type": click.Path(),
  190. "default": None,
  191. },
  192. {
  193. "names": ["-w/-n", "--cmake-warn-uninitialized/--no-warnings"],
  194. "help": ("Enable CMake uninitialized variable warnings for CMake files inside the project directory. "
  195. "(--no-warnings is now the default, and doesn't need to be specified.)"),
  196. "envvar": "IDF_CMAKE_WARN_UNINITIALIZED",
  197. "is_flag": True,
  198. "default": False,
  199. },
  200. {
  201. "names": ["-v", "--verbose"],
  202. "help": "Verbose build output.",
  203. "is_flag": True,
  204. "is_eager": True,
  205. "default": False,
  206. "callback": verbose_callback,
  207. },
  208. {
  209. "names": ["--preview"],
  210. "help": "Enable IDF features that are still in preview.",
  211. "is_flag": True,
  212. "default": False,
  213. },
  214. {
  215. "names": ["--ccache/--no-ccache"],
  216. "help": "Use ccache in build. Disabled by default.",
  217. "is_flag": True,
  218. "envvar": "IDF_CCACHE_ENABLE",
  219. "default": False,
  220. },
  221. {
  222. "names": ["-G", "--generator"],
  223. "help": "CMake generator.",
  224. "type": click.Choice(GENERATORS.keys()),
  225. },
  226. {
  227. "names": ["--dry-run"],
  228. "help": "Only process arguments, but don't execute actions.",
  229. "is_flag": True,
  230. "hidden": True,
  231. "default": False,
  232. },
  233. ],
  234. "global_action_callbacks": [validate_root_options],
  235. }
  236. build_actions = {
  237. "actions": {
  238. "all": {
  239. "aliases": ["build"],
  240. "callback": build_target,
  241. "short_help": "Build the project.",
  242. "help": (
  243. "Build the project. This can involve multiple steps:\n\n"
  244. "1. Create the build directory if needed. "
  245. "The sub-directory 'build' is used to hold build output, "
  246. "although this can be changed with the -B option.\n\n"
  247. "2. Run CMake as necessary to configure the project "
  248. "and generate build files for the main build tool.\n\n"
  249. "3. Run the main build tool (Ninja or GNU Make). "
  250. "By default, the build tool is automatically detected "
  251. "but it can be explicitly set by passing the -G option to idf.py.\n\n"),
  252. "options": global_options,
  253. "order_dependencies": [
  254. "reconfigure",
  255. "menuconfig",
  256. "clean",
  257. "fullclean",
  258. ],
  259. },
  260. "menuconfig": {
  261. "callback": menuconfig,
  262. "help": 'Run "menuconfig" project configuration tool.',
  263. "options": global_options + [
  264. {
  265. "names": ["--style", "--color-scheme", "style"],
  266. "help": (
  267. "Menuconfig style.\n"
  268. "The built-in styles include:\n\n"
  269. "- default - a yellowish theme,\n\n"
  270. "- monochrome - a black and white theme, or\n\n"
  271. "- aquatic - a blue theme.\n\n"
  272. "It is possible to customize these themes further"
  273. " as it is described in the Color schemes section of the kconfiglib documentation.\n"
  274. 'The default value is \"aquatic\".'),
  275. "envvar": "MENUCONFIG_STYLE",
  276. "default": "aquatic",
  277. }
  278. ],
  279. },
  280. "confserver": {
  281. "callback": build_target,
  282. "help": "Run JSON configuration server.",
  283. "options": global_options,
  284. },
  285. "size": {
  286. "callback": build_target,
  287. "help": "Print basic size information about the app.",
  288. "options": global_options,
  289. "dependencies": ["app"],
  290. },
  291. "size-components": {
  292. "callback": build_target,
  293. "help": "Print per-component size information.",
  294. "options": global_options,
  295. "dependencies": ["app"],
  296. },
  297. "size-files": {
  298. "callback": build_target,
  299. "help": "Print per-source-file size information.",
  300. "options": global_options,
  301. "dependencies": ["app"],
  302. },
  303. "bootloader": {
  304. "callback": build_target,
  305. "help": "Build only bootloader.",
  306. "options": global_options,
  307. },
  308. "app": {
  309. "callback": build_target,
  310. "help": "Build only the app.",
  311. "order_dependencies": ["clean", "fullclean", "reconfigure"],
  312. "options": global_options,
  313. },
  314. "efuse_common_table": {
  315. "callback": build_target,
  316. "help": "Generate C-source for IDF's eFuse fields.",
  317. "order_dependencies": ["reconfigure"],
  318. "options": global_options,
  319. },
  320. "efuse_custom_table": {
  321. "callback": build_target,
  322. "help": "Generate C-source for user's eFuse fields.",
  323. "order_dependencies": ["reconfigure"],
  324. "options": global_options,
  325. },
  326. "show_efuse_table": {
  327. "callback": build_target,
  328. "help": "Print eFuse table.",
  329. "order_dependencies": ["reconfigure"],
  330. "options": global_options,
  331. },
  332. "partition_table": {
  333. "callback": build_target,
  334. "help": "Build only partition table.",
  335. "order_dependencies": ["reconfigure"],
  336. "options": global_options,
  337. },
  338. "erase_otadata": {
  339. "callback": build_target,
  340. "help": "Erase otadata partition.",
  341. "options": global_options,
  342. },
  343. "read_otadata": {
  344. "callback": build_target,
  345. "help": "Read otadata partition.",
  346. "options": global_options,
  347. },
  348. "build-system-targets": {
  349. "callback": list_build_system_targets,
  350. "help": "Print list of build system targets.",
  351. },
  352. "fallback": {
  353. "callback": fallback_target,
  354. "help": "Handle for targets not known for idf.py.",
  355. "hidden": True,
  356. }
  357. }
  358. }
  359. clean_actions = {
  360. "actions": {
  361. "reconfigure": {
  362. "callback": reconfigure,
  363. "short_help": "Re-run CMake.",
  364. "help": (
  365. "Re-run CMake even if it doesn't seem to need re-running. "
  366. "This isn't necessary during normal usage, "
  367. "but can be useful after adding/removing files from the source tree, "
  368. "or when modifying CMake cache variables. "
  369. "For example, \"idf.py -DNAME='VALUE' reconfigure\" "
  370. 'can be used to set variable "NAME" in CMake cache to value "VALUE".'),
  371. "options": global_options,
  372. "order_dependencies": ["menuconfig", "fullclean"],
  373. },
  374. "set-target": {
  375. "callback": set_target,
  376. "short_help": "Set the chip target to build.",
  377. "help": (
  378. "Set the chip target to build. This will remove the "
  379. "existing sdkconfig file and corresponding CMakeCache and "
  380. "create new ones according to the new target.\nFor example, "
  381. "\"idf.py set-target esp32\" will select esp32 as the new chip "
  382. "target."),
  383. "arguments": [
  384. {
  385. "names": ["idf-target"],
  386. "nargs": 1,
  387. "type": TargetChoice(SUPPORTED_TARGETS + PREVIEW_TARGETS),
  388. },
  389. ],
  390. "dependencies": ["fullclean"],
  391. },
  392. "clean": {
  393. "callback": clean,
  394. "short_help": "Delete build output files from the build directory.",
  395. "help": (
  396. "Delete build output files from the build directory, "
  397. "forcing a 'full rebuild' the next time "
  398. "the project is built. Cleaning doesn't delete "
  399. "CMake configuration output and some other files"),
  400. "order_dependencies": ["fullclean"],
  401. },
  402. "fullclean": {
  403. "callback": fullclean,
  404. "short_help": "Delete the entire build directory contents.",
  405. "help": (
  406. "Delete the entire build directory contents. "
  407. "This includes all CMake configuration output."
  408. "The next time the project is built, "
  409. "CMake will configure it from scratch. "
  410. "Note that this option recursively deletes all files "
  411. "in the build directory, so use with care."
  412. "Project configuration is not deleted.")
  413. },
  414. "python-clean": {
  415. "callback": python_clean,
  416. "short_help": "Delete generated Python byte code from the IDF directory",
  417. "help": (
  418. "Delete generated Python byte code from the IDF directory "
  419. "which may cause issues when switching between IDF and Python versions. "
  420. "It is advised to run this target after switching versions.")
  421. },
  422. }
  423. }
  424. return merge_action_lists(root_options, build_actions, clean_actions)