Followon to PR #4348: more bool fixes
[scons.git] / SCons / Tool / JavaCommon.py
blob4a5a7c531e8dea528d580a04fe6e8b6e020682cc
1 # MIT License
3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 """Common routines for processing Java. """
26 import os
27 import re
28 import glob
29 from pathlib import Path
30 from typing import List
32 java_parsing = True
34 default_java_version = '1.4'
36 # a switch for which jdk versions to use the Scope state for smarter
37 # anonymous inner class parsing.
38 scopeStateVersions = ('1.8',)
40 # Glob patterns for use in finding where the JDK is.
42 # These are pairs, (*dir_glob, *version_dir_glob) depending on whether
43 # a JDK version was requested or not.
44 # For now only used for Windows, which doesn't install JDK in a
45 # path that would be in env['ENV']['PATH']. The specific tool will
46 # add the discovered path to this. Since Oracle changed the rules,
47 # there are many possible vendors, we can't guess them all, but take a shot.
48 java_win32_dir_glob = 'C:/Program Files*/*/*jdk*/bin'
50 # On windows, since Java 9, there is a dash between 'jdk' and the version
51 # string that wasn't there before. this glob should catch either way.
52 java_win32_version_dir_glob = 'C:/Program Files*/*/*jdk*%s*/bin'
54 # Glob patterns for use in finding where the JDK headers are.
55 # These are pairs, *dir_glob used in the general case,
56 # *version_dir_glob if matching only a specific version.
57 java_macos_include_dir_glob = '/System/Library/Frameworks/JavaVM.framework/Headers/'
58 java_macos_version_include_dir_glob = '/System/Library/Frameworks/JavaVM.framework/Versions/%s*/Headers/'
60 java_linux_include_dirs_glob = [
61 '/usr/lib/jvm/default-java/include',
62 '/usr/lib/jvm/java-*/include',
63 '/opt/oracle-jdk-bin-*/include',
64 '/opt/openjdk-bin-*/include',
65 '/usr/lib/openjdk-*/include',
67 # Need to match path like below (from Centos 7)
68 # /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-0.el7_5.x86_64/include/
69 java_linux_version_include_dirs_glob = [
70 '/usr/lib/jvm/java-*-sun-%s*/include',
71 '/usr/lib/jvm/java-%s*-openjdk*/include',
72 '/usr/java/jdk%s*/include',
75 if java_parsing:
76 # Parse Java files for class names.
78 # This is a really cool parser from Charles Crain
79 # that finds appropriate class names in Java source.
81 # A regular expression that will find, in a java file:
82 # newlines;
83 # double-backslashes;
84 # a single-line comment "//";
85 # single or double quotes preceeded by a backslash;
86 # single quotes, double quotes, open or close braces, semi-colons,
87 # periods, open or close parentheses;
88 # floating-point numbers;
89 # any alphanumeric token (keyword, class name, specifier);
90 # any alphanumeric token surrounded by angle brackets (generics);
91 # the multi-line comment begin and end tokens /* and */;
92 # array declarations "[]".
93 # Lambda function symbols: ->
94 _reToken = re.compile(r'(\n|\\\\|//|\\[\'"]|[\'"{\};.()]|' +
95 r'\d*\.\d*|[A-Za-z_][\w$.]*|<[A-Za-z_]\w+>|' +
96 r'/\*|\*/|\[\]|->)')
99 class OuterState:
100 """The initial state for parsing a Java file for classes,
101 interfaces, and anonymous inner classes."""
103 def __init__(self, version=default_java_version) -> None:
104 if version not in (
105 '1.1',
106 '1.2',
107 '1.3',
108 '1.4',
109 '1.5',
110 '1.6',
111 '1.7',
112 '1.8',
113 '5',
114 '6',
115 '9.0',
116 '10.0',
117 '11.0',
118 '12.0',
119 '13.0',
120 '14.0',
121 '15.0',
122 '16.0',
123 '17.0',
125 msg = "Java version %s not supported" % version
126 raise NotImplementedError(msg)
128 self.version = version
129 self.listClasses = []
130 self.listOutputs = []
131 self.stackBrackets = []
132 self.brackets = 0
133 self.nextAnon = 1
134 self.localClasses = []
135 self.stackAnonClassBrackets = []
136 self.anonStacksStack = [[0]]
137 self.package = None
139 def trace(self) -> None:
140 pass
142 def __getClassState(self):
143 try:
144 return self.classState
145 except AttributeError:
146 ret = ClassState(self)
147 self.classState = ret
148 return ret
150 def __getPackageState(self):
151 try:
152 return self.packageState
153 except AttributeError:
154 ret = PackageState(self)
155 self.packageState = ret
156 return ret
158 def __getAnonClassState(self):
159 try:
160 return self.anonState
161 except AttributeError:
162 self.outer_state = self
163 ret = SkipState(1, AnonClassState(self))
164 self.anonState = ret
165 return ret
167 def __getSkipState(self):
168 try:
169 return self.skipState
170 except AttributeError:
171 ret = SkipState(1, self)
172 self.skipState = ret
173 return ret
175 def _getAnonStack(self):
176 return self.anonStacksStack[-1]
178 def openBracket(self) -> None:
179 self.brackets = self.brackets + 1
181 def closeBracket(self) -> None:
182 self.brackets = self.brackets - 1
183 if len(self.stackBrackets) and \
184 self.brackets == self.stackBrackets[-1]:
185 self.listOutputs.append('$'.join(self.listClasses))
186 self.localClasses.pop()
187 self.listClasses.pop()
188 self.anonStacksStack.pop()
189 self.stackBrackets.pop()
190 if len(self.stackAnonClassBrackets) and \
191 self.brackets == self.stackAnonClassBrackets[-1] and \
192 self.version not in scopeStateVersions:
193 self._getAnonStack().pop()
194 self.stackAnonClassBrackets.pop()
196 def parseToken(self, token):
197 if token[:2] == '//':
198 return IgnoreState('\n', self)
199 elif token == '/*':
200 return IgnoreState('*/', self)
201 elif token == '{':
202 self.openBracket()
203 elif token == '}':
204 self.closeBracket()
205 elif token in ['"', "'"]:
206 return IgnoreState(token, self)
207 elif token == "new":
208 # anonymous inner class
209 if len(self.listClasses) > 0:
210 return self.__getAnonClassState()
211 return self.__getSkipState() # Skip the class name
212 elif token in ['class', 'interface', 'enum']:
213 if len(self.listClasses) == 0:
214 self.nextAnon = 1
215 self.stackBrackets.append(self.brackets)
216 return self.__getClassState()
217 elif token == 'package':
218 return self.__getPackageState()
219 elif token == '.':
220 # Skip the attribute, it might be named "class", in which
221 # case we don't want to treat the following token as
222 # an inner class name...
223 return self.__getSkipState()
224 return self
226 def addAnonClass(self) -> None:
227 """Add an anonymous inner class"""
228 if self.version in ('1.1', '1.2', '1.3', '1.4'):
229 clazz = self.listClasses[0]
230 self.listOutputs.append('%s$%d' % (clazz, self.nextAnon))
231 # TODO: shouldn't need to repeat versions here and in OuterState
232 elif self.version in (
233 '1.5',
234 '1.6',
235 '1.7',
236 '1.8',
237 '5',
238 '6',
239 '9.0',
240 '10.0',
241 '11.0',
242 '12.0',
243 '13.0',
244 '14.0',
245 '15.0',
246 '16.0',
247 '17.0',
249 self.stackAnonClassBrackets.append(self.brackets)
250 className = []
251 className.extend(self.listClasses)
252 self._getAnonStack()[-1] = self._getAnonStack()[-1] + 1
253 for anon in self._getAnonStack():
254 className.append(str(anon))
255 self.listOutputs.append('$'.join(className))
257 self.nextAnon = self.nextAnon + 1
258 self._getAnonStack().append(0)
260 def setPackage(self, package) -> None:
261 self.package = package
264 class ScopeState:
266 A state that parses code within a scope normally,
267 within the confines of a scope.
270 def __init__(self, old_state) -> None:
271 self.outer_state = old_state.outer_state
272 self.old_state = old_state
273 self.brackets = 0
275 def __getClassState(self):
276 try:
277 return self.classState
278 except AttributeError:
279 ret = ClassState(self)
280 self.classState = ret
281 return ret
283 def __getAnonClassState(self):
284 try:
285 return self.anonState
286 except AttributeError:
287 ret = SkipState(1, AnonClassState(self))
288 self.anonState = ret
289 return ret
291 def __getSkipState(self):
292 try:
293 return self.skipState
294 except AttributeError:
295 ret = SkipState(1, self)
296 self.skipState = ret
297 return ret
299 def openBracket(self) -> None:
300 self.brackets = self.brackets + 1
302 def closeBracket(self) -> None:
303 self.brackets = self.brackets - 1
305 def parseToken(self, token):
306 # if self.brackets == 0:
307 # return self.old_state.parseToken(token)
308 if token[:2] == '//':
309 return IgnoreState('\n', self)
310 elif token == '/*':
311 return IgnoreState('*/', self)
312 elif token == '{':
313 self.openBracket()
314 elif token == '}':
315 self.closeBracket()
316 if self.brackets == 0:
317 self.outer_state._getAnonStack().pop()
318 return self.old_state
319 elif token in ['"', "'"]:
320 return IgnoreState(token, self)
321 elif token == "new":
322 # anonymous inner class
323 return self.__getAnonClassState()
324 elif token == '.':
325 # Skip the attribute, it might be named "class", in which
326 # case we don't want to treat the following token as
327 # an inner class name...
328 return self.__getSkipState()
329 return self
332 class AnonClassState:
333 """A state that looks for anonymous inner classes."""
335 def __init__(self, old_state) -> None:
336 # outer_state is always an instance of OuterState
337 self.outer_state = old_state.outer_state
338 self.old_state = old_state
339 self.brace_level = 0
341 def parseToken(self, token):
342 # This is an anonymous class if and only if the next
343 # non-whitespace token is a bracket. Everything between
344 # braces should be parsed as normal java code.
345 if token[:2] == '//':
346 return IgnoreState('\n', self)
347 elif token == '/*':
348 return IgnoreState('*/', self)
349 elif token == '\n':
350 return self
351 elif token[0] == '<' and token[-1] == '>':
352 return self
353 elif token == '(':
354 self.brace_level = self.brace_level + 1
355 return self
356 if self.brace_level > 0:
357 if token == 'new':
358 # look further for anonymous inner class
359 return SkipState(1, AnonClassState(self))
360 elif token in ['"', "'"]:
361 return IgnoreState(token, self)
362 elif token == ')':
363 self.brace_level = self.brace_level - 1
364 return self
365 if token == '{':
366 self.outer_state.addAnonClass()
367 if self.outer_state.version in scopeStateVersions:
368 return ScopeState(old_state=self.old_state).parseToken(token)
369 return self.old_state.parseToken(token)
372 class SkipState:
373 """A state that will skip a specified number of tokens before
374 reverting to the previous state."""
376 def __init__(self, tokens_to_skip, old_state) -> None:
377 self.tokens_to_skip = tokens_to_skip
378 self.old_state = old_state
380 def parseToken(self, token):
381 self.tokens_to_skip = self.tokens_to_skip - 1
382 if self.tokens_to_skip < 1:
383 return self.old_state
384 return self
387 class ClassState:
388 """A state we go into when we hit a class or interface keyword."""
390 def __init__(self, outer_state) -> None:
391 # outer_state is always an instance of OuterState
392 self.outer_state = outer_state
394 def parseToken(self, token):
395 # the next non-whitespace token should be the name of the class
396 if token == '\n':
397 return self
398 # If that's an inner class which is declared in a method, it
399 # requires an index prepended to the class-name, e.g.
400 # 'Foo$1Inner'
401 # https://github.com/SCons/scons/issues/2087
402 if self.outer_state.localClasses and \
403 self.outer_state.stackBrackets[-1] > \
404 self.outer_state.stackBrackets[-2] + 1:
405 locals = self.outer_state.localClasses[-1]
406 try:
407 idx = locals[token]
408 locals[token] = locals[token] + 1
409 except KeyError:
410 locals[token] = 1
411 token = str(locals[token]) + token
412 self.outer_state.localClasses.append({})
413 self.outer_state.listClasses.append(token)
414 self.outer_state.anonStacksStack.append([0])
415 return self.outer_state
418 class IgnoreState:
419 """A state that will ignore all tokens until it gets to a
420 specified token."""
422 def __init__(self, ignore_until, old_state) -> None:
423 self.ignore_until = ignore_until
424 self.old_state = old_state
426 def parseToken(self, token):
427 if self.ignore_until == token:
428 return self.old_state
429 return self
432 class PackageState:
433 """The state we enter when we encounter the package keyword.
434 We assume the next token will be the package name."""
436 def __init__(self, outer_state) -> None:
437 # outer_state is always an instance of OuterState
438 self.outer_state = outer_state
440 def parseToken(self, token):
441 self.outer_state.setPackage(token)
442 return self.outer_state
445 def parse_java_file(fn, version=default_java_version):
446 with open(fn, 'r', encoding='utf-8') as f:
447 data = f.read()
448 return parse_java(data, version)
451 def parse_java(contents, version=default_java_version, trace=None):
452 """Parse a .java file and return a double of package directory,
453 plus a list of .class files that compiling that .java file will
454 produce"""
455 package = None
456 initial = OuterState(version)
457 currstate = initial
458 for token in _reToken.findall(contents):
459 # The regex produces a bunch of groups, but only one will
460 # have anything in it.
461 currstate = currstate.parseToken(token)
462 if trace: trace(token, currstate)
463 if initial.package:
464 package = initial.package.replace('.', os.sep)
465 return (package, initial.listOutputs)
467 else:
468 # Don't actually parse Java files for class names.
470 # We might make this a configurable option in the future if
471 # Java-file parsing takes too long (although it shouldn't relative
472 # to how long the Java compiler itself seems to take...).
474 def parse_java_file(fn, version=default_java_version):
475 """ "Parse" a .java file.
477 This actually just splits the file name, so the assumption here
478 is that the file name matches the public class name, and that
479 the path to the file is the same as the package name.
481 return os.path.split(fn)
484 def get_java_install_dirs(platform, version=None) -> List[str]:
485 """ Find possible java jdk installation directories.
487 Returns a list for use as `default_paths` when looking up actual
488 java binaries with :meth:`SCons.Tool.find_program_path`.
489 The paths are sorted by version, latest first.
491 Args:
492 platform: selector for search algorithm.
493 version: if not None, restrict the search to this version.
495 Returns:
496 list of default paths for jdk.
499 if platform == 'win32':
500 paths = []
501 if version:
502 paths = glob.glob(java_win32_version_dir_glob % version)
503 else:
504 paths = glob.glob(java_win32_dir_glob)
506 def win32getvnum(java):
507 """ Generates a sort key for win32 jdk versions.
509 We'll have gotten a path like ...something/*jdk*/bin because
510 that is the pattern we glob for. To generate the sort key,
511 extracts the next-to-last component, then trims it further if
512 it had a complex name, like 'java-1.8.0-openjdk-1.8.0.312-1',
513 to try and put it on a common footing with the more common style,
514 which looks like 'jdk-11.0.2'.
516 This is certainly fragile, and if someone has a 9.0 it won't
517 sort right since this will still be alphabetic, BUT 9.0 was
518 not an LTS release and is 30 mos out of support as this note
519 is written so just assume it will be okay.
521 d = Path(java).parts[-2]
522 if not d.startswith('jdk'):
523 d = 'jdk' + d.rsplit('jdk', 1)[-1]
524 return d
526 return sorted(paths, key=win32getvnum, reverse=True)
528 # other platforms, do nothing for now: we expect the standard
529 # paths to be enough to find a jdk (e.g. use alternatives system)
530 return []
533 def get_java_include_paths(env, javac, version) -> List[str]:
534 """Find java include paths for JNI building.
536 Cannot be called in isolation - `javac` refers to an already detected
537 compiler. Normally would would call :func:`get_java_install_dirs` first
538 and then do lookups on the paths it returns before calling us.
540 Args:
541 env: construction environment, used to extract platform.
542 javac: path to detected javac.
543 version: if not None, restrict the search to this version.
545 Returns:
546 list of include directory paths.
549 if not javac:
550 return []
552 # on Windows, we have a path to the actual javac, so look locally
553 if env['PLATFORM'] == 'win32':
554 javac_bin_dir = os.path.dirname(javac)
555 java_inc_dir = os.path.normpath(os.path.join(javac_bin_dir, '..', 'include'))
556 paths = [java_inc_dir, os.path.join(java_inc_dir, 'win32')]
558 # for the others, we probably found something which isn't in the JDK dir,
559 # so use the predefined patterns to glob for an include directory.
560 elif env['PLATFORM'] == 'darwin':
561 if not version:
562 paths = [java_macos_include_dir_glob]
563 else:
564 paths = sorted(glob.glob(java_macos_version_include_dir_glob % version))
565 else:
566 base_paths = []
567 if not version:
568 for p in java_linux_include_dirs_glob:
569 base_paths.extend(glob.glob(p))
570 else:
571 for p in java_linux_version_include_dirs_glob:
572 base_paths.extend(glob.glob(p % version))
574 paths = []
575 for p in base_paths:
576 paths.extend([p, os.path.join(p, 'linux')])
578 return paths
580 # Local Variables:
581 # tab-width:4
582 # indent-tabs-mode:nil
583 # End:
584 # vim: set expandtab tabstop=4 shiftwidth=4: