_local_setup_util_ps1.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. # Copyright 2016-2019 Dirk Thomas
  2. # Licensed under the Apache License, Version 2.0
  3. import argparse
  4. from collections import OrderedDict
  5. import os
  6. from pathlib import Path
  7. import sys
  8. FORMAT_STR_COMMENT_LINE = '# {comment}'
  9. FORMAT_STR_SET_ENV_VAR = 'Set-Item -Path "Env:{name}" -Value "{value}"'
  10. FORMAT_STR_USE_ENV_VAR = '$env:{name}'
  11. FORMAT_STR_INVOKE_SCRIPT = '_colcon_prefix_powershell_source_script "{script_path}"' # noqa: E501
  12. FORMAT_STR_REMOVE_LEADING_SEPARATOR = '' # noqa: E501
  13. FORMAT_STR_REMOVE_TRAILING_SEPARATOR = '' # noqa: E501
  14. DSV_TYPE_APPEND_NON_DUPLICATE = 'append-non-duplicate'
  15. DSV_TYPE_PREPEND_NON_DUPLICATE = 'prepend-non-duplicate'
  16. DSV_TYPE_PREPEND_NON_DUPLICATE_IF_EXISTS = 'prepend-non-duplicate-if-exists'
  17. DSV_TYPE_SET = 'set'
  18. DSV_TYPE_SET_IF_UNSET = 'set-if-unset'
  19. DSV_TYPE_SOURCE = 'source'
  20. def main(argv=sys.argv[1:]): # noqa: D103
  21. parser = argparse.ArgumentParser(
  22. description='Output shell commands for the packages in topological '
  23. 'order')
  24. parser.add_argument(
  25. 'primary_extension',
  26. help='The file extension of the primary shell')
  27. parser.add_argument(
  28. 'additional_extension', nargs='?',
  29. help='The additional file extension to be considered')
  30. parser.add_argument(
  31. '--merged-install', action='store_true',
  32. help='All install prefixes are merged into a single location')
  33. args = parser.parse_args(argv)
  34. packages = get_packages(Path(__file__).parent, args.merged_install)
  35. ordered_packages = order_packages(packages)
  36. for pkg_name in ordered_packages:
  37. if _include_comments():
  38. print(
  39. FORMAT_STR_COMMENT_LINE.format_map(
  40. {'comment': 'Package: ' + pkg_name}))
  41. prefix = os.path.abspath(os.path.dirname(__file__))
  42. if not args.merged_install:
  43. prefix = os.path.join(prefix, pkg_name)
  44. for line in get_commands(
  45. pkg_name, prefix, args.primary_extension,
  46. args.additional_extension
  47. ):
  48. print(line)
  49. for line in _remove_ending_separators():
  50. print(line)
  51. def get_packages(prefix_path, merged_install):
  52. """
  53. Find packages based on colcon-specific files created during installation.
  54. :param Path prefix_path: The install prefix path of all packages
  55. :param bool merged_install: The flag if the packages are all installed
  56. directly in the prefix or if each package is installed in a subdirectory
  57. named after the package
  58. :returns: A mapping from the package name to the set of runtime
  59. dependencies
  60. :rtype: dict
  61. """
  62. packages = {}
  63. # since importing colcon_core isn't feasible here the following constant
  64. # must match colcon_core.location.get_relative_package_index_path()
  65. subdirectory = 'share/colcon-core/packages'
  66. if merged_install:
  67. # return if workspace is empty
  68. if not (prefix_path / subdirectory).is_dir():
  69. return packages
  70. # find all files in the subdirectory
  71. for p in (prefix_path / subdirectory).iterdir():
  72. if not p.is_file():
  73. continue
  74. if p.name.startswith('.'):
  75. continue
  76. add_package_runtime_dependencies(p, packages)
  77. else:
  78. # for each subdirectory look for the package specific file
  79. for p in prefix_path.iterdir():
  80. if not p.is_dir():
  81. continue
  82. if p.name.startswith('.'):
  83. continue
  84. p = p / subdirectory / p.name
  85. if p.is_file():
  86. add_package_runtime_dependencies(p, packages)
  87. # remove unknown dependencies
  88. pkg_names = set(packages.keys())
  89. for k in packages.keys():
  90. packages[k] = {d for d in packages[k] if d in pkg_names}
  91. return packages
  92. def add_package_runtime_dependencies(path, packages):
  93. """
  94. Check the path and if it exists extract the packages runtime dependencies.
  95. :param Path path: The resource file containing the runtime dependencies
  96. :param dict packages: A mapping from package names to the sets of runtime
  97. dependencies to add to
  98. """
  99. content = path.read_text()
  100. dependencies = set(content.split(os.pathsep) if content else [])
  101. packages[path.name] = dependencies
  102. def order_packages(packages):
  103. """
  104. Order packages topologically.
  105. :param dict packages: A mapping from package name to the set of runtime
  106. dependencies
  107. :returns: The package names
  108. :rtype: list
  109. """
  110. # select packages with no dependencies in alphabetical order
  111. to_be_ordered = list(packages.keys())
  112. ordered = []
  113. while to_be_ordered:
  114. pkg_names_without_deps = [
  115. name for name in to_be_ordered if not packages[name]]
  116. if not pkg_names_without_deps:
  117. reduce_cycle_set(packages)
  118. raise RuntimeError(
  119. 'Circular dependency between: ' + ', '.join(sorted(packages)))
  120. pkg_names_without_deps.sort()
  121. pkg_name = pkg_names_without_deps[0]
  122. to_be_ordered.remove(pkg_name)
  123. ordered.append(pkg_name)
  124. # remove item from dependency lists
  125. for k in list(packages.keys()):
  126. if pkg_name in packages[k]:
  127. packages[k].remove(pkg_name)
  128. return ordered
  129. def reduce_cycle_set(packages):
  130. """
  131. Reduce the set of packages to the ones part of the circular dependency.
  132. :param dict packages: A mapping from package name to the set of runtime
  133. dependencies which is modified in place
  134. """
  135. last_depended = None
  136. while len(packages) > 0:
  137. # get all remaining dependencies
  138. depended = set()
  139. for pkg_name, dependencies in packages.items():
  140. depended = depended.union(dependencies)
  141. # remove all packages which are not dependent on
  142. for name in list(packages.keys()):
  143. if name not in depended:
  144. del packages[name]
  145. if last_depended:
  146. # if remaining packages haven't changed return them
  147. if last_depended == depended:
  148. return packages.keys()
  149. # otherwise reduce again
  150. last_depended = depended
  151. def _include_comments():
  152. # skipping comment lines when COLCON_TRACE is not set speeds up the
  153. # processing especially on Windows
  154. return bool(os.environ.get('COLCON_TRACE'))
  155. def get_commands(pkg_name, prefix, primary_extension, additional_extension):
  156. commands = []
  157. package_dsv_path = os.path.join(prefix, 'share', pkg_name, 'package.dsv')
  158. if os.path.exists(package_dsv_path):
  159. commands += process_dsv_file(
  160. package_dsv_path, prefix, primary_extension, additional_extension)
  161. return commands
  162. def process_dsv_file(
  163. dsv_path, prefix, primary_extension=None, additional_extension=None
  164. ):
  165. commands = []
  166. if _include_comments():
  167. commands.append(FORMAT_STR_COMMENT_LINE.format_map({'comment': dsv_path}))
  168. with open(dsv_path, 'r') as h:
  169. content = h.read()
  170. lines = content.splitlines()
  171. basenames = OrderedDict()
  172. for i, line in enumerate(lines):
  173. # skip over empty or whitespace-only lines
  174. if not line.strip():
  175. continue
  176. # skip over comments
  177. if line.startswith('#'):
  178. continue
  179. try:
  180. type_, remainder = line.split(';', 1)
  181. except ValueError:
  182. raise RuntimeError(
  183. "Line %d in '%s' doesn't contain a semicolon separating the "
  184. 'type from the arguments' % (i + 1, dsv_path))
  185. if type_ != DSV_TYPE_SOURCE:
  186. # handle non-source lines
  187. try:
  188. commands += handle_dsv_types_except_source(
  189. type_, remainder, prefix)
  190. except RuntimeError as e:
  191. raise RuntimeError(
  192. "Line %d in '%s' %s" % (i + 1, dsv_path, e)) from e
  193. else:
  194. # group remaining source lines by basename
  195. path_without_ext, ext = os.path.splitext(remainder)
  196. if path_without_ext not in basenames:
  197. basenames[path_without_ext] = set()
  198. assert ext.startswith('.')
  199. ext = ext[1:]
  200. if ext in (primary_extension, additional_extension):
  201. basenames[path_without_ext].add(ext)
  202. # add the dsv extension to each basename if the file exists
  203. for basename, extensions in basenames.items():
  204. if not os.path.isabs(basename):
  205. basename = os.path.join(prefix, basename)
  206. if os.path.exists(basename + '.dsv'):
  207. extensions.add('dsv')
  208. for basename, extensions in basenames.items():
  209. if not os.path.isabs(basename):
  210. basename = os.path.join(prefix, basename)
  211. if 'dsv' in extensions:
  212. # process dsv files recursively
  213. commands += process_dsv_file(
  214. basename + '.dsv', prefix, primary_extension=primary_extension,
  215. additional_extension=additional_extension)
  216. elif primary_extension in extensions and len(extensions) == 1:
  217. # source primary-only files
  218. commands += [
  219. FORMAT_STR_INVOKE_SCRIPT.format_map({
  220. 'prefix': prefix,
  221. 'script_path': basename + '.' + primary_extension})]
  222. elif additional_extension in extensions:
  223. # source non-primary files
  224. commands += [
  225. FORMAT_STR_INVOKE_SCRIPT.format_map({
  226. 'prefix': prefix,
  227. 'script_path': basename + '.' + additional_extension})]
  228. return commands
  229. def handle_dsv_types_except_source(type_, remainder, prefix):
  230. commands = []
  231. if type_ in (DSV_TYPE_SET, DSV_TYPE_SET_IF_UNSET):
  232. try:
  233. env_name, value = remainder.split(';', 1)
  234. except ValueError:
  235. raise RuntimeError(
  236. "doesn't contain a semicolon separating the environment name "
  237. 'from the value')
  238. try_prefixed_value = os.path.join(prefix, value) if value else prefix
  239. if os.path.exists(try_prefixed_value):
  240. value = try_prefixed_value
  241. if type_ == DSV_TYPE_SET:
  242. commands += _set(env_name, value)
  243. elif type_ == DSV_TYPE_SET_IF_UNSET:
  244. commands += _set_if_unset(env_name, value)
  245. else:
  246. assert False
  247. elif type_ in (
  248. DSV_TYPE_APPEND_NON_DUPLICATE,
  249. DSV_TYPE_PREPEND_NON_DUPLICATE,
  250. DSV_TYPE_PREPEND_NON_DUPLICATE_IF_EXISTS
  251. ):
  252. try:
  253. env_name_and_values = remainder.split(';')
  254. except ValueError:
  255. raise RuntimeError(
  256. "doesn't contain a semicolon separating the environment name "
  257. 'from the values')
  258. env_name = env_name_and_values[0]
  259. values = env_name_and_values[1:]
  260. for value in values:
  261. if not value:
  262. value = prefix
  263. elif not os.path.isabs(value):
  264. value = os.path.join(prefix, value)
  265. if (
  266. type_ == DSV_TYPE_PREPEND_NON_DUPLICATE_IF_EXISTS and
  267. not os.path.exists(value)
  268. ):
  269. comment = f'skip extending {env_name} with not existing ' \
  270. f'path: {value}'
  271. if _include_comments():
  272. commands.append(
  273. FORMAT_STR_COMMENT_LINE.format_map({'comment': comment}))
  274. elif type_ == DSV_TYPE_APPEND_NON_DUPLICATE:
  275. commands += _append_unique_value(env_name, value)
  276. else:
  277. commands += _prepend_unique_value(env_name, value)
  278. else:
  279. raise RuntimeError(
  280. 'contains an unknown environment hook type: ' + type_)
  281. return commands
  282. env_state = {}
  283. def _append_unique_value(name, value):
  284. global env_state
  285. if name not in env_state:
  286. if os.environ.get(name):
  287. env_state[name] = set(os.environ[name].split(os.pathsep))
  288. else:
  289. env_state[name] = set()
  290. # append even if the variable has not been set yet, in case a shell script sets the
  291. # same variable without the knowledge of this Python script.
  292. # later _remove_ending_separators() will cleanup any unintentional leading separator
  293. extend = FORMAT_STR_USE_ENV_VAR.format_map({'name': name}) + os.pathsep
  294. line = FORMAT_STR_SET_ENV_VAR.format_map(
  295. {'name': name, 'value': extend + value})
  296. if value not in env_state[name]:
  297. env_state[name].add(value)
  298. else:
  299. if not _include_comments():
  300. return []
  301. line = FORMAT_STR_COMMENT_LINE.format_map({'comment': line})
  302. return [line]
  303. def _prepend_unique_value(name, value):
  304. global env_state
  305. if name not in env_state:
  306. if os.environ.get(name):
  307. env_state[name] = set(os.environ[name].split(os.pathsep))
  308. else:
  309. env_state[name] = set()
  310. # prepend even if the variable has not been set yet, in case a shell script sets the
  311. # same variable without the knowledge of this Python script.
  312. # later _remove_ending_separators() will cleanup any unintentional trailing separator
  313. extend = os.pathsep + FORMAT_STR_USE_ENV_VAR.format_map({'name': name})
  314. line = FORMAT_STR_SET_ENV_VAR.format_map(
  315. {'name': name, 'value': value + extend})
  316. if value not in env_state[name]:
  317. env_state[name].add(value)
  318. else:
  319. if not _include_comments():
  320. return []
  321. line = FORMAT_STR_COMMENT_LINE.format_map({'comment': line})
  322. return [line]
  323. # generate commands for removing prepended underscores
  324. def _remove_ending_separators():
  325. # do nothing if the shell extension does not implement the logic
  326. if FORMAT_STR_REMOVE_TRAILING_SEPARATOR is None:
  327. return []
  328. global env_state
  329. commands = []
  330. for name in env_state:
  331. # skip variables that already had values before this script started prepending
  332. if name in os.environ:
  333. continue
  334. commands += [
  335. FORMAT_STR_REMOVE_LEADING_SEPARATOR.format_map({'name': name}),
  336. FORMAT_STR_REMOVE_TRAILING_SEPARATOR.format_map({'name': name})]
  337. return commands
  338. def _set(name, value):
  339. global env_state
  340. env_state[name] = value
  341. line = FORMAT_STR_SET_ENV_VAR.format_map(
  342. {'name': name, 'value': value})
  343. return [line]
  344. def _set_if_unset(name, value):
  345. global env_state
  346. line = FORMAT_STR_SET_ENV_VAR.format_map(
  347. {'name': name, 'value': value})
  348. if env_state.get(name, os.environ.get(name)):
  349. line = FORMAT_STR_COMMENT_LINE.format_map({'comment': line})
  350. return [line]
  351. if __name__ == '__main__': # pragma: no cover
  352. try:
  353. rc = main()
  354. except RuntimeError as e:
  355. print(str(e), file=sys.stderr)
  356. rc = 1
  357. sys.exit(rc)