waf: upgrade to 2.0.18
[metze/samba/wip.git] / third_party / waf / waflib / Tools / javaw.py
1 #!/usr/bin/env python
2 # encoding: utf-8
3 # Thomas Nagy, 2006-2018 (ita)
4
5 """
6 Java support
7
8 Javac is one of the few compilers that behaves very badly:
9
10 #. it outputs files where it wants to (-d is only for the package root)
11
12 #. it recompiles files silently behind your back
13
14 #. it outputs an undefined amount of files (inner classes)
15
16 Remember that the compilation can be performed using Jython[1] rather than regular Python. Instead of
17 running one of the following commands::
18
19    ./waf configure
20    python waf configure
21
22 You would have to run::
23
24    java -jar /path/to/jython.jar waf configure
25
26 [1] http://www.jython.org/
27
28 Usage
29 =====
30
31 Load the "java" tool.
32
33 def configure(conf):
34         conf.load('java')
35
36 Java tools will be autodetected and eventually, if present, the quite
37 standard JAVA_HOME environment variable will be used. The also standard
38 CLASSPATH variable is used for library searching.
39
40 In configuration phase checks can be done on the system environment, for
41 example to check if a class is known in the classpath::
42
43         conf.check_java_class('java.io.FileOutputStream')
44
45 or if the system supports JNI applications building::
46
47         conf.check_jni_headers()
48
49
50 The java tool supports compiling java code, creating jar files and
51 creating javadoc documentation. This can be either done separately or
52 together in a single definition. For example to manage them separately::
53
54         bld(features  = 'javac',
55                 srcdir    = 'src',
56                 compat    = '1.7',
57                 use       = 'animals',
58                 name      = 'cats-src',
59         )
60
61         bld(features  = 'jar',
62                 basedir   = '.',
63                 destfile  = '../cats.jar',
64                 name      = 'cats',
65                 use       = 'cats-src'
66         )
67
68
69 Or together by defining all the needed attributes::
70
71         bld(features   = 'javac jar javadoc',
72                 srcdir     = 'src/',  # folder containing the sources to compile
73                 outdir     = 'src',   # folder where to output the classes (in the build directory)
74                 compat     = '1.6',   # java compatibility version number
75                 classpath  = ['.', '..'],
76
77                 # jar
78                 basedir    = 'src', # folder containing the classes and other files to package (must match outdir)
79                 destfile   = 'foo.jar', # do not put the destfile in the folder of the java classes!
80                 use        = 'NNN',
81                 jaropts    = ['-C', 'default/src/', '.'], # can be used to give files
82                 manifest   = 'src/Manifest.mf', # Manifest file to include
83
84                 # javadoc
85                 javadoc_package = ['com.meow' , 'com.meow.truc.bar', 'com.meow.truc.foo'],
86                 javadoc_output  = 'javadoc',
87         )
88
89 External jar dependencies can be mapped to a standard waf "use" dependency by
90 setting an environment variable with a CLASSPATH prefix in the configuration,
91 for example::
92
93         conf.env.CLASSPATH_NNN = ['aaaa.jar', 'bbbb.jar']
94
95 and then NNN can be freely used in rules as::
96
97         use        = 'NNN',
98
99 In the java tool the dependencies via use are not transitive by default, as
100 this necessity depends on the code. To enable recursive dependency scanning
101 use on a specific rule:
102
103                 recurse_use = True
104
105 Or build-wise by setting RECURSE_JAVA:
106
107                 bld.env.RECURSE_JAVA = True
108
109 Unit tests can be integrated in the waf unit test environment using the javatest extra.
110 """
111
112 import os, shutil
113 from waflib import Task, Utils, Errors, Node
114 from waflib.Configure import conf
115 from waflib.TaskGen import feature, before_method, after_method, taskgen_method
116
117 from waflib.Tools import ccroot
118 ccroot.USELIB_VARS['javac'] = set(['CLASSPATH', 'JAVACFLAGS'])
119
120 SOURCE_RE = '**/*.java'
121 JAR_RE = '**/*'
122
123 class_check_source = '''
124 public class Test {
125         public static void main(String[] argv) {
126                 Class lib;
127                 if (argv.length < 1) {
128                         System.err.println("Missing argument");
129                         System.exit(77);
130                 }
131                 try {
132                         lib = Class.forName(argv[0]);
133                 } catch (ClassNotFoundException e) {
134                         System.err.println("ClassNotFoundException");
135                         System.exit(1);
136                 }
137                 lib = null;
138                 System.exit(0);
139         }
140 }
141 '''
142
143 @feature('javac')
144 @before_method('process_source')
145 def apply_java(self):
146         """
147         Create a javac task for compiling *.java files*. There can be
148         only one javac task by task generator.
149         """
150         Utils.def_attrs(self, jarname='', classpath='',
151                 sourcepath='.', srcdir='.',
152                 jar_mf_attributes={}, jar_mf_classpath=[])
153
154         outdir = getattr(self, 'outdir', None)
155         if outdir:
156                 if not isinstance(outdir, Node.Node):
157                         outdir = self.path.get_bld().make_node(self.outdir)
158         else:
159                 outdir = self.path.get_bld()
160         outdir.mkdir()
161         self.outdir = outdir
162         self.env.OUTDIR = outdir.abspath()
163
164         self.javac_task = tsk = self.create_task('javac')
165         tmp = []
166
167         srcdir = getattr(self, 'srcdir', '')
168         if isinstance(srcdir, Node.Node):
169                 srcdir = [srcdir]
170         for x in Utils.to_list(srcdir):
171                 if isinstance(x, Node.Node):
172                         y = x
173                 else:
174                         y = self.path.find_dir(x)
175                         if not y:
176                                 self.bld.fatal('Could not find the folder %s from %s' % (x, self.path))
177                 tmp.append(y)
178
179         tsk.srcdir = tmp
180
181         if getattr(self, 'compat', None):
182                 tsk.env.append_value('JAVACFLAGS', ['-source', str(self.compat)])
183
184         if hasattr(self, 'sourcepath'):
185                 fold = [isinstance(x, Node.Node) and x or self.path.find_dir(x) for x in self.to_list(self.sourcepath)]
186                 names = os.pathsep.join([x.srcpath() for x in fold])
187         else:
188                 names = [x.srcpath() for x in tsk.srcdir]
189
190         if names:
191                 tsk.env.append_value('JAVACFLAGS', ['-sourcepath', names])
192
193
194 @taskgen_method
195 def java_use_rec(self, name, **kw):
196         """
197         Processes recursively the *use* attribute for each referred java compilation
198         """
199         if name in self.tmp_use_seen:
200                 return
201
202         self.tmp_use_seen.append(name)
203
204         try:
205                 y = self.bld.get_tgen_by_name(name)
206         except Errors.WafError:
207                 self.uselib.append(name)
208                 return
209         else:
210                 y.post()
211                 # Add generated JAR name for CLASSPATH. Task ordering (set_run_after)
212                 # is already guaranteed by ordering done between the single tasks
213                 if hasattr(y, 'jar_task'):
214                         self.use_lst.append(y.jar_task.outputs[0].abspath())
215                 else:
216                         if hasattr(y,'outdir'):
217                                 self.use_lst.append(y.outdir.abspath())
218                         else:
219                                 self.use_lst.append(y.path.get_bld().abspath())
220
221         for x in self.to_list(getattr(y, 'use', [])):
222                 self.java_use_rec(x)
223
224 @feature('javac')
225 @before_method('propagate_uselib_vars')
226 @after_method('apply_java')
227 def use_javac_files(self):
228         """
229         Processes the *use* attribute referring to other java compilations
230         """
231         self.use_lst = []
232         self.tmp_use_seen = []
233         self.uselib = self.to_list(getattr(self, 'uselib', []))
234         names = self.to_list(getattr(self, 'use', []))
235         get = self.bld.get_tgen_by_name
236         for x in names:
237                 try:
238                         tg = get(x)
239                 except Errors.WafError:
240                         self.uselib.append(x)
241                 else:
242                         tg.post()
243                         if hasattr(tg, 'jar_task'):
244                                 self.use_lst.append(tg.jar_task.outputs[0].abspath())
245                                 self.javac_task.set_run_after(tg.jar_task)
246                                 self.javac_task.dep_nodes.extend(tg.jar_task.outputs)
247                         else:
248                                 if hasattr(tg, 'outdir'):
249                                         base_node = tg.outdir
250                                 else:
251                                         base_node = tg.path.get_bld()
252
253                                 self.use_lst.append(base_node.abspath())
254                                 self.javac_task.dep_nodes.extend([x for x in base_node.ant_glob(JAR_RE, remove=False, quiet=True)])
255
256                                 for tsk in tg.tasks:
257                                         self.javac_task.set_run_after(tsk)
258
259                 # If recurse use scan is enabled recursively add use attribute for each used one
260                 if getattr(self, 'recurse_use', False) or self.bld.env.RECURSE_JAVA:
261                         self.java_use_rec(x)
262
263         self.env.append_value('CLASSPATH', self.use_lst)
264
265 @feature('javac')
266 @after_method('apply_java', 'propagate_uselib_vars', 'use_javac_files')
267 def set_classpath(self):
268         """
269         Sets the CLASSPATH value on the *javac* task previously created.
270         """
271         if getattr(self, 'classpath', None):
272                 self.env.append_unique('CLASSPATH', getattr(self, 'classpath', []))
273         for x in self.tasks:
274                 x.env.CLASSPATH = os.pathsep.join(self.env.CLASSPATH) + os.pathsep
275
276 @feature('jar')
277 @after_method('apply_java', 'use_javac_files')
278 @before_method('process_source')
279 def jar_files(self):
280         """
281         Creates a jar task (one maximum per task generator)
282         """
283         destfile = getattr(self, 'destfile', 'test.jar')
284         jaropts = getattr(self, 'jaropts', [])
285         manifest = getattr(self, 'manifest', None)
286
287         basedir = getattr(self, 'basedir', None)
288         if basedir:
289                 if not isinstance(self.basedir, Node.Node):
290                         basedir = self.path.get_bld().make_node(basedir)
291         else:
292                 basedir = self.path.get_bld()
293         if not basedir:
294                 self.bld.fatal('Could not find the basedir %r for %r' % (self.basedir, self))
295
296         self.jar_task = tsk = self.create_task('jar_create')
297         if manifest:
298                 jarcreate = getattr(self, 'jarcreate', 'cfm')
299                 if not isinstance(manifest,Node.Node):
300                         node = self.path.find_resource(manifest)
301                 else:
302                         node = manifest
303                 if not node:
304                         self.bld.fatal('invalid manifest file %r for %r' % (manifest, self))
305                 tsk.dep_nodes.append(node)
306                 jaropts.insert(0, node.abspath())
307         else:
308                 jarcreate = getattr(self, 'jarcreate', 'cf')
309         if not isinstance(destfile, Node.Node):
310                 destfile = self.path.find_or_declare(destfile)
311         if not destfile:
312                 self.bld.fatal('invalid destfile %r for %r' % (destfile, self))
313         tsk.set_outputs(destfile)
314         tsk.basedir = basedir
315
316         jaropts.append('-C')
317         jaropts.append(basedir.bldpath())
318         jaropts.append('.')
319
320         tsk.env.JAROPTS = jaropts
321         tsk.env.JARCREATE = jarcreate
322
323         if getattr(self, 'javac_task', None):
324                 tsk.set_run_after(self.javac_task)
325
326 @feature('jar')
327 @after_method('jar_files')
328 def use_jar_files(self):
329         """
330         Processes the *use* attribute to set the build order on the
331         tasks created by another task generator.
332         """
333         self.uselib = self.to_list(getattr(self, 'uselib', []))
334         names = self.to_list(getattr(self, 'use', []))
335         get = self.bld.get_tgen_by_name
336         for x in names:
337                 try:
338                         y = get(x)
339                 except Errors.WafError:
340                         self.uselib.append(x)
341                 else:
342                         y.post()
343                         self.jar_task.run_after.update(y.tasks)
344
345 class JTask(Task.Task):
346         """
347         Base class for java and jar tasks; provides functionality to run long commands
348         """
349         def split_argfile(self, cmd):
350                 inline = [cmd[0]]
351                 infile = []
352                 for x in cmd[1:]:
353                         # jar and javac do not want -J flags in @file
354                         if x.startswith('-J'):
355                                 inline.append(x)
356                         else:
357                                 infile.append(self.quote_flag(x))
358                 return (inline, infile)
359
360 class jar_create(JTask):
361         """
362         Creates a jar file
363         """
364         color   = 'GREEN'
365         run_str = '${JAR} ${JARCREATE} ${TGT} ${JAROPTS}'
366
367         def runnable_status(self):
368                 """
369                 Wait for dependent tasks to be executed, then read the
370                 files to update the list of inputs.
371                 """
372                 for t in self.run_after:
373                         if not t.hasrun:
374                                 return Task.ASK_LATER
375                 if not self.inputs:
376                         try:
377                                 self.inputs = [x for x in self.basedir.ant_glob(JAR_RE, remove=False, quiet=True) if id(x) != id(self.outputs[0])]
378                         except Exception:
379                                 raise Errors.WafError('Could not find the basedir %r for %r' % (self.basedir, self))
380                 return super(jar_create, self).runnable_status()
381
382 class javac(JTask):
383         """
384         Compiles java files
385         """
386         color   = 'BLUE'
387         run_str = '${JAVAC} -classpath ${CLASSPATH} -d ${OUTDIR} ${JAVACFLAGS} ${SRC}'
388         vars = ['CLASSPATH', 'JAVACFLAGS', 'JAVAC', 'OUTDIR']
389         """
390         The javac task will be executed again if the variables CLASSPATH, JAVACFLAGS, JAVAC or OUTDIR change.
391         """
392         def uid(self):
393                 """Identify java tasks by input&output folder"""
394                 lst = [self.__class__.__name__, self.generator.outdir.abspath()]
395                 for x in self.srcdir:
396                         lst.append(x.abspath())
397                 return Utils.h_list(lst)
398
399         def runnable_status(self):
400                 """
401                 Waits for dependent tasks to be complete, then read the file system to find the input nodes.
402                 """
403                 for t in self.run_after:
404                         if not t.hasrun:
405                                 return Task.ASK_LATER
406
407                 if not self.inputs:
408                         self.inputs  = []
409                         for x in self.srcdir:
410                                 if x.exists():
411                                         self.inputs.extend(x.ant_glob(SOURCE_RE, remove=False, quiet=True))
412                 return super(javac, self).runnable_status()
413
414         def post_run(self):
415                 """
416                 List class files created
417                 """
418                 for node in self.generator.outdir.ant_glob('**/*.class', quiet=True):
419                         self.generator.bld.node_sigs[node] = self.uid()
420                 self.generator.bld.task_sigs[self.uid()] = self.cache_sig
421
422 @feature('javadoc')
423 @after_method('process_rule')
424 def create_javadoc(self):
425         """
426         Creates a javadoc task (feature 'javadoc')
427         """
428         tsk = self.create_task('javadoc')
429         tsk.classpath = getattr(self, 'classpath', [])
430         self.javadoc_package = Utils.to_list(self.javadoc_package)
431         if not isinstance(self.javadoc_output, Node.Node):
432                 self.javadoc_output = self.bld.path.find_or_declare(self.javadoc_output)
433
434 class javadoc(Task.Task):
435         """
436         Builds java documentation
437         """
438         color = 'BLUE'
439
440         def __str__(self):
441                 return '%s: %s -> %s\n' % (self.__class__.__name__, self.generator.srcdir, self.generator.javadoc_output)
442
443         def run(self):
444                 env = self.env
445                 bld = self.generator.bld
446                 wd = bld.bldnode
447
448                 #add src node + bld node (for generated java code)
449                 srcpath = self.generator.path.abspath() + os.sep + self.generator.srcdir
450                 srcpath += os.pathsep
451                 srcpath += self.generator.path.get_bld().abspath() + os.sep + self.generator.srcdir
452
453                 classpath = env.CLASSPATH
454                 classpath += os.pathsep
455                 classpath += os.pathsep.join(self.classpath)
456                 classpath = "".join(classpath)
457
458                 self.last_cmd = lst = []
459                 lst.extend(Utils.to_list(env.JAVADOC))
460                 lst.extend(['-d', self.generator.javadoc_output.abspath()])
461                 lst.extend(['-sourcepath', srcpath])
462                 lst.extend(['-classpath', classpath])
463                 lst.extend(['-subpackages'])
464                 lst.extend(self.generator.javadoc_package)
465                 lst = [x for x in lst if x]
466
467                 self.generator.bld.cmd_and_log(lst, cwd=wd, env=env.env or None, quiet=0)
468
469         def post_run(self):
470                 nodes = self.generator.javadoc_output.ant_glob('**', quiet=True)
471                 for node in nodes:
472                         self.generator.bld.node_sigs[node] = self.uid()
473                 self.generator.bld.task_sigs[self.uid()] = self.cache_sig
474
475 def configure(self):
476         """
477         Detects the javac, java and jar programs
478         """
479         # If JAVA_PATH is set, we prepend it to the path list
480         java_path = self.environ['PATH'].split(os.pathsep)
481         v = self.env
482
483         if 'JAVA_HOME' in self.environ:
484                 java_path = [os.path.join(self.environ['JAVA_HOME'], 'bin')] + java_path
485                 self.env.JAVA_HOME = [self.environ['JAVA_HOME']]
486
487         for x in 'javac java jar javadoc'.split():
488                 self.find_program(x, var=x.upper(), path_list=java_path, mandatory=(x not in ('javadoc')))
489
490         if 'CLASSPATH' in self.environ:
491                 v.CLASSPATH = self.environ['CLASSPATH']
492
493         if not v.JAR:
494                 self.fatal('jar is required for making java packages')
495         if not v.JAVAC:
496                 self.fatal('javac is required for compiling java classes')
497
498         v.JARCREATE = 'cf' # can use cvf
499         v.JAVACFLAGS = []
500
501 @conf
502 def check_java_class(self, classname, with_classpath=None):
503         """
504         Checks if the specified java class exists
505
506         :param classname: class to check, like java.util.HashMap
507         :type classname: string
508         :param with_classpath: additional classpath to give
509         :type with_classpath: string
510         """
511         javatestdir = '.waf-javatest'
512
513         classpath = javatestdir
514         if self.env.CLASSPATH:
515                 classpath += os.pathsep + self.env.CLASSPATH
516         if isinstance(with_classpath, str):
517                 classpath += os.pathsep + with_classpath
518
519         shutil.rmtree(javatestdir, True)
520         os.mkdir(javatestdir)
521
522         Utils.writef(os.path.join(javatestdir, 'Test.java'), class_check_source)
523
524         # Compile the source
525         self.exec_command(self.env.JAVAC + [os.path.join(javatestdir, 'Test.java')], shell=False)
526
527         # Try to run the app
528         cmd = self.env.JAVA + ['-cp', classpath, 'Test', classname]
529         self.to_log("%s\n" % str(cmd))
530         found = self.exec_command(cmd, shell=False)
531
532         self.msg('Checking for java class %s' % classname, not found)
533
534         shutil.rmtree(javatestdir, True)
535
536         return found
537
538 @conf
539 def check_jni_headers(conf):
540         """
541         Checks for jni headers and libraries. On success the conf.env variables xxx_JAVA are added for use in C/C++ targets::
542
543                 def options(opt):
544                         opt.load('compiler_c')
545
546                 def configure(conf):
547                         conf.load('compiler_c java')
548                         conf.check_jni_headers()
549
550                 def build(bld):
551                         bld.shlib(source='a.c', target='app', use='JAVA')
552         """
553         if not conf.env.CC_NAME and not conf.env.CXX_NAME:
554                 conf.fatal('load a compiler first (gcc, g++, ..)')
555
556         if not conf.env.JAVA_HOME:
557                 conf.fatal('set JAVA_HOME in the system environment')
558
559         # jni requires the jvm
560         javaHome = conf.env.JAVA_HOME[0]
561
562         dir = conf.root.find_dir(conf.env.JAVA_HOME[0] + '/include')
563         if dir is None:
564                 dir = conf.root.find_dir(conf.env.JAVA_HOME[0] + '/../Headers') # think different?!
565         if dir is None:
566                 conf.fatal('JAVA_HOME does not seem to be set properly')
567
568         f = dir.ant_glob('**/(jni|jni_md).h')
569         incDirs = [x.parent.abspath() for x in f]
570
571         dir = conf.root.find_dir(conf.env.JAVA_HOME[0])
572         f = dir.ant_glob('**/*jvm.(so|dll|dylib)')
573         libDirs = [x.parent.abspath() for x in f] or [javaHome]
574
575         # On windows, we need both the .dll and .lib to link.  On my JDK, they are
576         # in different directories...
577         f = dir.ant_glob('**/*jvm.(lib)')
578         if f:
579                 libDirs = [[x, y.parent.abspath()] for x in libDirs for y in f]
580
581         if conf.env.DEST_OS == 'freebsd':
582                 conf.env.append_unique('LINKFLAGS_JAVA', '-pthread')
583         for d in libDirs:
584                 try:
585                         conf.check(header_name='jni.h', define_name='HAVE_JNI_H', lib='jvm',
586                                 libpath=d, includes=incDirs, uselib_store='JAVA', uselib='JAVA')
587                 except Exception:
588                         pass
589                 else:
590                         break
591         else:
592                 conf.fatal('could not find lib jvm in %r (see config.log)' % libDirs)
593