forked from pantsbuild/pants
-
Notifications
You must be signed in to change notification settings - Fork 0
/
zinc_compile.py
649 lines (544 loc) · 28.2 KB
/
zinc_compile.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
# coding=utf-8
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import absolute_import, division, print_function, unicode_literals
import errno
import logging
import os
import re
import textwrap
from builtins import open
from collections import defaultdict
from contextlib import closing
from xml.etree import ElementTree
from future.utils import PY2, text_type
from pants.backend.jvm.subsystems.java import Java
from pants.backend.jvm.subsystems.jvm_platform import JvmPlatform
from pants.backend.jvm.subsystems.scala_platform import ScalaPlatform
from pants.backend.jvm.subsystems.zinc import Zinc
from pants.backend.jvm.targets.jvm_target import JvmTarget
from pants.backend.jvm.targets.scalac_plugin import ScalacPlugin
from pants.backend.jvm.tasks.classpath_util import ClasspathUtil
from pants.backend.jvm.tasks.jvm_compile.jvm_compile import JvmCompile
from pants.base.build_environment import get_buildroot
from pants.base.exceptions import TaskError
from pants.base.hash_utils import hash_file
from pants.base.workunit import WorkUnitLabel
from pants.engine.fs import DirectoryToMaterialize
from pants.engine.isolated_process import ExecuteProcessRequest
from pants.util.contextutil import open_zip
from pants.util.dirutil import fast_relpath
from pants.util.memo import memoized_method, memoized_property
from pants.util.meta import classproperty
from pants.util.strutil import ensure_text, safe_shlex_join
# Well known metadata file required to register scalac plugins with nsc.
_SCALAC_PLUGIN_INFO_FILE = 'scalac-plugin.xml'
logger = logging.getLogger(__name__)
class BaseZincCompile(JvmCompile):
"""An abstract base class for zinc compilation tasks."""
_name = 'zinc'
@staticmethod
def validate_arguments(log, whitelisted_args, args):
"""Validate that all arguments match whitelisted regexes."""
valid_patterns = {re.compile(p): v for p, v in whitelisted_args.items()}
def validate(idx):
arg = args[idx]
for pattern, has_argument in valid_patterns.items():
if pattern.match(arg):
return 2 if has_argument else 1
log.warn("Zinc argument '{}' is not supported, and is subject to change/removal!".format(arg))
return 1
arg_index = 0
while arg_index < len(args):
arg_index += validate(arg_index)
def _get_zinc_arguments(self, settings):
distribution = self._get_jvm_distribution()
return self._format_zinc_arguments(settings, distribution)
@staticmethod
def _format_zinc_arguments(settings, distribution):
"""Extracts and formats the zinc arguments given in the jvm platform settings.
This is responsible for the symbol substitution which replaces $JAVA_HOME with the path to an
appropriate jvm distribution.
:param settings: The jvm platform settings from which to extract the arguments.
:type settings: :class:`JvmPlatformSettings`
"""
zinc_args = [
'-C-source', '-C{}'.format(settings.source_level),
'-C-target', '-C{}'.format(settings.target_level),
]
if settings.args:
settings_args = settings.args
if any('$JAVA_HOME' in a for a in settings.args):
logger.debug('Substituting "$JAVA_HOME" with "{}" in jvm-platform args.'
.format(distribution.home))
settings_args = (a.replace('$JAVA_HOME', distribution.home) for a in settings.args)
zinc_args.extend(settings_args)
return zinc_args
@classmethod
def implementation_version(cls):
return super(BaseZincCompile, cls).implementation_version() + [('BaseZincCompile', 7)]
@classmethod
def get_jvm_options_default(cls, bootstrap_option_values):
return ('-Dfile.encoding=UTF-8', '-Dzinc.analysis.cache.limit=1000',
'-Djava.awt.headless=true', '-Xmx2g')
@classmethod
def get_args_default(cls, bootstrap_option_values):
return ('-C-encoding', '-CUTF-8', '-S-encoding', '-SUTF-8', '-S-g:vars')
@classmethod
def get_warning_args_default(cls):
return ('-C-deprecation', '-C-Xlint:all', '-C-Xlint:-serial', '-C-Xlint:-path',
'-S-deprecation', '-S-unchecked', '-S-Xlint')
@classmethod
def get_no_warning_args_default(cls):
return ('-C-nowarn', '-C-Xlint:none', '-S-nowarn', '-S-Xlint:none', )
@classproperty
def get_fatal_warnings_enabled_args_default(cls):
return ('-S-Xfatal-warnings', '-C-Werror')
@classproperty
def get_compiler_option_sets_enabled_default_value(cls):
return {'fatal_warnings': cls.get_fatal_warnings_enabled_args_default}
@classmethod
def register_options(cls, register):
super(BaseZincCompile, cls).register_options(register)
register('--whitelisted-args', advanced=True, type=dict,
default={
'-S.*': False,
'-C.*': False,
'-file-filter': True,
'-msg-filter': True,
},
help='A dict of option regexes that make up pants\' supported API for zinc. '
'Options not listed here are subject to change/removal. The value of the dict '
'indicates that an option accepts an argument.')
register('--incremental', advanced=True, type=bool, default=True,
help='When set, zinc will use sub-target incremental compilation, which dramatically '
'improves compile performance while changing large targets. When unset, '
'changed targets will be compiled with an empty output directory, as if after '
'running clean-all.')
register('--incremental-caching', advanced=True, type=bool,
help='When set, the results of incremental compiles will be written to the cache. '
'This is unset by default, because it is generally a good precaution to cache '
'only clean/cold builds.')
@classmethod
def subsystem_dependencies(cls):
return super(BaseZincCompile, cls).subsystem_dependencies() + (Zinc.Factory, JvmPlatform,)
@classmethod
def prepare(cls, options, round_manager):
super(BaseZincCompile, cls).prepare(options, round_manager)
ScalaPlatform.prepare_tools(round_manager)
@property
def incremental(self):
"""Zinc implements incremental compilation.
Setting this property causes the task infrastructure to clone the previous
results_dir for a target into the new results_dir for a target.
"""
return self.get_options().incremental
@property
def cache_incremental(self):
"""Optionally write the results of incremental compiles to the cache."""
return self.get_options().incremental_caching
@memoized_property
def _zinc(self):
return Zinc.Factory.global_instance().create(self.context.products, self.execution_strategy)
def __init__(self, *args, **kwargs):
super(BaseZincCompile, self).__init__(*args, **kwargs)
# A directory to contain per-target subdirectories with apt processor info files.
self._processor_info_dir = os.path.join(self.workdir, 'apt-processor-info')
# Validate zinc options.
ZincCompile.validate_arguments(self.context.log, self.get_options().whitelisted_args,
self._args)
if self.execution_strategy == self.HERMETIC:
# TODO: Make incremental compiles work. See:
# hermetically https://github.com/pantsbuild/pants/issues/6517
if self.get_options().incremental:
raise TaskError("Hermetic zinc execution does not currently support incremental compiles. "
"Please use --no-{}-incremental."
.format(self.get_options_scope_equivalent_flag_component()))
try:
fast_relpath(self.get_options().pants_workdir, get_buildroot())
except ValueError:
raise TaskError(
"Hermetic zinc execution currently requires the workdir to be a child of the buildroot "
"but workdir was {} and buildroot was {}".format(
self.get_options().pants_workdir,
get_buildroot(),
)
)
def select(self, target):
raise NotImplementedError()
def select_source(self, source_file_path):
raise NotImplementedError()
def register_extra_products_from_contexts(self, targets, compile_contexts):
compile_contexts = [self.select_runtime_context(compile_contexts[t]) for t in targets]
zinc_analysis = self.context.products.get_data('zinc_analysis')
zinc_args = self.context.products.get_data('zinc_args')
if zinc_analysis is not None:
for compile_context in compile_contexts:
zinc_analysis[compile_context.target] = (compile_context.classes_dir.path,
compile_context.jar_file.path,
compile_context.analysis_file)
if zinc_args is not None:
for compile_context in compile_contexts:
with open(compile_context.zinc_args_file, 'r') as fp:
args = fp.read().split()
zinc_args[compile_context.target] = args
def create_empty_extra_products(self):
if self.context.products.is_required_data('zinc_analysis'):
self.context.products.safe_create_data('zinc_analysis', dict)
if self.context.products.is_required_data('zinc_args'):
self.context.products.safe_create_data('zinc_args', lambda: defaultdict(list))
def extra_resources(self, compile_context):
"""Override `extra_resources` to additionally include scalac_plugin info."""
result = super(BaseZincCompile, self).extra_resources(compile_context)
target = compile_context.target
if isinstance(target, ScalacPlugin):
result[_SCALAC_PLUGIN_INFO_FILE] = textwrap.dedent("""
<plugin>
<name>{}</name>
<classname>{}</classname>
</plugin>
""".format(target.plugin, target.classname))
return result
def javac_classpath(self):
# Note that if this classpath is empty then Zinc will automatically use the javac from
# the JDK it was invoked with.
return Java.global_javac_classpath(self.context.products)
def scalac_classpath_entries(self):
"""Returns classpath entries for the scalac classpath."""
return ScalaPlatform.global_instance().compiler_classpath_entries(
self.context.products, self.context._scheduler)
def compile(self, ctx, args, dependency_classpath, upstream_analysis,
settings, compiler_option_sets, zinc_file_manager,
javac_plugin_map, scalac_plugin_map):
absolute_classpath = (ctx.classes_dir.path,) + tuple(ce.path for ce in dependency_classpath)
if self.get_options().capture_classpath:
self._record_compile_classpath(absolute_classpath, ctx.target, ctx.classes_dir.path)
self._verify_zinc_classpath(absolute_classpath, allow_dist=(self.execution_strategy != self.HERMETIC))
# TODO: Investigate upstream_analysis for hermetic compiles
self._verify_zinc_classpath(upstream_analysis.keys())
def relative_to_exec_root(path):
# TODO: Support workdirs not nested under buildroot by path-rewriting.
return fast_relpath(path, get_buildroot())
analysis_cache = relative_to_exec_root(ctx.analysis_file)
classes_dir = relative_to_exec_root(ctx.classes_dir.path)
jar_file = relative_to_exec_root(ctx.jar_file.path)
# TODO: Have these produced correctly, rather than having to relativize them here
relative_classpath = tuple(relative_to_exec_root(c) for c in absolute_classpath)
# list of classpath entries
scalac_classpath_entries = self.scalac_classpath_entries()
scala_path = [classpath_entry.path for classpath_entry in scalac_classpath_entries]
zinc_args = []
zinc_args.extend([
'-log-level', self.get_options().level,
'-analysis-cache', analysis_cache,
'-classpath', ':'.join(relative_classpath),
'-d', classes_dir,
'-jar', jar_file,
])
if not self.get_options().colors:
zinc_args.append('-no-color')
compiler_bridge_classpath_entry = self._zinc.compile_compiler_bridge(self.context)
zinc_args.extend(['-compiled-bridge-jar', relative_to_exec_root(compiler_bridge_classpath_entry.path)])
zinc_args.extend(['-scala-path', ':'.join(scala_path)])
zinc_args.extend(self._javac_plugin_args(javac_plugin_map))
# Search for scalac plugins on the classpath.
# Note that:
# - We also search in the extra scalac plugin dependencies, if specified.
# - In scala 2.11 and up, the plugin's classpath element can be a dir, but for 2.10 it must be
# a jar. So in-repo plugins will only work with 2.10 if --use-classpath-jars is true.
# - We exclude our own classes_dir/jar_file, because if we're a plugin ourselves, then our
# classes_dir doesn't have scalac-plugin.xml yet, and we don't want that fact to get
# memoized (which in practice will only happen if this plugin uses some other plugin, thus
# triggering the plugin search mechanism, which does the memoizing).
scalac_plugin_search_classpath = (
(set(absolute_classpath) | set(self.scalac_plugin_classpath_elements())) -
{ctx.classes_dir.path, ctx.jar_file.path}
)
zinc_args.extend(self._scalac_plugin_args(scalac_plugin_map, scalac_plugin_search_classpath))
if upstream_analysis:
zinc_args.extend(['-analysis-map',
','.join('{}:{}'.format(
relative_to_exec_root(k),
relative_to_exec_root(v)
) for k, v in upstream_analysis.items())])
zinc_args.extend(args)
zinc_args.extend(self._get_zinc_arguments(settings))
zinc_args.append('-transactional')
compiler_option_sets_args = self.get_merged_args_for_compiler_option_sets(compiler_option_sets)
zinc_args.extend(compiler_option_sets_args)
if not self._clear_invalid_analysis:
zinc_args.append('-no-clear-invalid-analysis')
if not zinc_file_manager:
zinc_args.append('-no-zinc-file-manager')
jvm_options = []
if self.javac_classpath():
# Make the custom javac classpath the first thing on the bootclasspath, to ensure that
# it's the one javax.tools.ToolProvider.getSystemJavaCompiler() loads.
# It will probably be loaded even on the regular classpath: If not found on the bootclasspath,
# getSystemJavaCompiler() constructs a classloader that loads from the JDK's tools.jar.
# That classloader will first delegate to its parent classloader, which will search the
# regular classpath. However it's harder to guarantee that our javac will preceed any others
# on the classpath, so it's safer to prefix it to the bootclasspath.
jvm_options.extend(['-Xbootclasspath/p:{}'.format(':'.join(self.javac_classpath()))])
jvm_options.extend(self._jvm_options)
zinc_args.extend(ctx.sources)
self.log_zinc_file(ctx.analysis_file)
with open(ctx.zinc_args_file, 'w') as fp:
for arg in zinc_args:
# NB: in Python 2, options are stored sometimes as bytes and sometimes as unicode in the OptionValueContainer.
# This is due to how Python 2 natively stores attributes as a map of `str` (aka `bytes`) to their value. So,
# the setattr() and getattr() functions sometimes use bytes.
if PY2:
arg = ensure_text(arg)
fp.write(arg)
fp.write('\n')
return self.execution_strategy_enum.resolve_for_enum_variant({
self.HERMETIC: lambda: self._compile_hermetic(
jvm_options, ctx, classes_dir, jar_file, zinc_args, compiler_bridge_classpath_entry,
dependency_classpath, scalac_classpath_entries),
self.SUBPROCESS: lambda: self._compile_nonhermetic(jvm_options, ctx, classes_dir, zinc_args),
self.NAILGUN: lambda: self._compile_nonhermetic(jvm_options, ctx, classes_dir, zinc_args),
})()
class ZincCompileError(TaskError):
"""An exception type specifically to signal a failed zinc execution."""
def _compile_nonhermetic(self, jvm_options, ctx, classes_directory, zinc_args):
exit_code = self.runjava(classpath=self.get_zinc_compiler_classpath(),
main=Zinc.ZINC_COMPILE_MAIN,
jvm_options=jvm_options,
args=zinc_args,
workunit_name=self.name(),
workunit_labels=[WorkUnitLabel.COMPILER],
dist=self._zinc.dist)
if exit_code != 0:
raise self.ZincCompileError('Zinc compile failed.', exit_code=exit_code)
self.context._scheduler.materialize_directories((
DirectoryToMaterialize(text_type(classes_directory), self.extra_resources_digest(ctx)),
))
def _compile_hermetic(self, jvm_options, ctx, classes_dir, jar_file, zinc_args,
compiler_bridge_classpath_entry, dependency_classpath,
scalac_classpath_entries):
zinc_relpath = fast_relpath(self._zinc.zinc, get_buildroot())
snapshots = [
self._zinc.snapshot(self.context._scheduler),
ctx.target.sources_snapshot(self.context._scheduler),
]
# scala_library() targets with java_sources have circular dependencies on those java source
# files, and we provide them to the same zinc command line that compiles the scala, so we need
# to make sure those source files are available in the hermetic execution sandbox.
java_sources_targets = getattr(ctx.target, 'java_sources', [])
java_sources_snapshots = [
tgt.sources_snapshot(self.context._scheduler)
for tgt in java_sources_targets
]
snapshots.extend(java_sources_snapshots)
# Ensure the dependencies and compiler bridge jars are available in the execution sandbox.
relevant_classpath_entries = dependency_classpath + [compiler_bridge_classpath_entry]
directory_digests = tuple(
entry.directory_digest for entry in relevant_classpath_entries if entry.directory_digest
)
if len(directory_digests) != len(relevant_classpath_entries):
for dep in relevant_classpath_entries:
if dep.directory_digest is None:
logger.warning(
"ClasspathEntry {} didn't have a Digest, so won't be present for hermetic "
"execution".format(dep)
)
snapshots.extend(
classpath_entry.directory_digest for classpath_entry in scalac_classpath_entries
)
if self._zinc.use_native_image:
if jvm_options:
raise ValueError(
"`{}` got non-empty jvm_options when running with a graal native-image, but this is "
"unsupported. jvm_options received: {}".format(self.options_scope, safe_shlex_join(jvm_options))
)
native_image_path, native_image_snapshot = self._zinc.native_image(self.context)
native_image_snapshots = (native_image_snapshot.directory_digest,)
scala_boot_classpath = [
classpath_entry.path for classpath_entry in scalac_classpath_entries
] + [
# We include rt.jar on the scala boot classpath because the compiler usually gets its
# contents from the VM it is executing in, but not in the case of a native image. This
# resolves a `object java.lang.Object in compiler mirror not found.` error.
'.jdk/jre/lib/rt.jar',
# The same goes for the jce.jar, which provides javax.crypto.
'.jdk/jre/lib/jce.jar',
]
image_specific_argv = [
native_image_path,
'-java-home', '.jdk',
'-Dscala.boot.class.path={}'.format(os.pathsep.join(scala_boot_classpath)),
'-Dscala.usejavacp=true',
]
else:
# TODO: Extract something common from Executor._create_command to make the command line
# TODO: Lean on distribution for the bin/java appending here
native_image_snapshots = ()
image_specific_argv = ['.jdk/bin/java'] + jvm_options + [
'-cp', zinc_relpath,
Zinc.ZINC_COMPILE_MAIN
]
merged_input_digest = self.context._scheduler.merge_directories(
tuple(s.directory_digest for s in snapshots) +
directory_digests +
native_image_snapshots +
(self.extra_resources_digest(ctx),)
)
argv = image_specific_argv + zinc_args
# TODO(#6071): Our ExecuteProcessRequest expects a specific string type for arguments,
# which py2 doesn't default to. This can be removed when we drop python 2.
argv = [text_type(arg) for arg in argv]
req = ExecuteProcessRequest(
argv=tuple(argv),
input_files=merged_input_digest,
output_files=(jar_file,) if self.get_options().use_classpath_jars else (),
output_directories=() if self.get_options().use_classpath_jars else (classes_dir,),
description="zinc compile for {}".format(ctx.target.address.spec),
jdk_home=self._zinc.underlying_dist.home,
)
res = self.context.execute_process_synchronously_or_raise(
req, self.name(), [WorkUnitLabel.COMPILER])
# TODO: Materialize as a batch in do_compile or somewhere
self.context._scheduler.materialize_directories((
DirectoryToMaterialize(get_buildroot(), res.output_directory_digest),
))
# TODO: This should probably return a ClasspathEntry rather than a Digest
return res.output_directory_digest
def get_zinc_compiler_classpath(self):
"""Get the classpath for the zinc compiler JVM tool.
This will just be the zinc compiler tool classpath normally, but tasks which invoke zinc along
with other JVM tools with nailgun (such as RscCompile) require zinc to be invoked with this
method to ensure a single classpath is used for all the tools they need to invoke so that the
nailgun instance (which is keyed by classpath and JVM options) isn't invalidated.
"""
return [self._zinc.zinc]
def _verify_zinc_classpath(self, classpath, allow_dist=True):
def is_outside(path, putative_parent):
return os.path.relpath(path, putative_parent).startswith(os.pardir)
dist = self._zinc.dist
for path in classpath:
if not os.path.isabs(path):
raise TaskError('Classpath entries provided to zinc should be absolute. '
'{} is not.'.format(path))
if is_outside(path, self.get_options().pants_workdir) and (not allow_dist or is_outside(path, dist.home)):
raise TaskError('Classpath entries provided to zinc should be in working directory or '
'part of the JDK. {} is not.'.format(path))
if path != os.path.normpath(path):
raise TaskError('Classpath entries provided to zinc should be normalized '
'(i.e. without ".." and "."). {} is not.'.format(path))
def log_zinc_file(self, analysis_file):
self.context.log.debug('Calling zinc on: {} ({})'
.format(analysis_file,
hash_file(analysis_file).upper()
if os.path.exists(analysis_file)
else 'nonexistent'))
@classmethod
def _javac_plugin_args(cls, javac_plugin_map):
ret = []
for plugin, args in javac_plugin_map.items():
for arg in args:
if ' ' in arg:
# Note: Args are separated by spaces, and there is no way to escape embedded spaces, as
# javac's Main does a simple split on these strings.
raise TaskError('javac plugin args must not contain spaces '
'(arg {} for plugin {})'.format(arg, plugin))
ret.append('-C-Xplugin:{} {}'.format(plugin, ' '.join(args)))
return ret
def _scalac_plugin_args(self, scalac_plugin_map, classpath):
if not scalac_plugin_map:
return []
plugin_jar_map = self._find_scalac_plugins(list(scalac_plugin_map.keys()), classpath)
ret = []
for name, cp_entries in plugin_jar_map.items():
# Note that the first element in cp_entries is the one containing the plugin's metadata,
# meaning that this is the plugin that will be loaded, even if there happen to be other
# plugins in the list of entries (e.g., because this plugin depends on another plugin).
ret.append('-S-Xplugin:{}'.format(':'.join(cp_entries)))
for arg in scalac_plugin_map[name]:
ret.append('-S-P:{}:{}'.format(name, arg))
return ret
def _find_scalac_plugins(self, scalac_plugins, classpath):
"""Returns a map from plugin name to list of plugin classpath entries.
The first entry in each list is the classpath entry containing the plugin metadata.
The rest are the internal transitive deps of the plugin.
This allows us to have in-repo plugins with dependencies (unlike javac, scalac doesn't load
plugins or their deps from the regular classpath, so we have to provide these entries
separately, in the -Xplugin: flag).
Note that we don't currently support external plugins with dependencies, as we can't know which
external classpath elements are required, and we'd have to put the entire external classpath
on each -Xplugin: flag, which seems excessive.
Instead, external plugins should be published as "fat jars" (which appears to be the norm,
since SBT doesn't support plugins with dependencies anyway).
"""
# Allow multiple flags and also comma-separated values in a single flag.
plugin_names = {p for val in scalac_plugins for p in val.split(',')}
if not plugin_names:
return {}
active_plugins = {}
buildroot = get_buildroot()
cp_product = self.context.products.get_data('runtime_classpath')
for classpath_element in classpath:
name = self._maybe_get_plugin_name(classpath_element)
if name in plugin_names:
plugin_target_closure = self._plugin_targets('scalac').get(name, [])
# It's important to use relative paths, as the compiler flags get embedded in the zinc
# analysis file, and we port those between systems via the artifact cache.
rel_classpath_elements = [
os.path.relpath(cpe, buildroot) for cpe in
ClasspathUtil.internal_classpath(plugin_target_closure, cp_product, self._confs)]
# If the plugin is external then rel_classpath_elements will be empty, so we take
# just the external jar itself.
rel_classpath_elements = rel_classpath_elements or [classpath_element]
# Some classpath elements may be repeated, so we allow for that here.
if active_plugins.get(name, rel_classpath_elements) != rel_classpath_elements:
raise TaskError('Plugin {} defined in {} and in {}'.format(name, active_plugins[name],
classpath_element))
active_plugins[name] = rel_classpath_elements
if len(active_plugins) == len(plugin_names):
# We've found all the plugins, so return now to spare us from processing
# of the rest of the classpath for no reason.
return active_plugins
# If we get here we must have unresolved plugins.
unresolved_plugins = plugin_names - set(active_plugins.keys())
raise TaskError('Could not find requested plugins: {}'.format(list(unresolved_plugins)))
@classmethod
@memoized_method
def _maybe_get_plugin_name(cls, classpath_element):
"""If classpath_element is a scalac plugin, returns its name.
Returns None otherwise.
"""
def process_info_file(cp_elem, info_file):
plugin_info = ElementTree.parse(info_file).getroot()
if plugin_info.tag != 'plugin':
raise TaskError('File {} in {} is not a valid scalac plugin descriptor'.format(
_SCALAC_PLUGIN_INFO_FILE, cp_elem))
return plugin_info.find('name').text
if os.path.isdir(classpath_element):
try:
with open(os.path.join(classpath_element, _SCALAC_PLUGIN_INFO_FILE), 'r') as plugin_info_file:
return process_info_file(classpath_element, plugin_info_file)
except IOError as e:
if e.errno != errno.ENOENT:
raise
else:
with open_zip(classpath_element, 'r') as jarfile:
try:
with closing(jarfile.open(_SCALAC_PLUGIN_INFO_FILE, 'r')) as plugin_info_file:
return process_info_file(classpath_element, plugin_info_file)
except KeyError:
pass
return None
class ZincCompile(BaseZincCompile):
"""Compile Scala and Java code to classfiles using Zinc."""
compiler_name = 'zinc'
@classmethod
def product_types(cls):
return ['runtime_classpath', 'zinc_analysis', 'zinc_args']
def select(self, target):
# Require that targets are marked for JVM compilation, to differentiate from
# targets owned by the scalajs contrib module.
if not isinstance(target, JvmTarget):
return False
return target.has_sources('.java') or target.has_sources('.scala')
def select_source(self, source_file_path):
return source_file_path.endswith('.java') or source_file_path.endswith('.scala')