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 from __future__
import annotations
31 from pathlib
import Path
37 default_java_version
= '1.4'
39 # a switch for which jdk versions to use the Scope state for smarter
40 # anonymous inner class parsing.
41 scopeStateVersions
= ('1.8',)
43 # Glob patterns for use in finding where the JDK is.
45 # These are pairs, (*dir_glob, *version_dir_glob) depending on whether
46 # a JDK version was requested or not.
47 # For now only used for Windows, which doesn't install JDK in a
48 # path that would be in env['ENV']['PATH']. The specific tool will
49 # add the discovered path to this. Since Oracle changed the rules,
50 # there are many possible vendors, we can't guess them all, but take a shot.
51 java_win32_dir_glob
= 'C:/Program Files*/*/*jdk*/bin'
53 # On windows, since Java 9, there is a dash between 'jdk' and the version
54 # string that wasn't there before. this glob should catch either way.
55 java_win32_version_dir_glob
= 'C:/Program Files*/*/*jdk*%s*/bin'
57 # Glob patterns for use in finding where the JDK headers are.
58 # These are pairs, *dir_glob used in the general case,
59 # *version_dir_glob if matching only a specific version.
60 java_macos_include_dir_glob
= '/System/Library/Frameworks/JavaVM.framework/Headers/'
61 java_macos_version_include_dir_glob
= '/System/Library/Frameworks/JavaVM.framework/Versions/%s*/Headers/'
63 java_linux_include_dirs_glob
= [
64 '/usr/lib/jvm/default-java/include',
65 '/usr/lib/jvm/java-*/include',
66 '/opt/oracle-jdk-bin-*/include',
67 '/opt/openjdk-bin-*/include',
68 '/usr/lib/openjdk-*/include',
70 # Need to match path like below (from Centos 7)
71 # /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-0.el7_5.x86_64/include/
72 java_linux_version_include_dirs_glob
= [
73 '/usr/lib/jvm/java-*-sun-%s*/include',
74 '/usr/lib/jvm/java-%s*-openjdk*/include',
75 '/usr/java/jdk%s*/include',
79 # Parse Java files for class names.
81 # This is a really cool parser from Charles Crain
82 # that finds appropriate class names in Java source.
84 # A regular expression that will find, in a java file:
87 # a single-line comment "//";
88 # single or double quotes preceeded by a backslash;
89 # single quotes, double quotes, open or close braces, semi-colons,
90 # periods, open or close parentheses;
91 # floating-point numbers;
92 # any alphanumeric token (keyword, class name, specifier);
93 # any alphanumeric token surrounded by angle brackets (generics);
94 # the multi-line comment begin and end tokens /* and */;
95 # array declarations "[]".
96 # Lambda function symbols: ->
97 _reToken
= re
.compile(r
'(\n|\\\\|//|\\[\'"]|[\'"{\
};.()]|
' +
98 r'\d
*\
.\d
*|
[A
-Za
-z_
][\w$
.]*|
<[A
-Za
-z_
]\w
+>|
' +
103 """The initial state for parsing a Java file for classes,
104 interfaces, and anonymous inner classes."""
106 def __init__(self, version=default_java_version) -> None:
132 msg = "Java version %s not supported" % version
133 raise NotImplementedError(msg)
135 self.version = version
136 self.listClasses = []
137 self.listOutputs = []
138 self.stackBrackets = []
141 self.localClasses = []
142 self.stackAnonClassBrackets = []
143 self.anonStacksStack = [[0]]
146 def trace(self) -> None:
149 def __getClassState(self):
151 return self.classState
152 except AttributeError:
153 ret = ClassState(self)
154 self.classState = ret
157 def __getPackageState(self):
159 return self.packageState
160 except AttributeError:
161 ret = PackageState(self)
162 self.packageState = ret
165 def __getAnonClassState(self):
167 return self.anonState
168 except AttributeError:
169 self.outer_state = self
170 ret = SkipState(1, AnonClassState(self))
174 def __getSkipState(self):
176 return self.skipState
177 except AttributeError:
178 ret = SkipState(1, self)
182 def _getAnonStack(self):
183 return self.anonStacksStack[-1]
185 def openBracket(self) -> None:
186 self.brackets = self.brackets + 1
188 def closeBracket(self) -> None:
189 self.brackets = self.brackets - 1
190 if len(self.stackBrackets) and \
191 self.brackets == self.stackBrackets[-1]:
192 self.listOutputs.append('$
'.join(self.listClasses))
193 self.localClasses.pop()
194 self.listClasses.pop()
195 self.anonStacksStack.pop()
196 self.stackBrackets.pop()
197 if len(self.stackAnonClassBrackets) and \
198 self.brackets == self.stackAnonClassBrackets[-1] and \
199 self.version not in scopeStateVersions:
200 self._getAnonStack().pop()
201 self.stackAnonClassBrackets.pop()
203 def parseToken(self, token):
204 if token[:2] == '//':
205 return IgnoreState('\n', self)
207 return IgnoreState('*/', self)
212 elif token in ['"', "'"]:
213 return IgnoreState(token, self)
215 # anonymous inner class
216 if len(self.listClasses) > 0:
217 return self.__getAnonClassState()
218 return self.__getSkipState() # Skip the class name
219 elif token in ['class', 'interface
', 'enum
']:
220 if len(self.listClasses) == 0:
222 self.stackBrackets.append(self.brackets)
223 return self.__getClassState()
224 elif token == 'package
':
225 return self.__getPackageState()
227 # Skip the attribute, it might be named "class", in which
228 # case we don't want to treat the following token
as
229 # an inner class name...
230 return self
.__getSkipState
()
233 def addAnonClass(self
) -> None:
234 """Add an anonymous inner class"""
235 if self
.version
in ('1.1', '1.2', '1.3', '1.4'):
236 clazz
= self
.listClasses
[0]
237 self
.listOutputs
.append('%s$%d' % (clazz
, self
.nextAnon
))
238 # TODO: shouldn't need to repeat versions here and in OuterState
239 elif self
.version
in (
260 self
.stackAnonClassBrackets
.append(self
.brackets
)
262 className
.extend(self
.listClasses
)
263 self
._getAnonStack
()[-1] = self
._getAnonStack
()[-1] + 1
264 for anon
in self
._getAnonStack
():
265 className
.append(str(anon
))
266 self
.listOutputs
.append('$'.join(className
))
268 self
.nextAnon
= self
.nextAnon
+ 1
269 self
._getAnonStack
().append(0)
271 def setPackage(self
, package
) -> None:
272 self
.package
= package
277 A state that parses code within a scope normally,
278 within the confines of a scope.
281 def __init__(self
, old_state
) -> None:
282 self
.outer_state
= old_state
.outer_state
283 self
.old_state
= old_state
286 def __getClassState(self
):
288 return self
.classState
289 except AttributeError:
290 ret
= ClassState(self
)
291 self
.classState
= ret
294 def __getAnonClassState(self
):
296 return self
.anonState
297 except AttributeError:
298 ret
= SkipState(1, AnonClassState(self
))
302 def __getSkipState(self
):
304 return self
.skipState
305 except AttributeError:
306 ret
= SkipState(1, self
)
310 def openBracket(self
) -> None:
311 self
.brackets
= self
.brackets
+ 1
313 def closeBracket(self
) -> None:
314 self
.brackets
= self
.brackets
- 1
316 def parseToken(self
, token
):
317 # if self.brackets == 0:
318 # return self.old_state.parseToken(token)
319 if token
[:2] == '//':
320 return IgnoreState('\n', self
)
322 return IgnoreState('*/', self
)
327 if self
.brackets
== 0:
328 self
.outer_state
._getAnonStack
().pop()
329 return self
.old_state
330 elif token
in ['"', "'"]:
331 return IgnoreState(token
, self
)
333 # anonymous inner class
334 return self
.__getAnonClassState
()
336 # Skip the attribute, it might be named "class", in which
337 # case we don't want to treat the following token as
338 # an inner class name...
339 return self
.__getSkipState
()
343 class AnonClassState
:
344 """A state that looks for anonymous inner classes."""
346 def __init__(self
, old_state
) -> None:
347 # outer_state is always an instance of OuterState
348 self
.outer_state
= old_state
.outer_state
349 self
.old_state
= old_state
352 def parseToken(self
, token
):
353 # This is an anonymous class if and only if the next
354 # non-whitespace token is a bracket. Everything between
355 # braces should be parsed as normal java code.
356 if token
[:2] == '//':
357 return IgnoreState('\n', self
)
359 return IgnoreState('*/', self
)
362 elif token
[0] == '<' and token
[-1] == '>':
365 self
.brace_level
= self
.brace_level
+ 1
367 if self
.brace_level
> 0:
369 # look further for anonymous inner class
370 return SkipState(1, AnonClassState(self
))
371 elif token
in ['"', "'"]:
372 return IgnoreState(token
, self
)
374 self
.brace_level
= self
.brace_level
- 1
377 self
.outer_state
.addAnonClass()
378 if self
.outer_state
.version
in scopeStateVersions
:
379 return ScopeState(old_state
=self
.old_state
).parseToken(token
)
380 return self
.old_state
.parseToken(token
)
384 """A state that will skip a specified number of tokens before
385 reverting to the previous state."""
387 def __init__(self
, tokens_to_skip
, old_state
) -> None:
388 self
.tokens_to_skip
= tokens_to_skip
389 self
.old_state
= old_state
391 def parseToken(self
, token
):
392 self
.tokens_to_skip
= self
.tokens_to_skip
- 1
393 if self
.tokens_to_skip
< 1:
394 return self
.old_state
399 """A state we go into when we hit a class or interface keyword."""
401 def __init__(self
, outer_state
) -> None:
402 # outer_state is always an instance of OuterState
403 self
.outer_state
= outer_state
405 def parseToken(self
, token
):
406 # the next non-whitespace token should be the name of the class
409 # If that's an inner class which is declared in a method, it
410 # requires an index prepended to the class-name, e.g.
412 # https://github.com/SCons/scons/issues/2087
413 if self
.outer_state
.localClasses
and \
414 self
.outer_state
.stackBrackets
[-1] > \
415 self
.outer_state
.stackBrackets
[-2] + 1:
416 locals = self
.outer_state
.localClasses
[-1]
419 locals[token
] = locals[token
] + 1
422 token
= str(locals[token
]) + token
423 self
.outer_state
.localClasses
.append({})
424 self
.outer_state
.listClasses
.append(token
)
425 self
.outer_state
.anonStacksStack
.append([0])
426 return self
.outer_state
430 """A state that will ignore all tokens until it gets to a
433 def __init__(self
, ignore_until
, old_state
) -> None:
434 self
.ignore_until
= ignore_until
435 self
.old_state
= old_state
437 def parseToken(self
, token
):
438 if self
.ignore_until
== token
:
439 return self
.old_state
444 """The state we enter when we encounter the package keyword.
445 We assume the next token will be the package name."""
447 def __init__(self
, outer_state
) -> None:
448 # outer_state is always an instance of OuterState
449 self
.outer_state
= outer_state
451 def parseToken(self
, token
):
452 self
.outer_state
.setPackage(token
)
453 return self
.outer_state
456 def parse_java_file(fn
, version
=default_java_version
):
457 with
open(fn
, "rb") as f
:
458 data
= SCons
.Util
.to_Text(f
.read())
459 return parse_java(data
, version
)
462 def parse_java(contents
, version
=default_java_version
, trace
=None):
463 """Parse a .java file and return a double of package directory,
464 plus a list of .class files that compiling that .java file will
467 initial
= OuterState(version
)
469 for token
in _reToken
.findall(contents
):
470 # The regex produces a bunch of groups, but only one will
471 # have anything in it.
472 currstate
= currstate
.parseToken(token
)
473 if trace
: trace(token
, currstate
)
475 package
= initial
.package
.replace('.', os
.sep
)
476 return (package
, initial
.listOutputs
)
479 # Don't actually parse Java files for class names.
481 # We might make this a configurable option in the future if
482 # Java-file parsing takes too long (although it shouldn't relative
483 # to how long the Java compiler itself seems to take...).
485 def parse_java_file(fn
, version
=default_java_version
):
486 """ "Parse" a .java file.
488 This actually just splits the file name, so the assumption here
489 is that the file name matches the public class name, and that
490 the path to the file is the same as the package name.
492 return os
.path
.split(fn
)
495 def get_java_install_dirs(platform
, version
=None) -> list[str]:
496 """ Find possible java jdk installation directories.
498 Returns a list for use as `default_paths` when looking up actual
499 java binaries with :meth:`SCons.Tool.find_program_path`.
500 The paths are sorted by version, latest first.
503 platform: selector for search algorithm.
504 version: if not None, restrict the search to this version.
507 list of default paths for jdk.
510 if platform
== 'win32':
513 paths
= glob
.glob(java_win32_version_dir_glob
% version
)
515 paths
= glob
.glob(java_win32_dir_glob
)
517 def win32getvnum(java
):
518 """ Generates a sort key for win32 jdk versions.
520 We'll have gotten a path like ...something/*jdk*/bin because
521 that is the pattern we glob for. To generate the sort key,
522 extracts the next-to-last component, then trims it further if
523 it had a complex name, like 'java-1.8.0-openjdk-1.8.0.312-1',
524 to try and put it on a common footing with the more common style,
525 which looks like 'jdk-11.0.2'.
527 This is certainly fragile, and if someone has a 9.0 it won't
528 sort right since this will still be alphabetic, BUT 9.0 was
529 not an LTS release and is 30 mos out of support as this note
530 is written so just assume it will be okay.
532 d
= Path(java
).parts
[-2]
533 if not d
.startswith('jdk'):
534 d
= 'jdk' + d
.rsplit('jdk', 1)[-1]
537 return sorted(paths
, key
=win32getvnum
, reverse
=True)
539 # other platforms, do nothing for now: we expect the standard
540 # paths to be enough to find a jdk (e.g. use alternatives system)
544 def get_java_include_paths(env
, javac
, version
) -> list[str]:
545 """Find java include paths for JNI building.
547 Cannot be called in isolation - `javac` refers to an already detected
548 compiler. Normally would would call :func:`get_java_install_dirs` first
549 and then do lookups on the paths it returns before calling us.
552 env: construction environment, used to extract platform.
553 javac: path to detected javac.
554 version: if not None, restrict the search to this version.
557 list of include directory paths.
563 # on Windows, we have a path to the actual javac, so look locally
564 if env
['PLATFORM'] == 'win32':
565 javac_bin_dir
= os
.path
.dirname(javac
)
566 java_inc_dir
= os
.path
.normpath(os
.path
.join(javac_bin_dir
, '..', 'include'))
567 paths
= [java_inc_dir
, os
.path
.join(java_inc_dir
, 'win32')]
569 # for the others, we probably found something which isn't in the JDK dir,
570 # so use the predefined patterns to glob for an include directory.
571 elif env
['PLATFORM'] == 'darwin':
573 paths
= [java_macos_include_dir_glob
]
575 paths
= sorted(glob
.glob(java_macos_version_include_dir_glob
% version
))
579 for p
in java_linux_include_dirs_glob
:
580 base_paths
.extend(glob
.glob(p
))
582 for p
in java_linux_version_include_dirs_glob
:
583 base_paths
.extend(glob
.glob(p
% version
))
587 paths
.extend([p
, os
.path
.join(p
, 'linux')])
593 # indent-tabs-mode:nil
595 # vim: set expandtab tabstop=4 shiftwidth=4: