PageRenderTime 58ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/contrib/go/src/python/pants/contrib/go/tasks/go_buildgen.py

https://gitlab.com/Ivy001/pants
Python | 459 lines | 421 code | 19 blank | 19 comment | 20 complexity | 149e702b80c398641ffe4cdbd2ed1e78 MD5 | raw file
  1. # coding=utf-8
  2. # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
  3. # Licensed under the Apache License, Version 2.0 (see LICENSE).
  4. from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
  5. unicode_literals, with_statement)
  6. import os
  7. from collections import defaultdict, namedtuple
  8. from textwrap import dedent
  9. from pants.base.build_environment import get_buildroot
  10. from pants.base.exceptions import TaskError
  11. from pants.base.generator import Generator, TemplateData
  12. from pants.base.workunit import WorkUnitLabel
  13. from pants.build_graph.address import Address
  14. from pants.build_graph.address_lookup_error import AddressLookupError
  15. from pants.util.contextutil import temporary_dir
  16. from pants.util.dirutil import safe_mkdir, safe_open
  17. from pants.contrib.go.subsystems.fetcher_factory import FetcherFactory
  18. from pants.contrib.go.targets.go_binary import GoBinary
  19. from pants.contrib.go.targets.go_library import GoLibrary
  20. from pants.contrib.go.targets.go_local_source import GoLocalSource
  21. from pants.contrib.go.targets.go_remote_library import GoRemoteLibrary
  22. from pants.contrib.go.tasks.go_task import GoTask
  23. class GoTargetGenerator(object):
  24. """Automatically generates a Go target graph given pre-existing target roots."""
  25. class GenerationError(Exception):
  26. """Raised to indicate an error auto-generating a Go target."""
  27. class WrongLocalSourceTargetTypeError(GenerationError):
  28. """Indicates a local source target was defined with the wrong type.
  29. For example, a Go main package was defined as a GoLibrary instead of a GoBinary.
  30. """
  31. class NewRemoteEncounteredButRemotesNotAllowedError(GenerationError):
  32. """Indicates a new remote library dependency was found but --remote was not enabled."""
  33. def __init__(self, import_oracle, build_graph, local_root, fetcher_factory,
  34. generate_remotes=False, remote_root=None):
  35. self._import_oracle = import_oracle
  36. self._build_graph = build_graph
  37. self._local_source_root = local_root
  38. self._fetcher_factory = fetcher_factory
  39. self._generate_remotes = generate_remotes
  40. self._remote_source_root = remote_root
  41. def generate(self, local_go_targets):
  42. """Automatically generates a Go target graph for the given local go targets.
  43. :param iter local_go_targets: The target roots to fill in a target graph for.
  44. :raises: :class:`GoTargetGenerator.GenerationError` if any missing targets cannot be generated.
  45. """
  46. visited = {l.import_path: l.address for l in local_go_targets}
  47. with temporary_dir() as gopath:
  48. for local_go_target in local_go_targets:
  49. deps = self._list_deps(gopath, local_go_target.address)
  50. self._generate_missing(gopath, local_go_target.address, deps, visited)
  51. return visited.items()
  52. def _generate_missing(self, gopath, local_address, import_listing, visited):
  53. target_type = GoBinary if import_listing.pkg_name == 'main' else GoLibrary
  54. existing = self._build_graph.get_target(local_address)
  55. if not existing:
  56. self._build_graph.inject_synthetic_target(address=local_address, target_type=target_type)
  57. elif existing and not isinstance(existing, target_type):
  58. raise self.WrongLocalSourceTargetTypeError('{} should be a {}'
  59. .format(existing, target_type.__name__))
  60. for import_path in import_listing.all_imports:
  61. if not self._import_oracle.is_go_internal_import(import_path):
  62. if import_path not in visited:
  63. if self._import_oracle.is_remote_import(import_path):
  64. remote_root = self._fetcher_factory.get_fetcher(import_path).root()
  65. remote_pkg_path = GoRemoteLibrary.remote_package_path(remote_root, import_path)
  66. name = remote_pkg_path or os.path.basename(import_path)
  67. address = Address(os.path.join(self._remote_source_root, remote_root), name)
  68. try:
  69. self._build_graph.inject_address_closure(address)
  70. except AddressLookupError:
  71. if not self._generate_remotes:
  72. raise self.NewRemoteEncounteredButRemotesNotAllowedError(
  73. 'Cannot generate dependency for remote import path {}'.format(import_path))
  74. self._build_graph.inject_synthetic_target(address=address,
  75. target_type=GoRemoteLibrary,
  76. pkg=remote_pkg_path)
  77. else:
  78. # Recurse on local targets.
  79. address = Address(os.path.join(self._local_source_root, import_path),
  80. os.path.basename(import_path))
  81. deps = self._list_deps(gopath, address)
  82. self._generate_missing(gopath, address, deps, visited)
  83. visited[import_path] = address
  84. dependency_address = visited[import_path]
  85. self._build_graph.inject_dependency(local_address, dependency_address)
  86. def _list_deps(self, gopath, local_address):
  87. # TODO(John Sirois): Lift out a local go sources target chroot util - GoWorkspaceTask and
  88. # GoTargetGenerator both create these chroot symlink trees now.
  89. import_path = GoLocalSource.local_import_path(self._local_source_root, local_address)
  90. src_path = os.path.join(gopath, 'src', import_path)
  91. safe_mkdir(src_path)
  92. package_src_root = os.path.join(get_buildroot(), local_address.spec_path)
  93. for source_file in os.listdir(package_src_root):
  94. source_path = os.path.join(package_src_root, source_file)
  95. if GoLocalSource.is_go_source(source_path):
  96. dest_path = os.path.join(src_path, source_file)
  97. os.symlink(source_path, dest_path)
  98. return self._import_oracle.list_imports(import_path, gopath=gopath)
  99. class GoBuildgen(GoTask):
  100. """Automatically generates Go BUILD files."""
  101. @classmethod
  102. def subsystem_dependencies(cls):
  103. return super(GoBuildgen, cls).subsystem_dependencies() + (FetcherFactory,)
  104. @classmethod
  105. def _default_template(cls):
  106. return dedent("""\
  107. {{#target.parameters?}}
  108. {{target.type}}(
  109. {{#target.parameters}}
  110. {{#deps?}}
  111. dependencies=[
  112. {{#deps}}
  113. '{{.}}',
  114. {{/deps}}
  115. ]
  116. {{/deps?}}
  117. {{#rev}}
  118. rev='{{.}}',
  119. {{/rev}}
  120. {{#pkgs?}}
  121. packages=[
  122. {{#pkgs}}
  123. '{{.}}',
  124. {{/pkgs}}
  125. ]
  126. {{/pkgs?}}
  127. {{/target.parameters}}
  128. )
  129. {{/target.parameters?}}
  130. {{^target.parameters?}}
  131. {{target.type}}()
  132. {{/target.parameters?}}
  133. """)
  134. @classmethod
  135. def register_options(cls, register):
  136. register('--remote', type=bool, advanced=True, fingerprint=True,
  137. help='Allow auto-generation of remote dependencies without pinned versions '
  138. '(FLOATING versions).')
  139. register('--fail-floating', type=bool, advanced=True, fingerprint=True,
  140. help='After generating all dependencies, fail if any newly generated or pre-existing '
  141. 'dependencies have un-pinned - aka FLOATING - versions.')
  142. register('--materialize', type=bool, advanced=True, fingerprint=True,
  143. help='Instead of just auto-generating missing go_binary and go_library targets in '
  144. 'memory, (re-)generate them on disk using the installed Go BUILD file template.')
  145. # TODO(John Sirois): Add docs for the template parameters.
  146. register('--template', metavar='<template>', fromfile=True,
  147. default=cls._default_template(),
  148. advanced=True, fingerprint=True,
  149. help='A Go BUILD file mustache template to use with --materialize.')
  150. register('--extension', default='', metavar='<ext>', advanced=True, fingerprint=True,
  151. help='An optional extension for all materialized BUILD files (should include the .)')
  152. def execute(self):
  153. materialize = self.get_options().materialize
  154. if materialize:
  155. local_go_targets = None # We want a full scan, which passing no local go targets signals.
  156. if self.context.target_roots:
  157. self.context.log.warn('{} ignoring targets passed on the command line and re-materializing '
  158. 'the complete Go BUILD forest.'.format(self.options_scope))
  159. else:
  160. local_go_targets = self.context.targets(self.is_local_src)
  161. if not local_go_targets:
  162. return
  163. generation_result = self.generate_targets(local_go_targets=local_go_targets)
  164. if not generation_result:
  165. return
  166. # TODO(John Sirois): It would be nice to fail for floating revs for either the materialize or
  167. # in-memory cases. Right now we only fail for the materialize case.
  168. if not materialize:
  169. msg = ('Auto generated the following Go targets: target (import path):\n\t{}'
  170. .format('\n\t'.join(sorted('{} ({})'.format(addr.reference(), ip)
  171. for ip, addr in generation_result.generated))))
  172. self.context.log.info(msg)
  173. elif generation_result:
  174. self._materialize(generation_result)
  175. class TemplateResult(namedtuple('TemplateResult', ['build_file_path', 'data', 'import_paths',
  176. 'local', 'rev', 'fail_floating'])):
  177. @classmethod
  178. def local_target(cls, build_file_path, data, import_paths):
  179. return cls(build_file_path=build_file_path, data=data, import_paths=import_paths, local=True,
  180. rev=None, fail_floating=False)
  181. @classmethod
  182. def remote_target(cls, build_file_path, data, import_paths, rev, fail_floating):
  183. return cls(build_file_path=build_file_path, data=data, import_paths=import_paths, local=False,
  184. rev=rev, fail_floating=fail_floating)
  185. def log(self, logger):
  186. """Log information about the generated target including its BUILD file and import paths.
  187. :param logger: The logger to log with.
  188. :type logger: A :class:`logging.Logger` compatible object.
  189. """
  190. log = logger.info if self.local or self.rev else logger.warn
  191. log('\t{}'.format(self))
  192. @property
  193. def failed(self):
  194. """Return `True` if the generated target should be considered a failed generation.
  195. :rtype: bool
  196. """
  197. return self.fail_floating and not self.rev
  198. def __str__(self):
  199. import_paths = ' '.join(sorted(self.import_paths))
  200. rev = '' if self.local else ' {}'.format(self.rev or 'FLOATING')
  201. return ('{build_file_path} ({import_paths}){rev}'
  202. .format(build_file_path=self.build_file_path, import_paths=import_paths, rev=rev))
  203. class FloatingRemoteError(TaskError):
  204. """Indicates Go remote libraries exist or were generated that don't specify a `rev`."""
  205. def _materialize(self, generation_result):
  206. remote = self.get_options().remote
  207. existing_go_buildfiles = set()
  208. def gather_go_buildfiles(rel_path):
  209. address_mapper = self.context.address_mapper
  210. for build_file in address_mapper.scan_build_files(base_path=rel_path):
  211. existing_go_buildfiles.add(build_file.relpath)
  212. gather_go_buildfiles(generation_result.local_root)
  213. if remote and generation_result.remote_root != generation_result.local_root:
  214. gather_go_buildfiles(generation_result.remote_root)
  215. targets = set(self.context.build_graph.targets(self.is_go))
  216. if remote and generation_result.remote_root:
  217. # Generation only walks out from local source, but we might have transitive remote
  218. # dependencies under the remote root which are not linked except by `resolve.go`. Add all
  219. # the remotes we can find to ensure they are re-materialized too.
  220. remote_root = os.path.join(get_buildroot(), generation_result.remote_root)
  221. targets.update(self.context.scan(remote_root).targets(self.is_remote_lib))
  222. failed_results = []
  223. for result in self.generate_build_files(targets):
  224. existing_go_buildfiles.discard(result.build_file_path)
  225. result.log(self.context.log)
  226. if result.failed:
  227. failed_results.append(result)
  228. if existing_go_buildfiles:
  229. deleted = []
  230. for existing_go_buildfile in existing_go_buildfiles:
  231. spec_path = os.path.dirname(existing_go_buildfile)
  232. for address in self.context.address_mapper.addresses_in_spec_path(spec_path):
  233. target = self.context.address_mapper.resolve(address)
  234. if isinstance(target, GoLocalSource):
  235. os.unlink(existing_go_buildfile)
  236. deleted.append(existing_go_buildfile)
  237. if deleted:
  238. self.context.log.info('Deleted the following obsolete BUILD files:\n\t{}'
  239. .format('\n\t'.join(sorted(deleted))))
  240. if failed_results:
  241. self.context.log.error('Un-pinned (FLOATING) Go remote library dependencies are not '
  242. 'allowed in this repository!\n'
  243. 'Found the following FLOATING Go remote libraries:\n\t{}'
  244. .format('\n\t'.join('{}'.format(result) for result in failed_results)))
  245. self.context.log.info('You can fix this by editing the target in each FLOATING BUILD file '
  246. 'listed above to include a `rev` parameter that points to a sha, tag '
  247. 'or commit id that pins the code in the source repository to a fixed, '
  248. 'non-FLOATING version.')
  249. raise self.FloatingRemoteError('Un-pinned (FLOATING) Go remote libraries detected.')
  250. class NoLocalRootsError(TaskError):
  251. """Indicates the Go local source owning targets' source roots are invalid."""
  252. class InvalidLocalRootsError(TaskError):
  253. """Indicates the Go local source owning targets' source roots are invalid."""
  254. class UnrootedLocalSourceError(TaskError):
  255. """Indicates there are Go local source owning targets that fall outside the source root."""
  256. class InvalidRemoteRootsError(TaskError):
  257. """Indicates the Go remote library source roots are invalid."""
  258. class GenerationError(TaskError):
  259. """Indicates an error generating Go targets."""
  260. def __init__(self, cause):
  261. super(GoBuildgen.GenerationError, self).__init__(str(cause))
  262. self.cause = cause
  263. class GenerationResult(namedtuple('GenerationResult', ['generated',
  264. 'local_root',
  265. 'remote_root'])):
  266. """Captures the result of a Go target generation round."""
  267. def generate_targets(self, local_go_targets=None):
  268. """Generate Go targets in memory to form a complete Go graph.
  269. :param local_go_targets: The local Go targets to fill in a complete target graph for. If
  270. `None`, then all local Go targets under the Go source root are used.
  271. :type local_go_targets: :class:`collections.Iterable` of
  272. :class:`pants.contrib.go.targets.go_local_source import GoLocalSource`
  273. :returns: A generation result if targets were generated, else `None`.
  274. :rtype: :class:`GoBuildgen.GenerationResult`
  275. """
  276. # TODO(John Sirois): support multiple source roots like GOPATH does?
  277. # The GOPATH's 1st element is read-write, the rest are read-only; ie: their sources build to
  278. # the 1st element's pkg/ and bin/ dirs.
  279. # TODO: Add "find source roots for lang" functionality to SourceRoots and use that instead.
  280. all_roots = list(self.context.source_roots.all_roots())
  281. local_roots = [sr.path for sr in all_roots if 'go' in sr.langs]
  282. if not local_roots:
  283. raise self.NoLocalRootsError('Can only BUILD gen if a Go local sources source root is '
  284. 'defined.')
  285. if len(local_roots) > 1:
  286. raise self.InvalidLocalRootsError('Can only BUILD gen for a single Go local sources source '
  287. 'root, found:\n\t{}'
  288. .format('\n\t'.join(sorted(local_roots))))
  289. local_root = local_roots.pop()
  290. if local_go_targets:
  291. unrooted_locals = {t for t in local_go_targets if t.target_base != local_root}
  292. if unrooted_locals:
  293. raise self.UnrootedLocalSourceError('Cannot BUILD gen until the following targets are '
  294. 'relocated to the source root at {}:\n\t{}'
  295. .format(local_root,
  296. '\n\t'.join(sorted(t.address.reference()
  297. for t in unrooted_locals))))
  298. else:
  299. root = os.path.join(get_buildroot(), local_root)
  300. local_go_targets = self.context.scan(root=root).targets(self.is_local_src)
  301. if not local_go_targets:
  302. return None
  303. remote_roots = [sr.path for sr in all_roots if 'go_remote' in sr.langs]
  304. if len(remote_roots) > 1:
  305. raise self.InvalidRemoteRootsError('Can only BUILD gen for a single Go remote library source '
  306. 'root, found:\n\t{}'
  307. .format('\n\t'.join(sorted(remote_roots))))
  308. remote_root = remote_roots.pop() if remote_roots else None
  309. generator = GoTargetGenerator(self.import_oracle,
  310. self.context.build_graph,
  311. local_root,
  312. self.get_fetcher_factory(),
  313. generate_remotes=self.get_options().remote,
  314. remote_root=remote_root)
  315. with self.context.new_workunit('go.buildgen', labels=[WorkUnitLabel.MULTITOOL]):
  316. try:
  317. generated = generator.generate(local_go_targets)
  318. return self.GenerationResult(generated=generated,
  319. local_root=local_root,
  320. remote_root=remote_root)
  321. except generator.GenerationError as e:
  322. raise self.GenerationError(e)
  323. def get_fetcher_factory(self):
  324. return FetcherFactory.global_instance()
  325. def generate_build_files(self, targets):
  326. goal_name = self.options_scope
  327. flags = '--materialize'
  328. if self.get_options().remote:
  329. flags += ' --remote'
  330. template_header = dedent("""\
  331. # Auto-generated by pants!
  332. # To re-generate run: `pants {goal_name} {flags}`
  333. """).format(goal_name=goal_name, flags=flags)
  334. template_text = template_header + self.get_options().template
  335. build_file_basename = 'BUILD' + self.get_options().extension
  336. targets_by_spec_path = defaultdict(set)
  337. for target in targets:
  338. targets_by_spec_path[target.address.spec_path].add(target)
  339. for spec_path, targets in targets_by_spec_path.items():
  340. rel_path = os.path.join(spec_path, build_file_basename)
  341. result = self._create_template_data(rel_path, list(targets))
  342. if result:
  343. generator = Generator(template_text, target=result.data)
  344. build_file_path = os.path.join(get_buildroot(), rel_path)
  345. with safe_open(build_file_path, mode='w') as fp:
  346. generator.write(stream=fp)
  347. yield result
  348. class NonUniformRemoteRevsError(TaskError):
  349. """Indicates packages with mis-matched versions are defined for a single remote root."""
  350. def _create_template_data(self, build_file_path, targets):
  351. if len(targets) == 1 and self.is_local_src(targets[0]):
  352. local_target = targets[0]
  353. data = self._data(target_type='go_binary' if self.is_binary(local_target) else 'go_library',
  354. deps=[d.address.reference() for d in local_target.dependencies])
  355. return self.TemplateResult.local_target(build_file_path=build_file_path,
  356. data=data,
  357. import_paths=[local_target.import_path])
  358. elif self.get_options().remote:
  359. fail_floating = self.get_options().fail_floating
  360. if len(targets) == 1 and not targets[0].pkg:
  361. remote_lib = targets[0]
  362. rev = remote_lib.rev
  363. data = self._data(target_type='go_remote_library', rev=rev)
  364. import_paths = (remote_lib.import_path,)
  365. return self.TemplateResult.remote_target(build_file_path=build_file_path,
  366. data=data,
  367. import_paths=import_paths,
  368. rev=rev,
  369. fail_floating=fail_floating)
  370. else:
  371. revs = {t.rev for t in targets if t.rev}
  372. if len(revs) > 1:
  373. msg = ('Cannot create BUILD file {} for the following packages at remote root {}, '
  374. 'they must all have the same version:\n\t{}'
  375. .format(build_file_path, targets[0].remote_root,
  376. '\n\t'.join('{} {}'.format(t.pkg, t.rev) for t in targets)))
  377. raise self.NonUniformRemoteRevsError(msg)
  378. rev = revs.pop() if revs else None
  379. data = self._data(target_type='go_remote_libraries',
  380. rev=rev,
  381. pkgs=sorted({t.pkg for t in targets}))
  382. import_paths = tuple(t.import_path for t in targets)
  383. return self.TemplateResult.remote_target(build_file_path=build_file_path,
  384. data=data,
  385. import_paths=import_paths,
  386. rev=rev,
  387. fail_floating=fail_floating)
  388. else:
  389. return None
  390. def _data(self, target_type, deps=None, rev=None, pkgs=None):
  391. parameters = TemplateData(deps=deps, rev=rev, pkgs=pkgs) if (deps or rev or pkgs) else None
  392. return TemplateData(type=target_type, parameters=parameters)