core_ext.py 19 KB

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