| #!/usr/bin/env python |
| # |
| # Copyright 2007 Google Inc. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| """Appcfg logic specific to Java apps.""" |
| from __future__ import with_statement |
| |
| import os.path |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| |
| from google.appengine.tools import app_engine_web_xml_parser |
| from google.appengine.tools import backends_xml_parser |
| from google.appengine.tools import cron_xml_parser |
| from google.appengine.tools import dispatch_xml_parser |
| from google.appengine.tools import dos_xml_parser |
| from google.appengine.tools import indexes_xml_parser |
| from google.appengine.tools import jarfile |
| from google.appengine.tools import queue_xml_parser |
| from google.appengine.tools import web_xml_parser |
| from google.appengine.tools import yaml_translator |
| |
| |
| _CLASSES_JAR_NAME_PREFIX = '_ah_webinf_classes' |
| _COMPILED_JSP_JAR_NAME_PREFIX = '_ah_compiled_jsps' |
| _LOCAL_JSPC_CLASS = 'com.google.appengine.tools.development.LocalJspC' |
| _MAX_COMPILED_JSP_JAR_SIZE = 1024 * 1024 * 5 |
| |
| |
| class Error(Exception): |
| pass |
| |
| |
| class ConfigurationError(Error): |
| """There was a configuration error in the application being uploaded.""" |
| pass |
| |
| |
| class CompileError(Error): |
| """There was a compilation error in a JSP file or its generated Java code.""" |
| pass |
| |
| |
| def IsWarFileWithoutYaml(dir_path): |
| if os.path.isfile(os.path.join(dir_path, 'app.yaml')): |
| return False |
| web_inf = os.path.join(dir_path, 'WEB-INF') |
| return (os.path.isdir(web_inf) and |
| set(['appengine-web.xml', 'web.xml']).issubset(os.listdir(web_inf))) |
| |
| |
| def AddUpdateOptions(parser): |
| """Adds options specific to the 'update' command on Java apps to 'parser'. |
| |
| Args: |
| parser: An instance of OptionsParser. |
| """ |
| parser.add_option('--retain_upload_dir', action='store_true', |
| dest='retain_upload_dir', default=False, |
| help='Do not delete temporary (staging) directory used ' |
| 'in uploading Java apps') |
| parser.add_option('--no_symlinks', action='store_true', |
| dest='no_symlinks', default=False, |
| help='Do not use symbolic links when making the temporary ' |
| '(staging) directory for uploading Java apps') |
| parser.add_option('--compile_encoding', action='store', |
| dest='compile_encoding', default='UTF-8', |
| help='Set the encoding to be used when compiling Java ' |
| 'source files (default "UTF-8").') |
| parser.add_option('--disable_jar_jsps', action='store_false', |
| dest='jar_jsps', default=True, |
| help='Do not jar the classes generated from JSPs.') |
| parser.add_option('--delete_jsps', action='store_true', |
| dest='delete_jsps', default=False, |
| help='Delete the JSP source files after compilation.') |
| parser.add_option('--enable_jar_classes', action='store_true', |
| dest='do_jar_classes', default=False, |
| help='Jar the WEB-INF/classes content.') |
| parser.add_option('--enable_jar_splitting', action='store_true', |
| dest='do_jar_splitting', default=False, |
| help='Split large jar files (> 32M) into smaller ' |
| 'fragments.') |
| parser.add_option('--jar_splitting_excludes', action='store', |
| dest='jar_splitting_exclude_suffixes', default='', |
| help='When --enable_jar_splitting is specified and ' |
| '--jar_splitting_excludes specifies a comma-separated list ' |
| 'of suffixes, a file in a jar whose name ends with one ' |
| 'of the suffixes will not be included in the split jar ' |
| 'fragments.') |
| |
| |
| class JavaAppUpdate(object): |
| """Performs Java-specific update configurations.""" |
| |
| _JSP_REGEX = re.compile('.*\\.jspx?') |
| |
| class _XmlParser(object): |
| |
| |
| |
| def __init__(self, xml_name, yaml_name, xml_to_yaml_function): |
| self.xml_name = xml_name |
| self.yaml_name = yaml_name |
| self.xml_to_yaml_function = xml_to_yaml_function |
| |
| _XML_PARSERS = [ |
| _XmlParser('backends.xml', 'backends.yaml', |
| backends_xml_parser.GetBackendsYaml), |
| _XmlParser('cron.xml', 'cron.yaml', cron_xml_parser.GetCronYaml), |
| _XmlParser('datastore-indexes.xml', 'index.yaml', |
| indexes_xml_parser.GetIndexYaml), |
| _XmlParser('dispatch.xml', 'dispatch.yaml', |
| dispatch_xml_parser.GetDispatchYaml), |
| _XmlParser('dos.xml', 'dos.yaml', dos_xml_parser.GetDosYaml), |
| _XmlParser('queue.xml', 'queue.yaml', queue_xml_parser.GetQueueYaml), |
| ] |
| |
| _XML_VALIDATOR_CLASS = 'com.google.appengine.tools.admin.XmlValidator' |
| |
| def __init__(self, basepath, options): |
| self.basepath = os.path.abspath(basepath) |
| self.options = options |
| if not hasattr(self.options, 'no_symlinks'): |
| |
| self.options.no_symlinks = True |
| |
| java_home, exec_suffix = JavaHomeAndSuffix() |
| self.java_command = os.path.join(java_home, 'bin', 'java' + exec_suffix) |
| self.javac_command = os.path.join(java_home, 'bin', 'javac' + exec_suffix) |
| |
| self._ValidateXmlFiles() |
| |
| self.app_engine_web_xml = self._ReadAppEngineWebXml() |
| self.app_engine_web_xml.app_root = self.basepath |
| if self.options.app_id: |
| self.app_engine_web_xml.app_id = self.options.app_id |
| if self.options.version: |
| self.app_engine_web_xml.version_id = self.options.version |
| self.web_xml = self._ReadWebXml() |
| |
| def _ValidateXmlFiles(self): |
| |
| |
| |
| |
| |
| |
| |
| |
| sdk_dir = os.path.dirname(jarfile.__file__) |
| xml_validator_jar = os.path.join( |
| sdk_dir, 'java', 'lib', 'impl', 'libxmlvalidator.jar') |
| if not os.path.exists(xml_validator_jar): |
| |
| print >>sys.stderr, ('Not validating XML files because %s does not ' |
| 'exist' % xml_validator_jar) |
| return |
| validator_args = [] |
| schema_dir = os.path.join(sdk_dir, 'java', 'docs') |
| for schema_name in os.listdir(schema_dir): |
| basename, extension = os.path.splitext(schema_name) |
| if extension == '.xsd': |
| schema_file = os.path.join(schema_dir, schema_name) |
| xml_file = os.path.join(self.basepath, 'WEB-INF', basename + '.xml') |
| if os.path.exists(xml_file): |
| validator_args += [xml_file, schema_file] |
| if validator_args: |
| command_and_args = [ |
| self.java_command, |
| '-classpath', |
| xml_validator_jar, |
| self._XML_VALIDATOR_CLASS, |
| ] + validator_args |
| status = subprocess.call(command_and_args) |
| if status: |
| |
| raise ConfigurationError('XML validation failed') |
| |
| def _ReadAppEngineWebXml(self): |
| return self._ReadAndParseXml( |
| basepath=self.basepath, |
| file_name='appengine-web.xml', |
| parser=app_engine_web_xml_parser.AppEngineWebXmlParser) |
| |
| def _ReadWebXml(self, basepath=None): |
| if not basepath: |
| basepath = self.basepath |
| return self._ReadAndParseXml( |
| basepath=basepath, |
| file_name='web.xml', |
| parser=web_xml_parser.WebXmlParser) |
| |
| def _ReadAndParseXml(self, basepath, file_name, parser): |
| with open(os.path.join(basepath, 'WEB-INF', file_name)) as file_handle: |
| return parser().ProcessXml(file_handle.read()) |
| |
| def CreateStagingDirectory(self, tools_dir): |
| """Creates a staging directory for uploading. |
| |
| This is where we perform the necessary actions to create an application |
| directory for the update command to work properly - files are organized |
| into the static folder, and yaml files are generated where they can be |
| found later. |
| |
| Args: |
| tools_dir: Path to the SDK tools directory |
| (typically .../google/appengine/tools) |
| |
| Returns: |
| The path to a new temporary directory which contains generated yaml files |
| and a static file directory. For the most part, the rest of the update and |
| upload flow can resume identically to Python/PHP/Go applications. |
| |
| Raises: |
| CompileError: if compilation of JSPs failed. |
| ConfigurationError: if the app to be staged has a configuration error. |
| IOError: if there was an I/O problem, for example when scanning jar files. |
| """ |
| stage_dir = tempfile.mkdtemp(prefix='appcfgpy') |
| static_dir = os.path.join(stage_dir, '__static__') |
| os.mkdir(static_dir) |
| self._CopyOrLink(self.basepath, stage_dir, static_dir, False) |
| self.app_engine_web_xml.app_root = stage_dir |
| |
| if self.options.compile_jsps: |
| self._CompileJspsIfAny(tools_dir, stage_dir) |
| |
| web_inf = os.path.join(stage_dir, 'WEB-INF') |
| web_inf_lib = os.path.join(web_inf, 'lib') |
| api_jar_dict = _FindApiJars(web_inf_lib) |
| api_versions = set(api_jar_dict.values()) |
| if not api_versions: |
| api_version = None |
| elif len(api_versions) == 1: |
| api_version = api_versions.pop() |
| else: |
| raise ConfigurationError('API jars have inconsistent versions: %s' % |
| api_jar_dict) |
| |
| |
| for staged_api_jar in api_jar_dict: |
| os.remove(staged_api_jar) |
| |
| appengine_generated = os.path.join( |
| stage_dir, 'WEB-INF', 'appengine-generated') |
| self._GenerateAppYaml(stage_dir, api_version, appengine_generated) |
| |
| app_id = self.options.app_id or self.app_engine_web_xml.app_id |
| assert app_id, 'Missing app id' |
| |
| |
| for parser in self._XML_PARSERS: |
| xml_name = os.path.join(web_inf, parser.xml_name) |
| if os.path.exists(xml_name): |
| with open(xml_name) as xml_file: |
| xml_string = xml_file.read() |
| yaml_string = parser.xml_to_yaml_function(app_id, xml_string) |
| yaml_file = os.path.join(appengine_generated, parser.yaml_name) |
| with open(yaml_file, 'w') as yaml: |
| yaml.write(yaml_string) |
| |
| return stage_dir |
| |
| def GenerateAppYamlString(self, static_file_list, api_version=None): |
| """Constructs an app.yaml string equivalent to the XML files under WEB-INF. |
| |
| Args: |
| static_file_list: a list of strings that are the absolute path names of |
| static file resources. |
| api_version: a string that is the Java API version number, or None if |
| not known or relevant. |
| |
| Returns: |
| A string that would have the same effect as the XML files under WEB-INF |
| if it were the contents of an app.yaml file. |
| """ |
| return yaml_translator.AppYamlTranslator( |
| self.app_engine_web_xml, |
| self.web_xml, |
| static_file_list, |
| api_version).GetYaml() |
| |
| def _GenerateAppYaml(self, stage_dir, api_version, appengine_generated): |
| """Creates the app.yaml file in WEB-INF/appengine-generated/. |
| |
| Returns: |
| The path to the WEB-INF/appengine-generated directory. |
| """ |
| static_file_list = self._GetStaticFileList(stage_dir) |
| yaml_str = self.GenerateAppYamlString(static_file_list, api_version) |
| if not os.path.isdir(appengine_generated): |
| os.mkdir(appengine_generated) |
| with open(os.path.join(appengine_generated, 'app.yaml'), 'w') as handle: |
| handle.write(yaml_str) |
| |
| def _CopyOrLink(self, source_dir, stage_dir, static_dir, inside_web_inf): |
| source_dir = os.path.abspath(source_dir) |
| stage_dir = os.path.abspath(stage_dir) |
| static_dir = os.path.abspath(static_dir) |
| for file_name in os.listdir(source_dir): |
| file_path = os.path.join(source_dir, file_name) |
| |
| if file_name.startswith('.') or file_name == 'appengine-generated': |
| continue |
| |
| if os.path.isdir(file_path): |
| self._CopyOrLink( |
| file_path, |
| os.path.join(stage_dir, file_name), |
| os.path.join(static_dir, file_name), |
| inside_web_inf or file_name == 'WEB-INF') |
| else: |
| if (inside_web_inf |
| or self.app_engine_web_xml.IncludesResource(file_path) |
| or (self.options.compile_jsps |
| and file_path.lower().endswith('.jsp'))): |
| self._CopyOrLinkFile(file_path, os.path.join(stage_dir, file_name)) |
| if (not inside_web_inf |
| and self.app_engine_web_xml.IncludesStatic(file_path)): |
| self._CopyOrLinkFile(file_path, os.path.join(static_dir, file_name)) |
| |
| def _CopyOrLinkFile(self, source, dest): |
| |
| destdir = os.path.dirname(dest) |
| if not os.path.exists(destdir): |
| os.makedirs(destdir) |
| if self._ShouldSplitJar(source): |
| self._SplitJar(source, destdir) |
| elif source.endswith('web.xml') or self.options.no_symlinks: |
| shutil.copy(source, dest) |
| else: |
| os.symlink(source, dest) |
| |
| def _MoveDirectoryContents(self, source_dir, dest_dir): |
| """Move the contents of source_dir to dest_dir, which might not exist. |
| |
| Raises: |
| IOError: if the dest_dir hierarchy already contains a file where the |
| source_dir hierarchy has a file or directory of the same name, or if |
| the dest_dir hierarchy already contains a directory where the source_dir |
| hierarchy has a file of the same name. |
| """ |
| if not os.path.exists(dest_dir): |
| os.mkdir(dest_dir) |
| for entry in os.listdir(source_dir): |
| source_entry = os.path.join(source_dir, entry) |
| dest_entry = os.path.join(dest_dir, entry) |
| if os.path.exists(dest_entry): |
| if os.path.isdir(source_entry) and os.path.isdir(dest_entry): |
| self._MoveDirectoryContents(source_entry, dest_entry) |
| else: |
| raise IOError('Cannot overwrite existing %s' % dest_entry) |
| else: |
| shutil.move(source_entry, dest_entry) |
| |
| _MAX_SIZE = 32 * 1000 * 1000 |
| |
| |
| def _ShouldSplitJar(self, path): |
| return (path.lower().endswith('.jar') and self.options.do_jar_splitting and |
| os.path.getsize(path) >= self._MAX_SIZE) |
| |
| def _SplitJar(self, jar_path, dest_dir): |
| """Split a source jar into two or more jars in the given dest_dir. |
| |
| Args: |
| jar_path: string that is the path to jar to be split. The contents of this |
| jar will be copied into the output jars, but the jar itself will not be |
| affected. |
| dest_dir: directory into which to put the jars that result from splitting |
| the input jar. |
| |
| Raises: |
| IOError: if the jar cannot be split. |
| """ |
| |
| exclude_suffixes = ( |
| set(self.options.jar_splitting_exclude_suffixes.split(',')) - set([''])) |
| include = lambda name: not any(name.endswith(s) for s in exclude_suffixes) |
| jarfile.SplitJar(jar_path, dest_dir, self._MAX_SIZE, include) |
| |
| @staticmethod |
| def _GetStaticFileList(staging_dir): |
| return _FilesMatching(os.path.join(staging_dir, '__static__')) |
| |
| def _CompileJspsIfAny(self, tools_dir, staging_dir): |
| """Compiles JSP files, if any, into .class files..""" |
| if self._MatchingFileExists(self._JSP_REGEX, staging_dir): |
| gen_dir = tempfile.mkdtemp() |
| try: |
| self._CompileJspsWithGenDir(tools_dir, staging_dir, gen_dir) |
| finally: |
| shutil.rmtree(gen_dir) |
| |
| def _CompileJspsWithGenDir(self, tools_dir, staging_dir, gen_dir): |
| staging_web_inf = os.path.join(staging_dir, 'WEB-INF') |
| lib_dir = os.path.join(staging_web_inf, 'lib') |
| |
| for jar_file in GetUserJspLibFiles(tools_dir): |
| self._CopyOrLinkFile( |
| jar_file, os.path.join(lib_dir, os.path.basename(jar_file))) |
| for jar_file in GetSharedJspLibFiles(tools_dir): |
| self._CopyOrLinkFile( |
| jar_file, os.path.join(lib_dir, os.path.basename(jar_file))) |
| |
| classes_dir = os.path.join(staging_web_inf, 'classes') |
| generated_web_xml = os.path.join(staging_web_inf, 'generated_web.xml') |
| |
| classpath = self._GetJspClasspath(tools_dir, classes_dir, gen_dir) |
| |
| command_and_args = [ |
| self.java_command, |
| '-classpath', classpath, |
| _LOCAL_JSPC_CLASS, |
| '-uriroot', staging_dir, |
| '-p', 'org.apache.jsp', |
| '-l', |
| '-v', |
| '-webinc', generated_web_xml, |
| '-d', gen_dir, |
| '-javaEncoding', self.options.compile_encoding, |
| ] |
| |
| status = subprocess.call(command_and_args) |
| if status: |
| raise CompileError( |
| 'Compilation of JSPs exited with status %d' % status) |
| |
| self._CompileJavaFiles(classpath, staging_web_inf, gen_dir) |
| |
| |
| self.web_xml = self._ReadWebXml(staging_dir) |
| |
| def _CompileJavaFiles(self, classpath, web_inf, jsp_class_dir): |
| """Compile all *.java files found under jsp_class_dir.""" |
| java_files = _FilesMatching(jsp_class_dir, lambda f: f.endswith('.java')) |
| if not java_files: |
| return |
| |
| command_and_args = [ |
| self.javac_command, |
| '-classpath', classpath, |
| '-d', jsp_class_dir, |
| '-encoding', self.options.compile_encoding, |
| ] + java_files |
| |
| status = subprocess.call(command_and_args) |
| if status: |
| raise CompileError( |
| 'Compilation of JSP-generated code exited with status %d' % status) |
| |
| if self.options.jar_jsps: |
| self._ZipJasperGeneratedFiles(web_inf, jsp_class_dir) |
| else: |
| web_inf_classes = os.path.join(web_inf, 'classes') |
| self._MoveDirectoryContents(jsp_class_dir, web_inf_classes) |
| |
| if self.options.delete_jsps: |
| jsps = _FilesMatching(os.path.dirname(web_inf), |
| lambda f: f.endswith('.jsp')) |
| for f in jsps: |
| os.remove(f) |
| |
| if self.options.do_jar_classes: |
| self._ZipWebInfClassesFiles(web_inf) |
| |
| @staticmethod |
| def _ZipJasperGeneratedFiles(web_inf, jsp_class_dir): |
| lib_dir = os.path.join(web_inf, 'lib') |
| jarfile.Make(jsp_class_dir, lib_dir, _COMPILED_JSP_JAR_NAME_PREFIX, |
| maximum_size=_MAX_COMPILED_JSP_JAR_SIZE, |
| include_predicate=lambda name: not name.endswith('.java')) |
| |
| @staticmethod |
| def _ZipWebInfClassesFiles(web_inf): |
| |
| |
| lib_dir = os.path.join(web_inf, 'lib') |
| classes_dir = os.path.join(web_inf, 'classes') |
| jarfile.Make(classes_dir, lib_dir, _CLASSES_JAR_NAME_PREFIX, |
| maximum_size=_MAX_COMPILED_JSP_JAR_SIZE) |
| shutil.rmtree(classes_dir) |
| |
| os.mkdir(classes_dir) |
| |
| @staticmethod |
| def _GetJspClasspath(tools_dir, classes_dir, gen_dir): |
| """Builds the classpath for the JSP Compilation system call.""" |
| lib_dir = os.path.join(os.path.dirname(classes_dir), 'lib') |
| elements = ( |
| GetImplLibs(tools_dir) + GetSharedLibFiles(tools_dir) + |
| [classes_dir, gen_dir] + |
| _FilesMatching( |
| lib_dir, lambda f: f.endswith('.jar') or f.endswith('.zip'))) |
| |
| return (os.pathsep).join(elements) |
| |
| @staticmethod |
| def _MatchingFileExists(regex, dir_path): |
| for _, _, files in os.walk(dir_path): |
| for f in files: |
| if re.search(regex, f): |
| return True |
| return False |
| |
| |
| def GetImplLibs(tools_dir): |
| return _GetLibsShallow(os.path.join(tools_dir, 'java', 'lib', 'impl')) |
| |
| |
| def GetSharedLibFiles(tools_dir): |
| return _GetLibsRecursive(os.path.join(tools_dir, 'java', 'lib', 'shared')) |
| |
| |
| def GetUserJspLibFiles(tools_dir): |
| return _GetLibsRecursive( |
| os.path.join(tools_dir, 'java', 'lib', 'tools', 'jsp')) |
| |
| |
| def GetSharedJspLibFiles(tools_dir): |
| return _GetLibsRecursive( |
| os.path.join(tools_dir, 'java', 'lib', 'shared', 'jsp')) |
| |
| |
| def _GetLibsRecursive(dir_path): |
| return _FilesMatching(dir_path, lambda f: f.endswith('.jar')) |
| |
| |
| def _GetLibsShallow(dir_path): |
| libs = [] |
| for f in os.listdir(dir_path): |
| if os.path.isfile(os.path.join(dir_path, f)) and f.endswith('.jar'): |
| libs.append(os.path.join(dir_path, f)) |
| return libs |
| |
| |
| def _FilesMatching(root, predicate=lambda f: True): |
| """Finds all files under the given root that match the given predicate. |
| |
| Args: |
| root: a string that is the absolute or relative path to a directory. |
| predicate: a function that takes a file name (without a directory) and |
| returns a truth value. |
| |
| Returns: |
| A list of strings that are the paths of every file under the given root |
| that satisfies the given predicate. The paths are absolute if and only if |
| the input root is absolute. |
| """ |
| matches = [] |
| for path, _, files in os.walk(root): |
| matches += [os.path.join(path, f) for f in files if predicate(f)] |
| return matches |
| |
| |
| def JavaHomeAndSuffix(): |
| """Find the directory that the JDK is installed in. |
| |
| The JDK install directory is expected to have a bin directory that contains |
| at a minimum the java and javac executables. If the environment variable |
| JAVA_HOME is set then it must point to such a directory. Otherwise, we look |
| for javac on the PATH and check that it is inside a JDK install directory. |
| |
| Returns: |
| A tuple where the first element is the JDK install directory and the second |
| element is a suffix that must be appended to executables in that directory |
| ('' on Unix-like systems, '.exe' on Windows). |
| |
| Raises: |
| RuntimeError: If JAVA_HOME is set but is not a JDK install directory, or |
| otherwise if a JDK install directory cannot be found based on the PATH. |
| """ |
| def ResultForJdkAt(path): |
| """Return (path, suffix) if path is a JDK install directory, else None.""" |
| def IsExecutable(binary): |
| return os.path.isfile(binary) and os.access(binary, os.X_OK) |
| |
| def ResultFor(path): |
| for suffix in ['', '.exe']: |
| if all(IsExecutable(os.path.join(path, 'bin', binary + suffix)) |
| for binary in ['java', 'javac', 'jar']): |
| return (path, suffix) |
| return None |
| |
| result = ResultFor(path) |
| if not result: |
| |
| |
| head, tail = os.path.split(path) |
| if tail == 'jre': |
| result = ResultFor(head) |
| return result |
| |
| java_home = os.getenv('JAVA_HOME') |
| if java_home: |
| result = ResultForJdkAt(java_home) |
| if result: |
| return result |
| else: |
| raise RuntimeError( |
| 'JAVA_HOME is set but does not reference a valid JDK: %s' % java_home) |
| for path_dir in os.environ['PATH'].split(os.pathsep): |
| maybe_root, last = os.path.split(path_dir) |
| if last == 'bin': |
| result = ResultForJdkAt(maybe_root) |
| if result: |
| return result |
| raise RuntimeError('Did not find JDK in PATH and JAVA_HOME is not set') |
| |
| |
| def _FindApiJars(lib_dir): |
| """Find the appengine-api-*.jar and its version. |
| |
| The version of an appengine-api-*.jar is the Specification-Version attribute |
| in the jar's manifest section whose Name is 'com/google/appengine/api/'. |
| |
| Args: |
| lib_dir: the base directory under which jars are to be found. |
| |
| Returns: |
| A dict from string to string, mapping all found API jars to their |
| corresponding versions. |
| |
| Raises: |
| IOError: if there was a problem reading the jars. |
| """ |
| result = {} |
| for jar_file in _FilesMatching(lib_dir, lambda f: f.endswith('.jar')): |
| manifest = jarfile.ReadManifest(jar_file) |
| if manifest: |
| section = manifest.sections.get('com/google/appengine/api/') |
| if section and 'Specification-Version' in section: |
| result[jar_file] = section['Specification-Version'] |
| return result |