_local_setup_util_ps1.py 13 KB

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