renamed SCons.Tool.ninja -> SCons.Tool.ninja_tool and added alias in tool loading...
[scons.git] / SCons / Tool / JavaCommon.py
blob0bcb0eaa5e0538ab179d130c912a7b3dfc863bf2
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 from __future__ import annotations
28 import os
29 import re
30 import glob
31 from pathlib import Path
33 import SCons.Util
35 java_parsing = True
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',
78 if java_parsing:
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:
85 # newlines;
86 # double-backslashes;
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+>|' +
99 r'/\*|\*/|\[\]|->)')
102 class OuterState:
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:
107 if version not in (
108 '1.1',
109 '1.2',
110 '1.3',
111 '1.4',
112 '1.5',
113 '1.6',
114 '1.7',
115 '1.8',
116 '5',
117 '6',
118 '9.0',
119 '10.0',
120 '11.0',
121 '12.0',
122 '13.0',
123 '14.0',
124 '15.0',
125 '16.0',
126 '17.0',
127 '18.0',
128 '19.0',
129 '20.0',
130 '21.0',
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 = []
139 self.brackets = 0
140 self.nextAnon = 1
141 self.localClasses = []
142 self.stackAnonClassBrackets = []
143 self.anonStacksStack = [[0]]
144 self.package = None
146 def trace(self) -> None:
147 pass
149 def __getClassState(self):
150 try:
151 return self.classState
152 except AttributeError:
153 ret = ClassState(self)
154 self.classState = ret
155 return ret
157 def __getPackageState(self):
158 try:
159 return self.packageState
160 except AttributeError:
161 ret = PackageState(self)
162 self.packageState = ret
163 return ret
165 def __getAnonClassState(self):
166 try:
167 return self.anonState
168 except AttributeError:
169 self.outer_state = self
170 ret = SkipState(1, AnonClassState(self))
171 self.anonState = ret
172 return ret
174 def __getSkipState(self):
175 try:
176 return self.skipState
177 except AttributeError:
178 ret = SkipState(1, self)
179 self.skipState = ret
180 return ret
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)
206 elif token == '/*':
207 return IgnoreState('*/', self)
208 elif token == '{':
209 self.openBracket()
210 elif token == '}':
211 self.closeBracket()
212 elif token in ['"', "'"]:
213 return IgnoreState(token, self)
214 elif token == "new":
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:
221 self.nextAnon = 1
222 self.stackBrackets.append(self.brackets)
223 return self.__getClassState()
224 elif token == 'package':
225 return self.__getPackageState()
226 elif token == '.':
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()
231 return self
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 (
240 '1.5',
241 '1.6',
242 '1.7',
243 '1.8',
244 '5',
245 '6',
246 '9.0',
247 '10.0',
248 '11.0',
249 '12.0',
250 '13.0',
251 '14.0',
252 '15.0',
253 '16.0',
254 '17.0',
255 '18.0',
256 '19.0',
257 '20.0',
258 '21.0',
260 self.stackAnonClassBrackets.append(self.brackets)
261 className = []
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
275 class ScopeState:
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
284 self.brackets = 0
286 def __getClassState(self):
287 try:
288 return self.classState
289 except AttributeError:
290 ret = ClassState(self)
291 self.classState = ret
292 return ret
294 def __getAnonClassState(self):
295 try:
296 return self.anonState
297 except AttributeError:
298 ret = SkipState(1, AnonClassState(self))
299 self.anonState = ret
300 return ret
302 def __getSkipState(self):
303 try:
304 return self.skipState
305 except AttributeError:
306 ret = SkipState(1, self)
307 self.skipState = ret
308 return ret
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)
321 elif token == '/*':
322 return IgnoreState('*/', self)
323 elif token == '{':
324 self.openBracket()
325 elif token == '}':
326 self.closeBracket()
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)
332 elif token == "new":
333 # anonymous inner class
334 return self.__getAnonClassState()
335 elif token == '.':
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()
340 return self
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
350 self.brace_level = 0
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)
358 elif token == '/*':
359 return IgnoreState('*/', self)
360 elif token == '\n':
361 return self
362 elif token[0] == '<' and token[-1] == '>':
363 return self
364 elif token == '(':
365 self.brace_level = self.brace_level + 1
366 return self
367 if self.brace_level > 0:
368 if token == 'new':
369 # look further for anonymous inner class
370 return SkipState(1, AnonClassState(self))
371 elif token in ['"', "'"]:
372 return IgnoreState(token, self)
373 elif token == ')':
374 self.brace_level = self.brace_level - 1
375 return self
376 if token == '{':
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)
383 class SkipState:
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
395 return self
398 class ClassState:
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
407 if token == '\n':
408 return self
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.
411 # 'Foo$1Inner'
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]
417 try:
418 idx = locals[token]
419 locals[token] = locals[token] + 1
420 except KeyError:
421 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
429 class IgnoreState:
430 """A state that will ignore all tokens until it gets to a
431 specified token."""
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
440 return self
443 class PackageState:
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
465 produce"""
466 package = None
467 initial = OuterState(version)
468 currstate = initial
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)
474 if initial.package:
475 package = initial.package.replace('.', os.sep)
476 return (package, initial.listOutputs)
478 else:
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.
502 Args:
503 platform: selector for search algorithm.
504 version: if not None, restrict the search to this version.
506 Returns:
507 list of default paths for jdk.
510 if platform == 'win32':
511 paths = []
512 if version:
513 paths = glob.glob(java_win32_version_dir_glob % version)
514 else:
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]
535 return d
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)
541 return []
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.
551 Args:
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.
556 Returns:
557 list of include directory paths.
560 if not javac:
561 return []
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':
572 if not version:
573 paths = [java_macos_include_dir_glob]
574 else:
575 paths = sorted(glob.glob(java_macos_version_include_dir_glob % version))
576 else:
577 base_paths = []
578 if not version:
579 for p in java_linux_include_dirs_glob:
580 base_paths.extend(glob.glob(p))
581 else:
582 for p in java_linux_version_include_dirs_glob:
583 base_paths.extend(glob.glob(p % version))
585 paths = []
586 for p in base_paths:
587 paths.extend([p, os.path.join(p, 'linux')])
589 return paths
591 # Local Variables:
592 # tab-width:4
593 # indent-tabs-mode:nil
594 # End:
595 # vim: set expandtab tabstop=4 shiftwidth=4: