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. """
29 from pathlib
import Path
30 from typing
import List
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',
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:
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
+>|
' +
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:
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 = []
134 self.localClasses = []
135 self.stackAnonClassBrackets = []
136 self.anonStacksStack = [[0]]
139 def trace(self) -> None:
142 def __getClassState(self):
144 return self.classState
145 except AttributeError:
146 ret = ClassState(self)
147 self.classState = ret
150 def __getPackageState(self):
152 return self.packageState
153 except AttributeError:
154 ret = PackageState(self)
155 self.packageState = ret
158 def __getAnonClassState(self):
160 return self.anonState
161 except AttributeError:
162 self.outer_state = self
163 ret = SkipState(1, AnonClassState(self))
167 def __getSkipState(self):
169 return self.skipState
170 except AttributeError:
171 ret = SkipState(1, self)
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)
200 return IgnoreState('*/', self)
205 elif token in ['"', "'"]:
206 return IgnoreState(token, self)
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:
215 self.stackBrackets.append(self.brackets)
216 return self.__getClassState()
217 elif token == 'package
':
218 return self.__getPackageState()
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
()
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 (
249 self
.stackAnonClassBrackets
.append(self
.brackets
)
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
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
275 def __getClassState(self
):
277 return self
.classState
278 except AttributeError:
279 ret
= ClassState(self
)
280 self
.classState
= ret
283 def __getAnonClassState(self
):
285 return self
.anonState
286 except AttributeError:
287 ret
= SkipState(1, AnonClassState(self
))
291 def __getSkipState(self
):
293 return self
.skipState
294 except AttributeError:
295 ret
= SkipState(1, self
)
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
)
311 return IgnoreState('*/', self
)
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
)
322 # anonymous inner class
323 return self
.__getAnonClassState
()
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
()
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
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
)
348 return IgnoreState('*/', self
)
351 elif token
[0] == '<' and token
[-1] == '>':
354 self
.brace_level
= self
.brace_level
+ 1
356 if self
.brace_level
> 0:
358 # look further for anonymous inner class
359 return SkipState(1, AnonClassState(self
))
360 elif token
in ['"', "'"]:
361 return IgnoreState(token
, self
)
363 self
.brace_level
= self
.brace_level
- 1
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
)
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
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
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.
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]
408 locals[token
] = 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
419 """A state that will ignore all tokens until it gets to a
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
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
:
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
456 initial
= OuterState(version
)
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
)
464 package
= initial
.package
.replace('.', os
.sep
)
465 return (package
, initial
.listOutputs
)
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.
492 platform: selector for search algorithm.
493 version: if not None, restrict the search to this version.
496 list of default paths for jdk.
499 if platform
== 'win32':
502 paths
= glob
.glob(java_win32_version_dir_glob
% version
)
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]
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)
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.
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.
546 list of include directory paths.
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':
562 paths
= [java_macos_include_dir_glob
]
564 paths
= sorted(glob
.glob(java_macos_version_include_dir_glob
% version
))
568 for p
in java_linux_include_dirs_glob
:
569 base_paths
.extend(glob
.glob(p
))
571 for p
in java_linux_version_include_dirs_glob
:
572 base_paths
.extend(glob
.glob(p
% version
))
576 paths
.extend([p
, os
.path
.join(p
, 'linux')])
582 # indent-tabs-mode:nil
584 # vim: set expandtab tabstop=4 shiftwidth=4: