core_ext.py 17 KB

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