jscompiler.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. # Copyright 2010 The Closure Library Authors. All Rights Reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS-IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Utility to use the Closure Compiler CLI from Python."""
  15. import logging
  16. import os
  17. import re
  18. import subprocess
  19. import tempfile
  20. # Pulls just the major and minor version numbers from the first line of
  21. # 'java -version'. Versions are in the format of [0-9]+(\.[0-9]+)? See:
  22. # http://openjdk.java.net/jeps/223
  23. _VERSION_REGEX = re.compile(r'"([0-9]+)(?:\.([0-9]+))?')
  24. class JsCompilerError(Exception):
  25. """Raised if there's an error in calling the compiler."""
  26. pass
  27. def _GetJavaVersionString():
  28. """Get the version string from the Java VM."""
  29. return subprocess.check_output(['java', '-version'], stderr=subprocess.STDOUT)
  30. def _ParseJavaVersion(version_string):
  31. """Returns a 2-tuple for the current version of Java installed.
  32. Args:
  33. version_string: String of the Java version (e.g. '1.7.2-ea').
  34. Returns:
  35. The major and minor versions, as a 2-tuple (e.g. (1, 7)).
  36. """
  37. match = _VERSION_REGEX.search(version_string)
  38. if match:
  39. version = tuple(int(x or 0) for x in match.groups())
  40. assert len(version) == 2
  41. return version
  42. def _JavaSupports32BitMode():
  43. """Determines whether the JVM supports 32-bit mode on the platform."""
  44. # Suppresses process output to stderr and stdout from showing up in the
  45. # console as we're only trying to determine 32-bit JVM support.
  46. supported = False
  47. try:
  48. devnull = open(os.devnull, 'wb')
  49. return subprocess.call(
  50. ['java', '-d32', '-version'], stdout=devnull, stderr=devnull) == 0
  51. except IOError:
  52. pass
  53. else:
  54. devnull.close()
  55. return supported
  56. def _GetJsCompilerArgs(compiler_jar_path, java_version, jvm_flags):
  57. """Assembles arguments for call to JsCompiler."""
  58. if java_version < (1, 7):
  59. raise JsCompilerError('Closure Compiler requires Java 1.7 or higher. '
  60. 'Please visit http://www.java.com/getjava')
  61. args = ['java']
  62. # Add JVM flags we believe will produce the best performance. See
  63. # https://groups.google.com/forum/#!topic/closure-library-discuss/7w_O9-vzlj4
  64. # Attempt 32-bit mode if available (Java 7 on Mac OS X does not support 32-bit
  65. # mode, for example).
  66. if _JavaSupports32BitMode():
  67. args += ['-d32']
  68. # Prefer the "client" VM.
  69. args += ['-client']
  70. # Add JVM flags, if any
  71. if jvm_flags:
  72. args += jvm_flags
  73. # Add the application JAR.
  74. args += ['-jar', compiler_jar_path]
  75. return args
  76. def _GetFlagFile(source_paths, compiler_flags):
  77. """Writes given source paths and compiler flags to a --flagfile.
  78. The given source_paths will be written as '--js' flags and the compiler_flags
  79. are written as-is.
  80. Args:
  81. source_paths: List of string js source paths.
  82. compiler_flags: List of string compiler flags.
  83. Returns:
  84. The file to which the flags were written.
  85. """
  86. args = []
  87. for path in source_paths:
  88. args += ['--js', path]
  89. # Add compiler flags, if any.
  90. if compiler_flags:
  91. args += compiler_flags
  92. flags_file = tempfile.NamedTemporaryFile(delete=False)
  93. flags_file.write(' '.join(args))
  94. flags_file.close()
  95. return flags_file
  96. def Compile(compiler_jar_path,
  97. source_paths,
  98. jvm_flags=None,
  99. compiler_flags=None):
  100. """Prepares command-line call to Closure Compiler.
  101. Args:
  102. compiler_jar_path: Path to the Closure compiler .jar file.
  103. source_paths: Source paths to build, in order.
  104. jvm_flags: A list of additional flags to pass on to JVM.
  105. compiler_flags: A list of additional flags to pass on to Closure Compiler.
  106. Returns:
  107. The compiled source, as a string, or None if compilation failed.
  108. """
  109. java_version = _ParseJavaVersion(str(_GetJavaVersionString()))
  110. args = _GetJsCompilerArgs(compiler_jar_path, java_version, jvm_flags)
  111. # Write source path arguments to flag file for avoiding "The filename or
  112. # extension is too long" error in big projects. See
  113. # https://github.com/google/closure-library/pull/678
  114. flags_file = _GetFlagFile(source_paths, compiler_flags)
  115. args += ['--flagfile', flags_file.name]
  116. logging.info('Compiling with the following command: %s', ' '.join(args))
  117. try:
  118. return subprocess.check_output(args)
  119. except subprocess.CalledProcessError:
  120. raise JsCompilerError('JavaScript compilation failed.')
  121. finally:
  122. os.remove(flags_file.name)