From 2ccd27ce4e32da8832d2810f152b1752074bfd41 Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 19 Dec 2011 13:17:49 -0800 Subject: Manifest Merger tool. Change-Id: Iae9033f04654e03151c206b5dfb1fd8f47eea8f2 --- manifmerger/.classpath | 10 + manifmerger/.gitignore | 1 + manifmerger/.project | 17 + manifmerger/.settings/org.eclipse.jdt.core.prefs | 77 ++ manifmerger/Android.mk | 5 + manifmerger/etc/Android.mk | 10 + manifmerger/etc/manifest.txt | 2 + manifmerger/etc/manifmerger | 87 ++ manifmerger/src/Android.mk | 16 + .../src/com/android/manifmerger/ArgvParser.java | 116 ++ manifmerger/src/com/android/manifmerger/Main.java | 114 ++ .../com/android/manifmerger/ManifestMerger.java | 1293 ++++++++++++++++++++ .../src/com/android/manifmerger/XmlUtils.java | 458 +++++++ manifmerger/tests/Android.mk | 28 + .../android/manifmerger/ManifestMergerTest.java | 140 +++ .../manifmerger/ManifestMergerTestCase.java | 412 +++++++ .../src/com/android/manifmerger/data/00_noop.xml | 225 ++++ .../manifmerger/data/01_ignore_app_attr.xml | 68 + .../manifmerger/data/02_ignore_instrumentation.xml | 62 + .../android/manifmerger/data/10_activity_merge.xml | 378 ++++++ .../android/manifmerger/data/11_activity_dup.xml | 387 ++++++ .../com/android/manifmerger/data/12_alias_dup.xml | 205 ++++ .../android/manifmerger/data/13_service_dup.xml | 155 +++ .../android/manifmerger/data/14_receiver_dup.xml | 176 +++ .../android/manifmerger/data/15_provider_dup.xml | 145 +++ .../android/manifmerger/data/20_uses_lib_merge.xml | 176 +++ .../manifmerger/data/21_uses_lib_errors.xml | 202 +++ .../manifmerger/data/25_permission_merge.xml | 255 ++++ .../android/manifmerger/data/26_permission_dup.xml | 303 +++++ .../manifmerger/data/28_uses_perm_merge.xml | 152 +++ .../android/manifmerger/data/30_uses_sdk_ok.xml | 80 ++ .../manifmerger/data/32_uses_sdk_minsdk_ok.xml | 64 + .../data/33_uses_sdk_minsdk_conflict.xml | 110 ++ .../data/36_uses_sdk_targetsdk_warning.xml | 73 ++ .../manifmerger/data/40_uses_feat_merge.xml | 178 +++ .../manifmerger/data/41_uses_feat_errors.xml | 205 ++++ .../manifmerger/data/45_uses_feat_gles_once.xml | 124 ++ .../data/47_uses_feat_gles_conflict.xml | 160 +++ .../manifmerger/data/50_uses_conf_warning.xml | 158 +++ .../data/52_support_screens_warning.xml | 158 +++ .../manifmerger/data/54_compat_screens_warning.xml | 200 +++ .../manifmerger/data/56_support_gltext_warning.xml | 148 +++ 42 files changed, 7333 insertions(+) create mode 100644 manifmerger/.classpath create mode 100644 manifmerger/.gitignore create mode 100644 manifmerger/.project create mode 100755 manifmerger/.settings/org.eclipse.jdt.core.prefs create mode 100644 manifmerger/Android.mk create mode 100644 manifmerger/etc/Android.mk create mode 100644 manifmerger/etc/manifest.txt create mode 100755 manifmerger/etc/manifmerger create mode 100644 manifmerger/src/Android.mk create mode 100755 manifmerger/src/com/android/manifmerger/ArgvParser.java create mode 100644 manifmerger/src/com/android/manifmerger/Main.java create mode 100755 manifmerger/src/com/android/manifmerger/ManifestMerger.java create mode 100755 manifmerger/src/com/android/manifmerger/XmlUtils.java create mode 100755 manifmerger/tests/Android.mk create mode 100755 manifmerger/tests/src/com/android/manifmerger/ManifestMergerTest.java create mode 100755 manifmerger/tests/src/com/android/manifmerger/ManifestMergerTestCase.java create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/00_noop.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/01_ignore_app_attr.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/02_ignore_instrumentation.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/10_activity_merge.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/11_activity_dup.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/12_alias_dup.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/13_service_dup.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/14_receiver_dup.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/15_provider_dup.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/20_uses_lib_merge.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/21_uses_lib_errors.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/25_permission_merge.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/26_permission_dup.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/28_uses_perm_merge.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/30_uses_sdk_ok.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/32_uses_sdk_minsdk_ok.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/33_uses_sdk_minsdk_conflict.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/36_uses_sdk_targetsdk_warning.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/40_uses_feat_merge.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/41_uses_feat_errors.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/45_uses_feat_gles_once.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/47_uses_feat_gles_conflict.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/50_uses_conf_warning.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/52_support_screens_warning.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/54_compat_screens_warning.xml create mode 100755 manifmerger/tests/src/com/android/manifmerger/data/56_support_gltext_warning.xml (limited to 'manifmerger') diff --git a/manifmerger/.classpath b/manifmerger/.classpath new file mode 100644 index 0000000..d042f23 --- /dev/null +++ b/manifmerger/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/manifmerger/.gitignore b/manifmerger/.gitignore new file mode 100644 index 0000000..ba077a4 --- /dev/null +++ b/manifmerger/.gitignore @@ -0,0 +1 @@ +bin diff --git a/manifmerger/.project b/manifmerger/.project new file mode 100644 index 0000000..0d4dcb4 --- /dev/null +++ b/manifmerger/.project @@ -0,0 +1,17 @@ + + + ManifestMerger + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/manifmerger/.settings/org.eclipse.jdt.core.prefs b/manifmerger/.settings/org.eclipse.jdt.core.prefs new file mode 100755 index 0000000..8c01a02 --- /dev/null +++ b/manifmerger/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,77 @@ +#Tue Dec 27 15:48:05 PST 2011 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=enabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=ignore +org.eclipse.jdt.core.compiler.problem.emptyStatement=warning +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=ignore +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=ignore +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=ignore +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=enabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/manifmerger/Android.mk b/manifmerger/Android.mk new file mode 100644 index 0000000..245744a --- /dev/null +++ b/manifmerger/Android.mk @@ -0,0 +1,5 @@ +# Copyright 2011 The Android Open Source Project +# +MANIFMERGER_LOCAL_DIR := $(call my-dir) +include $(MANIFMERGER_LOCAL_DIR)/src/Android.mk +include $(MANIFMERGER_LOCAL_DIR)/etc/Android.mk diff --git a/manifmerger/etc/Android.mk b/manifmerger/etc/Android.mk new file mode 100644 index 0000000..6b180ab --- /dev/null +++ b/manifmerger/etc/Android.mk @@ -0,0 +1,10 @@ +# Copyright 2011 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_PREBUILT_EXECUTABLES := manifmerger +LOCAL_MODULE_TAGS := optional + +include $(BUILD_HOST_PREBUILT) + diff --git a/manifmerger/etc/manifest.txt b/manifmerger/etc/manifest.txt new file mode 100644 index 0000000..bfc9524 --- /dev/null +++ b/manifmerger/etc/manifest.txt @@ -0,0 +1,2 @@ +Main-Class: com.android.manifestmerger.Main +Class-Path: sdklib.jar diff --git a/manifmerger/etc/manifmerger b/manifmerger/etc/manifmerger new file mode 100755 index 0000000..1e2c2a7 --- /dev/null +++ b/manifmerger/etc/manifmerger @@ -0,0 +1,87 @@ +#!/bin/sh +# Copyright 2011, The Android Open Source Project +# +# 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. + +# Set up prog to be the path of this script, including following symlinks, +# and set up progdir to be the fully-qualified pathname of its directory. +prog="$0" +while [ -h "${prog}" ]; do + newProg=`/bin/ls -ld "${prog}"` + newProg=`expr "${newProg}" : ".* -> \(.*\)$"` + if expr "x${newProg}" : 'x/' >/dev/null; then + prog="${newProg}" + else + progdir=`dirname "${prog}"` + prog="${progdir}/${newProg}" + fi +done +oldwd=`pwd` +progdir=`dirname "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/`basename "${prog}"` +cd "${oldwd}" + +jarfile=manifmerger.jar +frameworkdir="$progdir" +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/tools/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/framework +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + echo `basename "$prog"`": can't find $jarfile" + exit 1 +fi + + +# Check args. +if [ debug = "$1" ]; then + # add this in for debugging + java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y + shift 1 +else + java_debug= +fi + +java_cmd="java" + +# Mac OS X needs an additional arg, or you get an "illegal thread" complaint. +if [ `uname` = "Darwin" ]; then + os_opts="-XstartOnFirstThread" +else + os_opts= +fi + +if [ `uname` = "Linux" ]; then + export GDK_NATIVE_WINDOWS=true +fi + +if [ "$OSTYPE" = "cygwin" ] ; then + jarpath=`cygpath -w "$frameworkdir/$jarfile"` + progdir=`cygpath -w "$progdir"` +else + jarpath="$frameworkdir/$jarfile" +fi + +# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored +exec "$java_cmd" \ + -Xmx256M $os_opts $java_debug \ + -Dcom.android.manifmergerdir="$progdir" \ + -classpath "$jarpath" \ + com.android.manifestmerger.Main "$@" diff --git a/manifmerger/src/Android.mk b/manifmerger/src/Android.mk new file mode 100644 index 0000000..5e533eb --- /dev/null +++ b/manifmerger/src/Android.mk @@ -0,0 +1,16 @@ +# Copyright 2011 The Android Open Source Project +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_JAVA_RESOURCE_DIRS := + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_JAR_MANIFEST := ../etc/manifest.txt +LOCAL_JAVA_LIBRARIES := \ + common \ + sdklib +LOCAL_MODULE := manifmerger +LOCAL_MODULE_TAGS := optional + +include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/manifmerger/src/com/android/manifmerger/ArgvParser.java b/manifmerger/src/com/android/manifmerger/ArgvParser.java new file mode 100755 index 0000000..5aed998 --- /dev/null +++ b/manifmerger/src/com/android/manifmerger/ArgvParser.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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. + */ + +package com.android.manifmerger; + +import com.android.sdklib.ISdkLog; +import com.android.sdklib.util.CommandLineParser; + +import java.util.List; + + +/** + * Specific command-line flags for the {@link ManifestMerger}. + */ +class ArgvParser extends CommandLineParser { + + /* + * Steps needed to add a new action: + * - Each action is defined as a "verb object" followed by parameters. + * - Either reuse a VERB_ constant or define a new one. + * - Either reuse an OBJECT_ constant or define a new one. + * - Add a new entry to mAction with a one-line help summary. + * - In the constructor, add a define() call for each parameter (either mandatory + * or optional) for the given action. + */ + + public final static String VERB_MERGE = "merge"; //$NON-NLS-1$ + public static final String KEY_OUT = "out"; //$NON-NLS-1$ + public static final String KEY_MAIN = "main"; //$NON-NLS-1$ + public static final String KEY_LIBS = "libs"; //$NON-NLS-1$ + + /** + * Action definitions for ManifestMerger command line. + *

+ * This list serves two purposes: first it is used to know which verb/object + * actions are acceptable on the command-line; second it provides a summary + * for each action that is printed in the help. + *

+ * Each entry is a string array with: + *

+ */ + private final static String[][] ACTIONS = { + + { VERB_MERGE, NO_VERB_OBJECT, + "Merge two or more manifests." }, + + }; + + public ArgvParser(ISdkLog logger) { + super(logger, ACTIONS); + + // The following defines the parameters of the actions defined in mAction. + + // --- merge manifest --- + + define(Mode.STRING, true, + VERB_MERGE, NO_VERB_OBJECT, "o", KEY_OUT, //$NON-NLS-1$ + "Output path (where to write the merged manifest). Use - for stdout.", null); + + define(Mode.STRING, true, + VERB_MERGE, NO_VERB_OBJECT, "1", KEY_MAIN, //$NON-NLS-1$ + "Path of the main manifest (what to merge *into*)", null); + + define(Mode.STRING_ARRAY, true, + VERB_MERGE, NO_VERB_OBJECT, "2", KEY_LIBS, //$NON-NLS-1$ + "Paths of library manifests to be merged into the main one.", + null); + } + + @Override + public boolean acceptLackOfVerb() { + return true; + } + + // -- some helpers for generic action flags + + /** Helper to retrieve the --out value. */ + public String getParamOut() { + return (String) getValue(null, null, KEY_OUT); + } + + /** Helper to retrieve the --main value. */ + public String getParamMain() { + return (String) getValue(null, null, KEY_MAIN); + } + + /** + * Helper to retrieve the --libs values. + */ + public String[] getParamLibs() { + Object v = getValue(null, null, KEY_LIBS); + if (v instanceof List) { + List a = (List) v; + return a.toArray(new String[a.size()]); + } + return null; + } +} diff --git a/manifmerger/src/com/android/manifmerger/Main.java b/manifmerger/src/com/android/manifmerger/Main.java new file mode 100644 index 0000000..e2e6f30 --- /dev/null +++ b/manifmerger/src/com/android/manifmerger/Main.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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. + */ + +package com.android.manifmerger; + +import com.android.sdklib.ISdkLog; + +import java.io.File; + +/** + * Command-line entry point of the Manifest Merger. + * The goal of the manifest merger is to merge library manifest into a main application manifest. + * See {@link ManifestMerger} for the exact merging rules. + *

+ * The command-line version creates a {@link ManifestMerger} + * which takes file arguments from the command-line and dumps all errors and warnings on the + * stdout/stderr console. + *

+ * Usage:
+ * {@code $ manifmerger merge --main main_manifest.xml --libs lib1.xml lib2.xml --out result.xml} + *

+ * When used as a library, please call {@link ManifestMerger#process(File, File, File[])} directly. + */ +public class Main { + + /** Logger object. Use this to print normal output, warnings or errors. Never null. */ + private ISdkLog mSdkLog; + /** Command line parser. Never null. */ + private ArgvParser mArgvParser; + + public static void main(String[] args) { + new Main().run(args); + } + + /** + * Runs the sdk manager app + */ + private void run(String[] args) { + createLogger(); + + mArgvParser = new ArgvParser(mSdkLog); + mArgvParser.parseArgs(args); + + // Create a new ManifestMerger and call its process method. + // It will take care of validating its own arguments. + ManifestMerger mm = new ManifestMerger(mSdkLog); + + String[] libPaths = mArgvParser.getParamLibs(); + File[] libFiles = new File[libPaths.length]; + for (int n = libPaths.length - 1; n >= 0; n--) { + libFiles[n] = new File(libPaths[n]); + } + + boolean ok = mm.process( + new File(mArgvParser.getParamOut()), + new File(mArgvParser.getParamMain()), + libFiles + ); + System.exit(ok ? 0 : 1); + } + + /** + * Creates the {@link #mSdkLog} object. + * This logger prints to the attached console. + */ + private void createLogger() { + mSdkLog = new ISdkLog() { + @Override + public void error(Throwable t, String errorFormat, Object... args) { + if (errorFormat != null) { + System.err.printf("Error: " + errorFormat, args); + if (!errorFormat.endsWith("\n")) { + System.err.printf("\n"); + } + } + if (t != null) { + System.err.printf("Error: %s\n", t.getMessage()); + } + } + + @Override + public void warning(String warningFormat, Object... args) { + System.out.printf("Warning: " + warningFormat, args); + if (!warningFormat.endsWith("\n")) { + System.out.printf("\n"); + } + } + + @Override + public void printf(String msgFormat, Object... args) { + System.out.printf(msgFormat, args); + } + }; + } + + /** For testing */ + public void setLogger(ISdkLog logger) { + mSdkLog = logger; + } + +} diff --git a/manifmerger/src/com/android/manifmerger/ManifestMerger.java b/manifmerger/src/com/android/manifmerger/ManifestMerger.java new file mode 100755 index 0000000..6eac978 --- /dev/null +++ b/manifmerger/src/com/android/manifmerger/ManifestMerger.java @@ -0,0 +1,1293 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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. + */ + +package com.android.manifmerger; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.ISdkLog; +import com.android.sdklib.SdkConstants; +import com.android.sdklib.xml.AndroidXPathFactory; + +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; + +/** + * Merges a library manifest into a main application manifest. + *

+ * To use, create with {@link ManifestMerger#ManifestMerger(ISdkLog)} then + * call {@link ManifestMerger#process(File, File, File[])}. + *

+ *

 Merge operations:
+ * - root manifest: attributes ignored, warn if defined.
+ * - application:
+ *      {@code @attributes}: ignored in libs
+ *      C- activity / activity-alias / service / receiver / provider
+ *          => Merge as-is. Error if exists in the destination (same {@code @name})
+ *             unless the definitions are exactly the same.
+ *             New elements are always merged at the end of the application element.
+ *          => Indicate if there's a dup.
+ *      D- uses-library
+ *          => Merge. OK if already exists same {@code @name}.
+ *          => Merge {@code @required}: true>false.
+ * A- instrumentation:
+ *      => Do not merge. ignore the ones from libs.
+ * C- permission / permission-group / permission-tree:
+ *      => Merge as-is. Error if exists in the destination (same {@code @name})
+ *         unless the definitions are exactly the same.
+ * C- uses-permission:
+ *      => Add. OK if already defined.
+ * E- uses-sdk:
+ *      {@code @minSdkVersion}: error if dest<lib. Never automatically change dest minsdk.
+ *      {@code @targetSdkVersion}: warning if dest<lib.
+ *                                 Never automatically change dest targetsdk.
+ *      {@code @maxSdkVersion}: obsolete, ignored. Not used in comparisons and not merged.
+ * D- uses-feature with {@code @name}:
+ *      => Merge with same {@code @name}
+ *      => Merge {@code @required}: true>false.
+ *      - Do not merge any {@code @glEsVersion} attribute at this point.
+ * F- uses-feature with {@code @glEsVersion}:
+ *      => Error if defined in lib+dest with dest<lib. Never automatically change dest.
+ * B- uses-configuration:
+ *      => There can be many. Error if source defines one that is not an exact match in dest.
+ *      (e.g. right now app must manually define something that matches exactly each lib)
+ * B- supports-screens / compatible-screens:
+ *      => Do not merge.
+ *      => Error (warn?) if defined in lib and not strictly the same as in dest.
+ * B- supports-gl-texture:
+ *      => Do not merge. Can have more than one.
+ *      => Error (warn?) if defined in lib and not present as-is in dest.
+ *
+ * Strategies:
+ * A = Ignore, do not merge (no-op).
+ * B = Do not merge but if defined in both must match equally.
+ * C = Must not exist in dest or be exactly the same (key is the {@code @name} attribute).
+ * D = Add new or merge with same key {@code @name}, adjust {@code @required} true>false.
+ * E, F = Custom strategies; see above.
+ *
+ * What happens when merging libraries with conflicting information?
+ * Say for example a main manifest has a minSdkVersion of 3, whereas libraries have
+ * a minSdkVersion of 4 and 11. We could have 2 point of views:
+ * - Play it safe: If we have a library with a minSdkVersion of 11, it means this
+ *   library code knows it can't work reliably on a lower API level. So the safest end
+ *   result would be a merged manifest with the highest minSdkVersion of all libraries.
+ * - Trust the main manifest: When an app declares a given minSdkVersion, it also expects
+ *   to run a given range of devices. If we change the final minSdkVersion, the app won't
+ *   be available on as many devices as the developer might expect. And as a counterpoint
+ *   to issue 1, the app may be careful and not call the library without checking the
+ *   necessary features or APIs are available before hand.
+ * Both points of views are conflicting. The solution taken here is to be conservative
+ * and generate an error rather than merge and change a value that might be surprising.
+ * On the other hand this can be problematic and force a developer to keep the main
+ * manifest in sync with the libraries ones, in essence reducing the usefulness of the
+ * automated merge to pure trivial cases. The idea is to just start this way and enhance
+ * or revisit the mechanism later.
+ * 
+ */ +public class ManifestMerger { + + /** Logger object. Never null. */ + private ISdkLog mSdkLog; + private XPath mXPath; + private Document mMainDoc; + + private String NS_URI = SdkConstants.NS_RESOURCES; + private String NS_PREFIX = AndroidXPathFactory.DEFAULT_NS_PREFIX; + private int destMinSdk; + + public ManifestMerger(ISdkLog log) { + mSdkLog = log; + } + + /** + * Performs the merge operation. + *

+ * This does NOT stop on errors, in an attempt to accumulate as much + * info as possible to return to the user. + * Unless it failed to read the main manifest, a result file will be + * created. However if process() returns false, the file should not + * be used except for debugging purposes. + * + * @param outputFile The output path to generate. Can be the same as the main path. + * @param mainFile The main manifest paths to read. What we merge into. + * @param libraryFiles The library manifest paths to read. Must not be null. + * @return True if the merge was completed, false otherwise. + */ + public boolean process(File outputFile, File mainFile, File[] libraryFiles) { + Document mainDoc = XmlUtils.parseDocument(mainFile, mSdkLog); + if (mainDoc == null) { + return false; + } + + boolean success = process(mainDoc, libraryFiles); + + if (!XmlUtils.printXmlFile(mainDoc, outputFile, mSdkLog)) { + success = false; + } + return success; + } + + /** + * Performs the merge operation in-place in the given DOM. + *

+ * This does NOT stop on errors, in an attempt to accumulate as much + * info as possible to return to the user. + * + * @param mainDoc The document to merge into. Will be modified in-place. + * @param libraryFiles The library manifest paths to read. Must not be null. + * @return True on success, false if any error occurred (printed to the {@link ISdkLog}). + */ + public boolean process(Document mainDoc, File[] libraryFiles) { + + boolean success = true; + mMainDoc = mainDoc; + + String prefix = XmlUtils.lookupNsPrefix(mainDoc, SdkConstants.NS_RESOURCES); + mXPath = AndroidXPathFactory.newXPath(prefix); + + for (File libFile : libraryFiles) { + Document libDoc = XmlUtils.parseDocument(libFile, mSdkLog); + if (libDoc == null || !mergeLibDoc(libDoc)) { + success = false; + } + } + + mXPath = null; + mMainDoc = null; + return success; + } + + // -------- + + /** + * Merges the given library manifest into the destination manifest. + * See {@link ManifestMerger} for merge details. + * + * @param libDoc The library document to merge from. Must not be null. + * @return True on success, false if any error occurred (printed to the {@link ISdkLog}). + */ + private boolean mergeLibDoc(Document libDoc) { + + boolean err = false; + + // Strategy B + err |= !doNotMergeCheckEqual("/manifest/uses-configuration", libDoc); //$NON-NLS-1$ + err |= !doNotMergeCheckEqual("/manifest/supports-screens", libDoc); //$NON-NLS-1$ + err |= !doNotMergeCheckEqual("/manifest/compatible-screens", libDoc); //$NON-NLS-1$ + err |= !doNotMergeCheckEqual("/manifest/supports-gl-texture", libDoc); //$NON-NLS-1$ + + // Strategy C + err |= !mergeNewOrEqual( + "/manifest/application/activity", //$NON-NLS-1$ + "name", //$NON-NLS-1$ + libDoc, + true); + err |= !mergeNewOrEqual( + "/manifest/application/activity-alias", //$NON-NLS-1$ + "name", //$NON-NLS-1$ + libDoc, + true); + err |= !mergeNewOrEqual( + "/manifest/application/service", //$NON-NLS-1$ + "name", //$NON-NLS-1$ + libDoc, + true); + err |= !mergeNewOrEqual( + "/manifest/application/receiver", //$NON-NLS-1$ + "name", //$NON-NLS-1$ + libDoc, + true); + err |= !mergeNewOrEqual( + "/manifest/application/provider", //$NON-NLS-1$ + "name", //$NON-NLS-1$ + libDoc, + true); + err |= !mergeNewOrEqual( + "/manifest/permission", //$NON-NLS-1$ + "name", //$NON-NLS-1$ + libDoc, + false); + err |= !mergeNewOrEqual( + "/manifest/permission-group", //$NON-NLS-1$ + "name", //$NON-NLS-1$ + libDoc, + false); + err |= !mergeNewOrEqual( + "/manifest/permission-tree", //$NON-NLS-1$ + "name", //$NON-NLS-1$ + libDoc, + false); + err |= !mergeNewOrEqual( + "/manifest/uses-permission", //$NON-NLS-1$ + "name", //$NON-NLS-1$ + libDoc, + false); + + // Strategy D + err |= !mergeAdjustRequired( + "/manifest/application/uses-library", //$NON-NLS-1$ + "name", //$NON-NLS-1$ + "required", //$NON-NLS-1$ + libDoc, + null /*alternateKeyAttr*/); + err |= !mergeAdjustRequired( + "/manifest/uses-feature", //$NON-NLS-1$ + "name", //$NON-NLS-1$ + "required", //$NON-NLS-1$ + libDoc, + "glEsVersion" /*alternateKeyAttr*/); + + // Strategy E + err |= !checkSdkVersion(libDoc); + + // Strategy F + err |= !checkGlEsVersion(libDoc); + + return !err; + } + + /** + * Do not merge anything. Instead it checks that the requested elements from the + * given library are all present and equal in the destination and prints a warning + * if it's not the case. + *

+ * For example if a library supports a given screen configuration, print a + * warning if the main manifest doesn't indicate the app supports the same configuration. + * We should not merge it since we don't want to silently give the impression an app + * supports a configuration just because it uses a library which does. + * On the other hand we don't want to silently ignore this fact. + *

+ * TODO there should be a way to silence this warning. + * The current behavior is certainly arbitrary and needs to be tweaked somehow. + * + * @param path The XPath of the elements to merge from the library. Must not be null. + * @param libDoc The library document to merge from. Must not be null. + * @return True on success, false if any error occurred (printed to the {@link ISdkLog}). + */ + private boolean doNotMergeCheckEqual(String path, Document libDoc) { + + for (Element src : findElements(libDoc, path)) { + + boolean found = false; + + for (Element dest : findElements(mMainDoc, path)) { + if (compareElements(src, dest, false, null /*diff*/, null /*keyAttr*/)) { + found = true; + break; + } + } + + if (!found) { + mSdkLog.warning("[%1$s] %2$s missing from %3$s:\n%4$s", + fileLineInfo(src, "library"), + path, + xmlFileName(mMainDoc, "main manifest"), + XmlUtils.dump(src, false /*nextSiblings*/)); + } + } + + return true; + } + + /** + * Merges the requested elements from the library in the main document. + * The key attribute name is used to identify the same elements. + * Merged elements must either not exist in the destination or be identical. + *

+ * When merging, append to the end of the application element. + * Also merges any preceding whitespace and up to one comment just prior to the merged element. + * + * @param path The XPath of the elements to merge from the library. Must not be null. + * @param keyAttr The Android-namespace attribute used as key to identify similar elements. + * E.g. "name" for "android:name" + * @param libDoc The library document to merge from. Must not be null. + * @param warnDups When true, will print a warning when a library definition is already + * present in the destination and is equal. + * @return True on success, false if any error occurred (printed to the {@link ISdkLog}). + */ + private boolean mergeNewOrEqual( + String path, + String keyAttr, + Document libDoc, + boolean warnDups) { + + // The parent of XPath /p1/p2/p3 is /p1/p2. To find it, delete the last "/segment" + int pos = path.lastIndexOf('/'); + assert pos > 1; + String parentPath = path.substring(0, pos); + Element parent = findFirstElement(mMainDoc, parentPath); + assert parent != null; + if (parent == null) { + mSdkLog.error(null, "[%1$s] Could not find element %2$s.", + xmlFileName(mMainDoc, "main manifest"), + parentPath); + return false; + } + + boolean success = true; + + nextSource: for (Element src : findElements(libDoc, path)) { + Attr attr = src.getAttributeNodeNS(NS_URI, keyAttr); + String name = attr == null ? "" : attr.getNodeValue(); //$NON-NLS-1$ + if (name.length() == 0) { + mSdkLog.error(null, "[%1$s] Undefined '%2$s' attribute in %3$s.", + fileLineInfo(src, "library"), + keyAttr, path); + success = false; + continue; + } + + // Look for the same item in the destination + List dests = findElements(mMainDoc, path, keyAttr, name); + if (dests.size() > 1) { + // This should not be happening. We'll just use the first one found in this case. + mSdkLog.warning("[%1$s] has more than one %2$s[@%3$s=%4$s] element.", + fileLineInfo(dests.get(0), "main manifest"), + path, keyAttr, name); + } + for (Element dest : dests) { + // If there's already a similar node in the destination, check it's identical. + StringBuilder diff = new StringBuilder(); + if (compareElements(src, dest, false, diff, keyAttr)) { + // Same element. Skip. + if (warnDups) { + mSdkLog.printf("[%1$s, %2$s] Skipping identical %3$s[@%4$s=%5$s] element.", + fileLineInfo(src, "library"), + fileLineInfo(dest, "main manifest"), + path, keyAttr, name); + } + continue nextSource; + } else { + // Print the diff we got from the comparison. + mSdkLog.error(null, + "[%1$s, %2$s] Trying to merge incompatible %3$s[@%4$s=%5$s] element:\n%6$s", + fileLineInfo(src, "library"), + fileLineInfo(dest, "main manifest"), + path, keyAttr, name, diff.toString()); + success = false; + continue nextSource; + } + } + + // Ready to merge element src. Select which previous siblings to merge. + Node start = selectPreviousSiblings(src); + + insertAtEndOf(parent, start, src); + } + + return success; + } + + /** + * Merge elements as identified by their key name attribute. + * The element must have an option boolean "required" attribute which can be either "true" or + * "false". Default is true if the attribute is misisng. When merging, a "false" is superseded + * by a "true" (explicit or implicit). + *

+ * When merging, this does NOT merge any other attributes than {@code keyAttr} and + * {@code requiredAttr}. + * + * @param path The XPath of the elements to merge from the library. Must not be null. + * @param keyAttr The Android-namespace attribute used as key to identify similar elements. + * E.g. "name" for "android:name" + * @param requiredAttr The name of the Android-namespace boolean attribute that must be merged. + * Typically should be "required". + * @param libDoc The library document to merge from. Must not be null. + * @param alternateKeyAttr When non-null, this is an alternate valid key attribute. If the + * default key attribute is missing, we won't output a warning if the alternate one is + * present. + * @return True on success, false if any error occurred (printed to the {@link ISdkLog}). + */ + private boolean mergeAdjustRequired( + String path, + String keyAttr, + String requiredAttr, + Document libDoc, + @Nullable String alternateKeyAttr) { + + // The parent of XPath /p1/p2/p3 is /p1/p2. To find it, delete the last "/segment" + int pos = path.lastIndexOf('/'); + assert pos > 1; + String parentPath = path.substring(0, pos); + Element parent = findFirstElement(mMainDoc, parentPath); + assert parent != null; + if (parent == null) { + mSdkLog.error(null, "[%1$s] Could not find element %2$s.", + xmlFileName(mMainDoc, "main manifest"), + parentPath); + return false; + } + + boolean success = true; + + for (Element src : findElements(libDoc, path)) { + Attr attr = src.getAttributeNodeNS(NS_URI, keyAttr); + String name = attr == null ? "" : attr.getNodeValue().trim(); //$NON-NLS-1$ + if (name.length() == 0) { + if (alternateKeyAttr != null) { + attr = src.getAttributeNodeNS(NS_URI, alternateKeyAttr); + String s = attr == null ? "" : attr.getNodeValue().trim(); //$NON-NLS-1$ + if (s.length() != 0) { + // This element lacks the keyAttr but has the alternateKeyAttr. Skip it. + continue; + } + } + + mSdkLog.error(null, "[%1$s] Undefined '%2$s' attribute in %3$s.", + fileLineInfo(src, "library"), + keyAttr, path); + success = false; + continue; + } + + // Look for the same item in the destination + List dests = findElements(mMainDoc, path, keyAttr, name); + if (dests.size() > 1) { + // This should not be happening. We'll just use the first one found in this case. + mSdkLog.warning("[%1$s] has more than one %2$s[@%3$s=%4$s] element.", + fileLineInfo(dests.get(0), "main manifest"), + path, keyAttr, name); + } + if (dests.size() > 0) { + attr = src.getAttributeNodeNS(NS_URI, requiredAttr); + String value = attr == null ? "true" : attr.getNodeValue(); //$NON-NLS-1$ + if (value == null || !(value.equals("true") || value.equals("false"))) { + mSdkLog.warning("[%1$s] Invalid attribute '%2$s' in %3$s[@%4$s=%5$s] element:\nExpected 'true' or 'false' but found '%6$s'.", + fileLineInfo(src, "library"), + requiredAttr, path, keyAttr, name, value); + continue; + } + boolean boolE = Boolean.parseBoolean(value); + + for (Element dest : dests) { + // Destination node exists. Compare the required attributes. + + attr = dest.getAttributeNodeNS(NS_URI, requiredAttr); + value = attr == null ? "true" : attr.getNodeValue(); //$NON-NLS-1$ + if (value == null || !(value.equals("true") || value.equals("false"))) { + mSdkLog.warning("[%1$s] Invalid attribute '%2$s' in %3$s[@%4$s=%5$s] element:\nExpected 'true' or 'false' but found '%6$s'.", + fileLineInfo(dest, "main manifest"), + requiredAttr, path, keyAttr, name, value); + continue; + } + boolean boolD = Boolean.parseBoolean(value); + + if (!boolD && boolE) { + // Required attributes differ: destination is false and source was true + // so we need to change the destination to true. + + // If attribute was already in the destination, change it in place + if (attr != null) { + attr.setNodeValue("true"); //$NON-NLS-1$ + } else { + // Otherwise, do nothing. The destination doesn't have the + // required=true attribute, and true is the default value. + // Consequently not setting is the right thing to do. + + // -- code snippet for reference -- + // If we wanted to create a new attribute, we'd use the code + // below. There's a simpler call to d.setAttributeNS(ns, name, value) + // but experience shows that it would create a new prefix out of the + // blue instead of looking it up. + // + // Attr a = d.getOwnerDocument().createAttributeNS(NS_URI, requiredAttr); + // String prefix = d.lookupPrefix(NS_URI); + // if (prefix != null) { + // a.setPrefix(prefix); + // } + // a.setValue("true"); //$NON-NLS-1$ + // d.setAttributeNodeNS(attr); + } + } + } + } else { + // Destination doesn't exist. We simply merge the source element. + // Select which previous siblings to merge. + Node start = selectPreviousSiblings(src); + + Node node = insertAtEndOf(parent, start, src); + + NamedNodeMap attrs = node.getAttributes(); + if (attrs != null) { + for (int i = 0; i < attrs.getLength(); i++) { + Node a = attrs.item(i); + if (a.getNodeType() == Node.ATTRIBUTE_NODE) { + boolean keep = NS_URI.equals(a.getNamespaceURI()); + if (keep) { + name = a.getLocalName(); + keep = keyAttr.equals(name) || requiredAttr.equals(name); + } + if (!keep) { + attrs.removeNamedItemNS(NS_URI, name); + // Restart the loop from index 0 since there's no + // guarantee on the order of the nodes in the "map". + // This makes it O(n+2n) at most, where n is [2..3] in + // a typical case. + i = -1; + } + } + } + } + } + } + + return success; + } + + + + /** + * Checks (but does not merge) uses-feature glEsVersion attribute using the following rules: + *

+     * - Error if defined in lib+dest with dest<lib.
+     * - Never automatically change dest.
+     * - Default implied value is 1.0 (0x00010000).
+     * 
+ * + * @param libDoc The library document to merge from. Must not be null. + * @return True on success, false if any error occurred (printed to the {@link ISdkLog}). + */ + private boolean checkGlEsVersion(Document libDoc) { + + String parentPath = "/manifest"; //$NON-NLS-1$ + Element parent = findFirstElement(mMainDoc, parentPath); + assert parent != null; + if (parent == null) { + mSdkLog.error(null, "[%1$s] Could not find element %2$s.", + xmlFileName(mMainDoc, "main manifest"), + parentPath); + return false; + } + + // Find the max glEsVersion on the destination side + String path = "/manifest/uses-feature"; //$NON-NLS-1$ + String keyAttr = "glEsVersion"; //$NON-NLS-1$ + long destGlEsVersion = 0x00010000L; // default minimum is 1.0 + Element destNode = null; + boolean result = true; + for (Element dest : findElements(mMainDoc, path)) { + Attr attr = dest.getAttributeNodeNS(NS_URI, keyAttr); + String value = attr == null ? "" : attr.getNodeValue().trim(); //$NON-NLS-1$ + if (value.length() != 0) { + try { + // Note that the value can be an hex number such as 0x00020001 so we + // need Integer.decode instead of Integer.parseInt. + // Note: Integer.decode cannot handle "ffffffff", see JDK issue 6624867 + // so we just treat the version as a long and test like this, ignoring + // the fact that a value of 0xFFFF/.0xFFFF is probably invalid anyway + // in the context of glEsVersion. + long version = Long.decode(value); + if (version >= destGlEsVersion) { + destGlEsVersion = version; + destNode = dest; + } else if (version < 0x00010000) { + mSdkLog.warning("[%1$s] Ignoring because it's smaller than 1.0.", + fileLineInfo(dest, "main manifest"), + value); + } + } catch (NumberFormatException e) { + // Note: NumberFormatException.toString() has no interesting information + // so we don't output it. + mSdkLog.error(null, + "[%1$s] Failed to parse : must be an integer in the form 0x00020001.", + fileLineInfo(dest, "main manifest"), + value); + result = false; + } + } + } + + // If we found at least one valid with no error, use that, otherwise bail out. + if (!result && destNode == null) { + return false; + } + + // Now find the max glEsVersion on the source side. + + long srcGlEsVersion = 0x00010000L; // default minimum is 1.0 + Element srcNode = null; + result = true; + for (Element src : findElements(libDoc, path)) { + Attr attr = src.getAttributeNodeNS(NS_URI, keyAttr); + String value = attr == null ? "" : attr.getNodeValue().trim(); //$NON-NLS-1$ + if (value.length() != 0) { + try { + // See comment on Long.decode above. + long version = Long.decode(value); + if (version >= srcGlEsVersion) { + srcGlEsVersion = version; + srcNode = src; + } else if (version < 0x00010000) { + mSdkLog.warning("[%1$s] Ignoring because it's smaller than 1.0.", + fileLineInfo(src, "library"), + value); + } + } catch (NumberFormatException e) { + // Note: NumberFormatException.toString() has no interesting information + // so we don't output it. + mSdkLog.error(null, + "[%1$s] Failed to parse : must be an integer in the form 0x00020001.", + fileLineInfo(src, "library"), + value); + result = false; + } + } + } + + if (srcNode != null && destGlEsVersion < srcGlEsVersion) { + mSdkLog.warning( + "[%1$s, %2$s] Main manifest has but library uses glEsVersion='0x%4$08x'%5$s", + fileLineInfo(srcNode, "library"), + fileLineInfo(destNode == null ? mMainDoc : destNode, "main manifest"), + destGlEsVersion, + srcGlEsVersion, + destNode != null ? "" : //$NON-NLS-1$ + "\nNote: main manifest lacks a declaration, and thus defaults to glEsVersion=0x00010000." + ); + result = false; + } + + return result; + } + + /** + * Checks (but does not merge) uses-sdk attribues using the following rules: + *
+     * - {@code @minSdkVersion}: error if dest<lib. Never automatically change dest minsdk.
+     * - {@code @targetSdkVersion}: warning if dest<lib. Never automatically change destination.
+     * - {@code @maxSdkVersion}: obsolete, ignored. Not used in comparisons and not merged.
+     * 
+ * @param libDoc The library document to merge from. Must not be null. + * @return True on success, false if any error occurred (printed to the {@link ISdkLog}). + */ + private boolean checkSdkVersion(Document libDoc) { + + boolean result = true; + + Element destUsesSdk = findFirstElement(mMainDoc, "/manifest/uses-sdk"); //$NON-NLS-1$ + Element srcUsesSdk = findFirstElement(libDoc, "/manifest/uses-sdk"); //$NON-NLS-1$ + + AtomicInteger destValue = new AtomicInteger(1); + AtomicInteger srcValue = new AtomicInteger(1); + AtomicBoolean destImplied = new AtomicBoolean(true); + AtomicBoolean srcImplied = new AtomicBoolean(true); + + // Check minSdkVersion + destMinSdk = 1; + result = extractSdkVersionAttribute( + libDoc, + destUsesSdk, srcUsesSdk, + "min", //$NON-NLS-1$ + destValue, srcValue, + destImplied, srcImplied); + + if (result) { + // Make it an error for an application to use a library with a greater + // minSdkVersion. This means the library code may crash unexpectedly. + // TODO it would be nice to be able to work around this in case the + // user think s/he knows what s/he's doing. + // We could define a simple XML comment flag: + + destMinSdk = destValue.get(); + + if (destMinSdk < srcValue.get()) { + mSdkLog.error(null, + "[%1$s, %2$s] Main manifest has but library uses minSdkVersion='%4$d'%5$s", + fileLineInfo(srcUsesSdk == null ? libDoc : srcUsesSdk, "library"), + fileLineInfo(destUsesSdk == null ? mMainDoc : destUsesSdk, "main manifest"), + destMinSdk, + srcValue.get(), + !destImplied.get() ? "" : //$NON-NLS-1$ + "\nNote: main manifest lacks a declaration, which defaults to value 1." + ); + result = false; + } + } + + // Check targetSdkVersion. + + // Note that destValue/srcValue purposely defaults to whatever minSdkVersion was last read + // since that's their definition when missing. + destImplied.set(true); + srcImplied.set(true); + + boolean result2 = extractSdkVersionAttribute( + libDoc, + destUsesSdk, srcUsesSdk, + "target", //$NON-NLS-1$ + destValue, srcValue, + destImplied, srcImplied); + + result &= result2; + if (result2) { + // Make it a warning for an application to use a library with a greater + // targetSdkVersion. + + int destTargetSdk = destImplied.get() ? destMinSdk : destValue.get(); + + if (destTargetSdk < srcValue.get()) { + mSdkLog.warning( + "[%1$s, %2$s] Main manifest has but library uses targetSdkVersion='%4$d'%5$s", + fileLineInfo(srcUsesSdk == null ? libDoc : srcUsesSdk, "library"), + fileLineInfo(destUsesSdk == null ? mMainDoc : destUsesSdk, "main manifest"), + destTargetSdk, + srcValue.get(), + !destImplied.get() ? "" : //$NON-NLS-1$ + "\nNote: main manifest lacks a declaration, which defaults to value minSdkVersion or 1." + ); + result = false; + } + } + + return result; + } + + /** + * Implementation detail for {@link #checkSdkVersion(Document)}. + * Note that the various atomic out-variables must be preset to their default before + * the call. + *

+ * destValue/srcValue will be filled with the integer value of the field, if present + * and a correct number, in which case destImplied/destImplied are also set to true. + * Otherwise the values and the implied variables are left untouched. + */ + private boolean extractSdkVersionAttribute( + Document libDoc, + Element destUsesSdk, + Element srcUsesSdk, + String attr, + AtomicInteger destValue, + AtomicInteger srcValue, + AtomicBoolean destImplied, + AtomicBoolean srcImplied) { + String s = destUsesSdk == null ? "" //$NON-NLS-1$ + : destUsesSdk.getAttributeNS(NS_URI, attr + "SdkVersion"); //$NON-NLS-1$ + + assert s != null; + s = s.trim(); + try { + if (s.length() > 0) { + destValue.set(Integer.parseInt(s)); + destImplied.set(false); + } + } catch (NumberFormatException e) { + // Note: NumberFormatException.toString() has no interesting information + // so we don't output it. + mSdkLog.error(null, + "[%1$s] Failed to parse : must be an integer number.", + fileLineInfo(destUsesSdk == null ? mMainDoc : destUsesSdk, "main manifest"), + attr, + s); + return false; + } + + s = srcUsesSdk == null ? "" //$NON-NLS-1$ + : srcUsesSdk.getAttributeNS(NS_URI, attr + "SdkVersion"); //$NON-NLS-1$ + assert s != null; + s = s.trim(); + try { + if (s.length() > 0) { + srcValue.set(Integer.parseInt(s)); + srcImplied.set(false); + } + } catch (NumberFormatException e) { + mSdkLog.error(null, + "[%1$s] Failed to parse : must be an integer number.", + fileLineInfo(srcUsesSdk == null ? libDoc : srcUsesSdk, "library"), + attr, + s); + return false; + } + + return true; + } + + + // ----- + + + /** + * Given an element E, select which previous siblings we want to merge. + * We want to include any whitespace up to the closing of the previous element. + * We also want to include up preceding comment nodes and their preceding whitespace. + *

+ * This may returns either {@code end} or a previous sibling. Never returns null. + */ + @NonNull + private Node selectPreviousSiblings(Node end) { + + Node start = end; + Node prev = start.getPreviousSibling(); + while (prev != null) { + short t = prev.getNodeType(); + if (t == Node.TEXT_NODE) { + String text = prev.getNodeValue(); + if (text == null || text.trim().length() != 0) { + // Not whitespace, we don't want it. + break; + } + } else if (t == Node.COMMENT_NODE) { + // It's a comment. We'll take it. + } else { + // Not a comment node nor a whitespace text. We don't want it. + break; + } + start = prev; + prev = start.getPreviousSibling(); + } + + return start; + } + + /** + * Inserts all siblings from {@code start} to {@code end} at the end + * of the given destination element. + *

+ * Implementation detail: this clones the source nodes into the destination. + * + * @param dest The destination at the end of which to insert. Cannot be null. + * @param start The first element to insert. Must not be null. + * @param end The last element to insert (included). Must not be null. + * Must be a direct "next sibling" of the start node. + * Can be equal to the start node to insert just that one node. + * @return The copy of the {@code end} node in the destination document or null + * if no such copy was created and added to the destination. + */ + private Node insertAtEndOf(Element dest, Node start, Node end) { + // Check whether we'll need to adjust URI prefixes + String destPrefix = mMainDoc.lookupPrefix(NS_URI); + String srcPrefix = start.getOwnerDocument().lookupPrefix(NS_URI); + boolean needPrefixChange = destPrefix != null && !destPrefix.equals(srcPrefix); + + // First let's figure out the insertion point. + // We want the end of the last 'content' element of the + // destination element and basically we want to insert right + // before the last whitespace of the destination element. + Node target = dest.getLastChild(); + while (target != null) { + if (target.getNodeType() == Node.TEXT_NODE) { + String text = target.getNodeValue(); + if (text == null || text.trim().length() != 0) { + // Not whitespace, insert after. + break; + } + } else { + // Not text. Insert after + break; + } + target = target.getPreviousSibling(); + } + if (target != null) { + target = target.getNextSibling(); + } + + // Destination and start..end must not be part of the same document + // because we try to import below. If they were, it would mess the + // structure. + assert dest.getOwnerDocument() == mMainDoc; + assert dest.getOwnerDocument() != start.getOwnerDocument(); + assert start.getOwnerDocument() == end.getOwnerDocument(); + + while (start != null) { + Node node = mMainDoc.importNode(start, true /*deep*/); + if (needPrefixChange) { + changePrefix(node, srcPrefix, destPrefix); + } + dest.insertBefore(node, target); + + if (start == end) { + return node; + } + start = start.getNextSibling(); + } + return null; + } + + /** + * Changes the namespace prefix of all nodes, recursively. + * + * @param node The node to process, as well as all it's descendants. Can be null. + * @param srcPrefix The prefix to match. + * @param destPrefix The new prefix to replace with. + */ + private void changePrefix(Node node, String srcPrefix, String destPrefix) { + for (; node != null; node = node.getNextSibling()) { + if (srcPrefix.equals(node.getPrefix())) { + node.setPrefix(destPrefix); + } + Node child = node.getFirstChild(); + if (child != null) { + changePrefix(child, srcPrefix, destPrefix); + } + } + } + + /** + * Compares two {@link Element}s recursively. They must be identical with the same + * structure and order. Whitespace and comments are ignored. + * + * @param e1 The first element to compare. + * @param e2 The second element to compare with. + * @param nextSiblings If true, will also compare the following siblings. + * If false, it will just compare the given node. + * @param diff An optional {@link StringBuilder} where to accumulate a diff output. + * @param keyAttr An optional key attribute to always add to elements when dumping a diff. + * @return True if {@code e1} and {@code e2} are equal. + */ + private boolean compareElements( + @NonNull Node e1, + @NonNull Node e2, + boolean nextSiblings, + @Nullable StringBuilder diff, + @Nullable String keyAttr) { + return compareElements(e1, e2, nextSiblings, diff, 0, keyAttr); + } + + /** + * Do not call directly. This is an implementation detail for + * {@link #compareElements(Node, Node, boolean, StringBuilder, String)}. + */ + private boolean compareElements( + @NonNull Node e1, + @NonNull Node e2, + boolean nextSiblings, + @Nullable StringBuilder diff, + int diffOffset, + @Nullable String keyAttr) { + while(true) { + // Find the next non-whitespace text or non-comment in e1. + while (e1 != null) { + short t = e1.getNodeType(); + + if (t == Node.COMMENT_NODE) { + e1 = e1.getNextSibling(); + } else if (t == Node.TEXT_NODE) { + String s = e1.getNodeValue().trim(); + if (s.length() == 0) { + e1 = e1.getNextSibling(); + } else { + break; + } + } else { + break; + } + } + + // Find the next non-whitespace text or non-comment in e2. + while (e2 != null) { + short t = e2.getNodeType(); + + if (t == Node.COMMENT_NODE) { + e2 = e2.getNextSibling(); + } else if (t == Node.TEXT_NODE) { + String s = e2.getNodeValue().trim(); + if (s.length() == 0) { + e2 = e2.getNextSibling(); + } else { + break; + } + } else { + break; + } + } + + // Same elements, or both null? + if (e1 == e2 || (e1 == null && e2 == null)) { + return true; + } + + // Is one null but not the other? + if ((e1 == null && e2 != null) || (e1 != null && e2 == null)) { + break; // dumpMismatchAndExit + } + + assert e1 != null; + assert e2 != null; + + // Same type? + short t = e1.getNodeType(); + if (t != e2.getNodeType()) { + break; // dumpMismatchAndExit + } + + // Same node name? Must both be null or have the same value. + String s1 = e1.getNodeName(); + String s2 = e2.getNodeName(); + if ( !( (s1 == null && s2 == null) || (s1 != null && s1.equals(s2)) ) ) { + break; // dumpMismatchAndExit + } + + // Same node value? Must both be null or have the same value once whitespace is trimmed. + s1 = e1.getNodeValue(); + s2 = e2.getNodeValue(); + if (s1 != null) { + s1 = s1.trim(); + } + if (s2 != null) { + s2 = s2.trim(); + } + if ( !( (s1 == null && s2 == null) || (s1 != null && s1.equals(s2)) ) ) { + break; // dumpMismatchAndExit + } + + if (diff != null) { + // So far e1 and e2 seem pretty much equal. Dump it to the diff. + // We need to print to the diff before dealing with the children or attributes. + // Note: diffOffset + 1 because we want to reserve 2 spaces to write -/+ + diff.append(XmlUtils.dump(e1, diffOffset + 1, + false /*nextSiblings*/, false /*deep*/, keyAttr)); + } + + // Now compare the attributes. When using the w3c.DOM this way, attributes are + // accessible via the Node/Element attributeMap and are not actually exposed + // as ATTR_NODEs in the node list. The downside is that we don't really + // have the proper attribute order but that's not an issue as far as the validity + // of the XML since attribute order should never matter. + List a1 = XmlUtils.sortedAttributeList(e1.getAttributes()); + List a2 = XmlUtils.sortedAttributeList(e2.getAttributes()); + if (a1.size() > 0 || a2.size() > 0) { + + int count1 = 0; + int count2 = 0; + Map map = new TreeMap(); + for (Attr a : a1) { + AttrDiff ad1 = new AttrDiff(a, "--"); //$NON-NLS-1$ + map.put(ad1.mKey, ad1); + count1++; + } + + for (Attr a : a2) { + AttrDiff ad2 = new AttrDiff(a, "++"); //$NON-NLS-1$ + AttrDiff ad1 = map.get(ad2.mKey); + if (ad1 != null) { + ad1.mSide = " "; //$NON-NLS-1$ + count1--; + } else { + map.put(ad2.mKey, ad2); + count2++; + } + } + + if (count1 != 0 || count2 != 0) { + // We found some items not matching in both sets. Dump the result. + if (diff != null) { + for (AttrDiff ad : map.values()) { + diff.append(ad.mSide) + .append(XmlUtils.dump(ad.mAttr, diffOffset, + false /*nextSiblings*/, false /*deep*/, + keyAttr)); + } + } + // Exit without dumping + return false; + } + } + + // Compare recursively for elements. + if (t == Node.ELEMENT_NODE && + !compareElements( + e1.getFirstChild(), e2.getFirstChild(), true, + diff, diffOffset + 1, keyAttr)) { + // Exit without dumping since the recursive call take cares of its own diff + return false; + } + + if (nextSiblings) { + e1 = e1.getNextSibling(); + e2 = e2.getNextSibling(); + continue; + } else { + return true; + } + } + + // + if (diff != null) { + diff.append("--") + .append(XmlUtils.dump(e1, diffOffset, + false /*nextSiblings*/, false /*deep*/, keyAttr)); + diff.append("++") + .append(XmlUtils.dump(e2, diffOffset, + false /*nextSiblings*/, false /*deep*/, keyAttr)); + } + return false; + } + + private static class AttrDiff { + public final String mKey; + public final Attr mAttr; + public String mSide; + + public AttrDiff(Attr attr, String side) { + mKey = getKey(attr); + mAttr = attr; + mSide = side; + } + + String getKey(Attr attr) { + return String.format("%s=%s", attr.getNodeName(), attr.getNodeValue()); + } + } + + /** + * Finds the first element matching the given XPath expression in the given document. + * + * @param doc The document where to find the expression. + * @param path The XPath expression. It must yield an {@link Element} node type. + * @return The {@link Element} found or null. + */ + @Nullable + private Element findFirstElement( + @NonNull Document doc, + @NonNull String path) { + Node result; + try { + result = (Node) mXPath.evaluate(path, doc, XPathConstants.NODE); + if (result instanceof Element) { + return (Element) result; + } + + if (result != null) { + mSdkLog.error(null, + "Unexpected Node type %s when evaluating %s", //$NON-NLS-1$ + result.getClass().getName(), path); + } + } catch (XPathExpressionException e) { + mSdkLog.error(e, "XPath error on expr %s", path); //$NON-NLS-1$ + } + return null; + } + + /** + * Finds zero or more elements matching the given XPath expression in the given document. + * + * @param doc The document where to find the expression. + * @param path The XPath expression. Only {@link Element}s nodes will be returned. + * @return A list of {@link Element} found, possibly empty but never null. + */ + private List findElements( + @NonNull Document doc, + @NonNull String path) { + return findElements(doc, path, null, null); + } + + + /** + * Finds zero or more elements matching the given XPath expression in the given document. + *

+ * Furthermore, the elements must have an attribute matching the given attribute name + * and value if provided. (If you don't need to match an attribute, use the other version.) + *

+ * Note that if you provide {@code attrName} as non-null then the {@code attrValue} + * must be non-null too. In this case the XPath expression will be modified to add + * the check by naively appending a "[name='value']" filter. + * + * @param doc The document where to find the expression. + * @param path The XPath expression. Only {@link Element}s nodes will be returned. + * @param attrName The name of the optional attribute to match. Can be null. + * @param attrValue The value of the optiona attribute to match. + * Can be null if {@code attrName} is null, otherwise must be non-null. + * @return A list of {@link Element} found, possibly empty but never null. + * + * @see #findElements(Document, String) + */ + private List findElements( + @NonNull Document doc, + @NonNull String path, + @Nullable String attrName, + @Nullable String attrValue) { + List elements = new ArrayList(); + + if (attrName != null) { + assert attrValue != null; + // Generate expression /manifest/application/activity[@android:name='my.fqcn'] + path = String.format("%1$s[@%2$s:%3$s='%4$s']", //$NON-NLS-1$ + path, NS_PREFIX, attrName, attrValue); + } + + try { + NodeList results = (NodeList) mXPath.evaluate(path, doc, XPathConstants.NODESET); + if (results != null && results.getLength() > 0) { + for (int i = 0; i < results.getLength(); i++) { + Node n = results.item(i); + assert n instanceof Element; + if (n instanceof Element) { + elements.add((Element) n); + } else { + mSdkLog.error(null, + "Unexpected Node type %s when evaluating %s", //$NON-NLS-1$ + n.getClass().getName(), path); + } + } + } + + } catch (XPathExpressionException e) { + mSdkLog.error(e, "XPath error on expr %s", path); //$NON-NLS-1$ + } + + return elements; + } + + /** + * Tries to returns the base filename used from which the XML was parsed. + * @param node Any node from a document parsed by {@link XmlUtils#parseDocument(File, ISdkLog)}. + * @param defaultName The string to return if the XML filename cannot be determined. + * @return The base filename used from which the XML was parsed or the default name. + */ + private String xmlFileName(Node node, String defaultName) { + File f = XmlUtils.extractXmlFilename(node); + if (f != null) { + return f.getName(); + } else { + return defaultName; + } + } + + /** + * Tries to returns the base filename & line number from which the XML node was parsed. + * + * @param node Any node from a document parsed by {@link XmlUtils#parseDocument(File, ISdkLog)}. + * @param defaultName The string to return if the XML filename cannot be determined. + * @return The base filename used from which the XML was parsed with the line number + * (if available) or the default name. + */ + private String fileLineInfo(Node node, String defaultName) { + String name = xmlFileName(node, defaultName); + int line = XmlUtils.extractLineNumber(node); + if (line <= 0) { + return name; + } else { + return name + ':' + line; + } + } + +} diff --git a/manifmerger/src/com/android/manifmerger/XmlUtils.java b/manifmerger/src/com/android/manifmerger/XmlUtils.java new file mode 100755 index 0000000..d18eebb --- /dev/null +++ b/manifmerger/src/com/android/manifmerger/XmlUtils.java @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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. + */ + +package com.android.manifmerger; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.ISdkLog; + +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXParseException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +/** + * A few XML handling utilities. + */ +class XmlUtils { + + private static final String DATA_ORIGIN_FILE = "origin_file"; //$NON-NLS-1$ + private static final String DATA_LINE_NUMBER = "line#"; //$NON-NLS-1$ + + /** + * Parses the given XML file as a DOM document. + * The parser does not validate the DTD nor any kind of schema. + * It is namespace aware. + *

+ * This adds a user tag with the original {@link File} to the returned document. + * You can retrieve this file later by using {@link #extractXmlFilename(Node)}. + * + * @param xmlFile The XML {@link File} to parse. Must not be null. + * @param log An {@link ISdkLog} for reporting errors. Must not be null. + * @return A new DOM {@link Document}, or null. + */ + @Nullable + static Document parseDocument(@NonNull final File xmlFile, @NonNull final ISdkLog log) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + InputSource is = new InputSource(new FileReader(xmlFile)); + factory.setNamespaceAware(true); + factory.setValidating(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + + // We don't want the default handler which prints errors to stderr. + builder.setErrorHandler(new ErrorHandler() { + @Override + public void warning(SAXParseException e) { + log.printf("Warning when parsing %s: %s", xmlFile.getName(), e.toString()); + } + @Override + public void fatalError(SAXParseException e) { + log.printf("Fatal error when parsing %s: %s", xmlFile.getName(), e.toString()); + } + @Override + public void error(SAXParseException e) { + log.printf("Error when parsing %s: %s", xmlFile.getName(), e.toString()); + } + }); + + Document doc = builder.parse(is); + doc.setUserData(DATA_ORIGIN_FILE, xmlFile, null /*handler*/); + findLineNumbers(doc, 1); + + return doc; + + } catch (FileNotFoundException e) { + log.error(null, "XML file not found: %s", xmlFile.getName()); + + } catch (Exception e) { + log.error(e, "Failed to parse XML file: %s", xmlFile.getName()); + } + + return null; + } + + /** + * Parses the given XML string as a DOM document. + * The parser does not validate the DTD nor any kind of schema. + * It is namespace aware. + * + * @param xml The XML string to parse. Must not be null. + * @param log An {@link ISdkLog} for reporting errors. Must not be null. + * @return A new DOM {@link Document}, or null. + */ + @Nullable + static Document parseDocument(@NonNull String xml, @NonNull ISdkLog log) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + InputSource is = new InputSource(new StringReader(xml)); + factory.setNamespaceAware(true); + factory.setValidating(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(is); + findLineNumbers(doc, 1); + return doc; + } catch (Exception e) { + log.error(e, "Failed to parse XML string"); + } + + return null; + } + + /** + * Extracts the origin {@link File} that {@link #parseDocument(File, ISdkLog)} + * added to the XML document. + * + * @param xmlNode Any node from a document returned by {@link #parseDocument(File, ISdkLog)}. + * @return The {@link File} object used to create the document or null. + */ + @Nullable + static File extractXmlFilename(@Nullable Node xmlNode) { + if (xmlNode != null && xmlNode.getNodeType() != Node.DOCUMENT_NODE) { + xmlNode = xmlNode.getOwnerDocument(); + } + if (xmlNode != null) { + Object data = xmlNode.getUserData(DATA_ORIGIN_FILE); + if (data instanceof File) { + return (File) data; + } + } + + return null; + } + + /** + * This is a CRUDE INEXACT HACK to decorate the DOM with some kind of line number + * information for elements. It's inexact because by the time we get the DOM we + * already have lost all the information about whitespace between attributes. + *

+ * Also we don't even try to deal with \n vs \r vs \r\n insanity. This only counts + * the \n occuring in text nodes to determine line advances, which is clearly flawed. + *

+ * However it's good enough for testing, and we'll replace it by a PositionXmlParser + * once it's moved into com.android.util. + */ + private static int findLineNumbers(Node node, int line) { + for (; node != null; node = node.getNextSibling()) { + node.setUserData(DATA_LINE_NUMBER, Integer.valueOf(line), null /*handler*/); + + if (node.getNodeType() == Node.TEXT_NODE) { + String text = node.getNodeValue(); + if (text.length() > 0) { + for (int pos = 0; (pos = text.indexOf('\n', pos)) != -1; pos++) { + ++line; + } + } + } + + Node child = node.getFirstChild(); + if (child != null) { + line = findLineNumbers(child, line); + } + } + return line; + } + + /** + * Extracts the line number that {@link #findLineNumbers} added to the XML nodes. + * + * @param xmlNode Any node from a document returned by {@link #parseDocument(File, ISdkLog)}. + * @return The line number if found or 0. + */ + @Nullable + static int extractLineNumber(@Nullable Node xmlNode) { + if (xmlNode != null) { + Object data = xmlNode.getUserData(DATA_LINE_NUMBER); + if (data instanceof Integer) { + return ((Integer) data).intValue(); + } + } + + return 0; + } + + /** + * Find the prefix for the given NS_URI in the document. + * + * @param doc The document root. + * @param nsUri The Namespace URI to look for. + * @return The namespace prefix if found or null. + */ + static String lookupNsPrefix(Document doc, String nsUri) { + // Note: if this is not available, there's an alternate implementation at + // com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode.lookupNamespacePrefix(Node, String) + return doc.lookupPrefix(nsUri); + } + + /** + * Outputs the given XML {@link Document} to the file {@code outFile}. + * + * TODO right now reformats the document. Needs to output as-is, respecting white-space. + * + * @param doc The document to output. Must not be null. + * @param outFile The {@link File} where to write the document. + * @param log A log in case of error. + * @return True if the file was written, false in case of error. + */ + static boolean printXmlFile( + @NonNull Document doc, + @NonNull File outFile, + @NonNull ISdkLog log) { + // Quick thing based on comments from http://stackoverflow.com/questions/139076 + try { + Transformer tf = TransformerFactory.newInstance().newTransformer(); + tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); //$NON-NLS-1$ + tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$ + tf.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ + tf.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", //$NON-NLS-1$ + "4"); //$NON-NLS-1$ + tf.transform(new DOMSource(doc), new StreamResult(outFile)); + return true; + } catch (TransformerException e) { + log.error(e, "Failed to write XML file: %s", outFile); + return false; + } + } + + /** + * Outputs the given XML {@link Document} as a string. + * + * TODO right now reformats the document. Needs to output as-is, respecting white-space. + * + * @param doc The document to output. Must not be null. + * @param log A log in case of error. + * @return A string representation of the XML. Null in case of error. + */ + static String printXmlString( + @NonNull Document doc, + @NonNull ISdkLog log) { + try { + Transformer tf = TransformerFactory.newInstance().newTransformer(); + tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); //$NON-NLS-1$ + tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$ + tf.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ + tf.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", //$NON-NLS-1$ + "4"); //$NON-NLS-1$ + StringWriter sw = new StringWriter(); + tf.transform(new DOMSource(doc), new StreamResult(sw)); + return sw.toString(); + } catch (TransformerException e) { + log.error(e, "Failed to write XML file"); + return null; + } + } + + /** + * Dumps the structure of the DOM to a simple text string. + * + * @param node The first node to dump (recursively). Can be null. + * @param nextSiblings If true, will also dump the following siblings. + * If false, it will just process the given node. + * @return A string representation of the Node structure, useful for debugging. + */ + @NonNull + static String dump(@Nullable Node node, boolean nextSiblings) { + return dump(node, 0 /*offset*/, nextSiblings, true /*deep*/, null /*keyAttr*/); + } + + + /** + * Dumps the structure of the DOM to a simple text string. + * Each line is terminated with a \n separator. + * + * @param node The first node to dump. Can be null. + * @param offsetIndex The offset to add at the begining of each line. Each offset is + * converted into 2 space characters. + * @param nextSiblings If true, will also dump the following siblings. + * If false, it will just process the given node. + * @param deep If true, this will recurse into children. + * @param keyAttr An optional attribute *local* name to insert when writing an element. + * For example when writing an Activity, it helps to always insert "name" attribute. + * @return A string representation of the Node structure, useful for debugging. + */ + @NonNull + static String dump( + @Nullable Node node, + int offsetIndex, + boolean nextSiblings, + boolean deep, + @Nullable String keyAttr) { + StringBuilder sb = new StringBuilder(); + + String offset = ""; //$NON-NLS-1$ + for (int i = 0; i < offsetIndex; i++) { + offset += " "; //$NON-NLS-1$ + } + + if (node == null) { + sb.append(offset).append("(end reached)\n"); + + } else { + for (; node != null; node = node.getNextSibling()) { + String type = null; + short t = node.getNodeType(); + switch(t) { + case Node.ELEMENT_NODE: + String attr = ""; + if (keyAttr != null) { + NamedNodeMap attrs = node.getAttributes(); + if (attrs != null) { + for (int i = 0; i < attrs.getLength(); i++) { + Node a = attrs.item(i); + if (a != null && keyAttr.equals(a.getLocalName())) { + attr = String.format(" %1$s=%2$s", + a.getNodeName(), a.getNodeValue()); + break; + } + } + } + } + sb.append(String.format("%1$s<%2$s%3$s>\n", + offset, node.getNodeName(), attr)); + break; + case Node.COMMENT_NODE: + sb.append(String.format("%1$s\n", + offset, node.getNodeValue())); + break; + case Node.TEXT_NODE: + String txt = node.getNodeValue().trim(); + if (txt.length() == 0) { + // Keep this for debugging. TODO make it a flag + // to dump whitespace on debugging. Otherwise ignore it. + // txt = "[whitespace]"; + break; + } + sb.append(String.format("%1$s%2$s\n", offset, txt)); + break; + case Node.ATTRIBUTE_NODE: + sb.append(String.format("%1$s @%2$s = %3$s\n", + offset, node.getNodeName(), node.getNodeValue())); + break; + case Node.CDATA_SECTION_NODE: + type = "cdata"; //$NON-NLS-1$ + break; + case Node.DOCUMENT_NODE: + type = "document"; //$NON-NLS-1$ + break; + case Node.PROCESSING_INSTRUCTION_NODE: + type = "PI"; //$NON-NLS-1$ + break; + default: + type = Integer.toString(t); + } + + if (type != null) { + sb.append(String.format("%1$s[%2$s] <%3$s> %4$s\n", + offset, type, node.getNodeName(), node.getNodeValue())); + } + + if (deep) { + List attrs = sortedAttributeList(node.getAttributes()); + for (Attr attr : attrs) { + sb.append(String.format("%1$s @%2$s = %3$s\n", + offset, attr.getNodeName(), attr.getNodeValue())); + } + + Node child = node.getFirstChild(); + if (child != null) { + sb.append(dump(child, offsetIndex+1, true, true, keyAttr)); + } + } + + if (!nextSiblings) { + break; + } + } + } + return sb.toString(); + } + + /** + * Returns a sorted list of attributes. + * The list is never null and does not contain null items. + * + * @param attrMap A Node map as returned by {@link Node#getAttributes()}. + * Can be null, in which case an empty list is returned. + * @return A non-null, possible empty, list of all nodes that are actual {@link Attr}, + * sorted by increasing attribute name. + */ + @NonNull + static List sortedAttributeList(@Nullable NamedNodeMap attrMap) { + List list = new ArrayList(); + + if (attrMap != null) { + for (int i = 0; i < attrMap.getLength(); i++) { + Node attr = attrMap.item(i); + if (attr instanceof Attr) { + list.add((Attr) attr); + } + } + } + + if (list.size() > 1) { + // Sort it by attribute name + Collections.sort(list, getAttrComparator()); + } + + return list; + } + + /** + * Returns a comparator for {@link Attr}, alphabetically sorted by name. + * The "name" attribute is special and always sorted to the front. + */ + @NonNull + static Comparator getAttrComparator() { + return new Comparator() { + @Override + public int compare(Attr a1, Attr a2) { + String s1 = a1 == null ? "" : a1.getNodeName(); //$NON-NLS-1$ + String s2 = a2 == null ? "" : a2.getNodeValue(); //$NON-NLS-1$ + + int prio1 = s1.equals("name") ? 0 : 1; //$NON-NLS-1$ + int prio2 = s2.equals("name") ? 0 : 1; //$NON-NLS-1$ + if (prio1 == 0 || prio2 == 0) { + return prio1 - prio2; + } + + return s1.compareTo(s2); + } + }; + } +} diff --git a/manifmerger/tests/Android.mk b/manifmerger/tests/Android.mk new file mode 100755 index 0000000..c1dcea9 --- /dev/null +++ b/manifmerger/tests/Android.mk @@ -0,0 +1,28 @@ +# Copyright (C) 2011 The Android Open Source Project +# +# 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +# Only compile source java files in this lib. +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_JAVA_RESOURCE_DIRS := src + +LOCAL_MODULE := manifmerger-tests +LOCAL_MODULE_TAGS := optional + +LOCAL_JAVA_LIBRARIES := manifmerger junit + +include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTest.java b/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTest.java new file mode 100755 index 0000000..6a78a5d --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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. + */ + +package com.android.manifmerger; + + +/** + * Unit tests for {@link ManifestMerger}. + */ +public class ManifestMergerTest extends ManifestMergerTestCase { + + /* + * Wait, I hear you, where are the tests? + * + * processTestFiles() uses loadTestData(), which infers the data filename + * from the caller method name. + * E.g. the method "test00_noop" will use the data file named "data/00_noop.xml". + * + * We could simplify this even further by simply iterating on the data + * files and getting rid of the test methods; however there's some value in + * having tests break on a method name that easily points to the data file. + */ + + public void test00_noop() throws Exception { + processTestFiles(); + } + + public void test01_ignore_app_attr() throws Exception { + processTestFiles(); + } + + public void test02_ignore_instrumentation() throws Exception { + processTestFiles(); + } + + public void test10_activity_merge() throws Exception { + processTestFiles(); + } + + public void test11_activity_dup() throws Exception { + processTestFiles(); + } + + public void test12_alias_dup() throws Exception { + processTestFiles(); + } + + public void test13_service_dup() throws Exception { + processTestFiles(); + } + + public void test14_receiver_dup() throws Exception { + processTestFiles(); + } + + public void test15_provider_dup() throws Exception { + processTestFiles(); + } + + public void test20_uses_lib_merge() throws Exception { + processTestFiles(); + } + + public void test21_uses_lib_errors() throws Exception { + processTestFiles(); + } + + public void test25_permission_merge() throws Exception { + processTestFiles(); + } + + public void test26_permission_dup() throws Exception { + processTestFiles(); + } + + public void test28_uses_perm_merge() throws Exception { + processTestFiles(); + } + + public void test30_uses_sdk_ok() throws Exception { + processTestFiles(); + } + + public void test32_uses_sdk_minsdk_ok() throws Exception { + processTestFiles(); + } + + public void test33_uses_sdk_minsdk_conflict() throws Exception { + processTestFiles(); + } + + public void test36_uses_sdk_targetsdk_warning() throws Exception { + processTestFiles(); + } + + public void test40_uses_feat_merge() throws Exception { + processTestFiles(); + } + + public void test41_uses_feat_errors() throws Exception { + processTestFiles(); + } + + public void test45_uses_feat_gles_once() throws Exception { + processTestFiles(); + } + + public void test47_uses_feat_gles_conflict() throws Exception { + processTestFiles(); + } + + public void test50_uses_conf_warning() throws Exception { + processTestFiles(); + } + + public void test52_support_screens_warning() throws Exception { + processTestFiles(); + } + + public void test54_compat_screens_warning() throws Exception { + processTestFiles(); + } + + public void test56_support_gltext_warning() throws Exception { + processTestFiles(); + } +} diff --git a/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTestCase.java b/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTestCase.java new file mode 100755 index 0000000..66b80e8 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTestCase.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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. + */ + +package com.android.manifmerger; + +import com.android.annotations.NonNull; +import com.android.sdklib.mock.MockLog; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +/** + * Some utilities to reduce repetitions in the {@link ManifestMergerTest}s. + *

+ * See {@link #loadTestData(String)} for an explanation of the data file format. + */ +abstract class ManifestMergerTestCase extends TestCase { + + /** + * Delimiter that indicates the test must fail. + * An XML output and errors are still generated and checked. + */ + private static final String DELIM_FAILS = "fails"; + /** + * Delimiter that starts a library XML content. + * The delimiter name must be in the form {@code @libSomeName} and it will be + * used as the base for the test file name. Using separate lib names is encouraged + * since it makes the error output easier to read. + */ + private static final String DELIM_LIB = "lib"; + /** + * Delimiter that starts the main manifest XML content. + */ + private static final String DELIM_MAIN = "main"; + /** + * Delimiter that starts the resulting XML content, whatever is generated by the merge. + */ + private static final String DELIM_RESULT = "result"; + /** + * Delimiter that starts the SdkLog output. + * The logger prints each entry on its lines, prefixed with E for errors, + * W for warnings and P for regular printfs. + */ + private static final String DELIM_ERRORS = "errors"; + + static class TestFiles { + private final File mMain; + private final File[] mLibs; + private final File mActualResult; + private final String mExpectedResult; + private final String mExpectedErrors; + private final boolean mShouldFail; + + /** Files used by a given test case. */ + public TestFiles( + boolean shouldFail, + @NonNull File main, + @NonNull File[] libs, + @NonNull File actualResult, + @NonNull String expectedResult, + @NonNull String expectedErrors) { + mShouldFail = shouldFail; + mMain = main; + mLibs = libs; + mActualResult = actualResult; + mExpectedResult = expectedResult; + mExpectedErrors = expectedErrors; + } + + public boolean getShouldFail() { + return mShouldFail; + } + + @NonNull + public File getMain() { + return mMain; + } + + @NonNull + public File[] getLibs() { + return mLibs; + } + + @NonNull + public File getActualResult() { + return mActualResult; + } + + @NonNull + public String getExpectedResult() { + return mExpectedResult; + } + + public String getExpectedErrors() { + return mExpectedErrors; + } + + // Try to delete any temp file potentially created. + public void cleanup() { + if (mMain != null && mMain.isFile()) { + mMain.delete(); + } + + if (mActualResult != null && mActualResult.isFile()) { + mActualResult.delete(); + } + + for (File f : mLibs) { + if (f != null && f.isFile()) { + f.delete(); + } + } + } + } + + /** + * Calls {@link #loadTestData(String)} by + * inferring the data filename from the caller's method name. + *

+ * The caller method name must be composed of "test" + the leaf filename. + * Extensions ".xml" or ".txt" are implied. + *

+ * E.g. to use the data file "12_foo.xml", simply call this from a method + * named "test12_foo". + * + * @return A new {@link TestFiles} instance. Never null. + * @throws Exception when things go wrong. + * @see #loadTestData(String) + */ + @NonNull + TestFiles loadTestData() throws Exception { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + for (int i = 0, n = stack.length; i < n; i++) { + StackTraceElement caller = stack[i]; + String name = caller.getMethodName(); + if (name.startsWith("test")) { + return loadTestData(name.substring(4)); + } + } + + throw new IllegalArgumentException("No caller method found which name started with 'test'"); + } + + /** + * Loads test data for a given test case. + * The input (main + libs) are stored in temp files. + * A new destination temp file is created to store the actual result output. + * The expected result is actually kept in a string. + *

+ * Data File Syntax: + *

    + *
  • Lines starting with # are ignored (anywhere, as long as # is the first char). + *
  • Lines before the first {@code @delimiter} are ignored. + *
  • Empty lines just after the {@code @delimiter} + * and before the first < XML line are ignored. + *
  • Valid delimiters are {@code @main} for the XML of the main app manifest. + *
  • Following delimiters are {@code @libXYZ}, read in the order of definition. + * The name can be anything as long as it starts with "{@code @lib}". + *
+ * + * @param filename The test data filename. If no extension is provided, this will + * try with .xml or .txt. Must not be null. + * @return A new {@link TestFiles} instance. Must not be null. + * @throws Exception when things fail to load properly. + */ + @NonNull + TestFiles loadTestData(@NonNull String filename) throws Exception { + + String resName = "data" + File.separator + filename; + InputStream is = null; + BufferedReader reader = null; + BufferedWriter writer = null; + + try { + is = this.getClass().getResourceAsStream(resName); + if (is == null && !filename.endsWith(".xml")) { + String resName2 = resName + ".xml"; + is = this.getClass().getResourceAsStream(resName2); + if (is != null) { + filename = resName2; + } + } + if (is == null && !filename.endsWith(".txt")) { + String resName3 = resName + ".txt"; + is = this.getClass().getResourceAsStream(resName3); + if (is != null) { + filename = resName3; + } + } + assertNotNull("Test data file not found for " + filename, is); + + reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); + + // Get the temporary directory to use. Just create a temp file, extracts its + // directory and remove the file. + File tempFile = File.createTempFile(this.getClass().getSimpleName(), ".tmp"); + File tempDir = tempFile.getParentFile(); + if (!tempFile.delete()) { + tempFile.deleteOnExit(); + } + + String line = null; + String delimiter = null; + boolean skipEmpty = true; + + boolean shouldFail = false; + StringBuilder expectedResult = new StringBuilder(); + StringBuilder expectedErrors = new StringBuilder(); + File mainFile = null; + File actualResultFile = null; + List libFiles = new ArrayList(); + int tempIndex = 0; + + while ((line = reader.readLine()) != null) { + if (skipEmpty && line.trim().length() == 0) { + continue; + } + if (line.length() > 0 && line.charAt(0) == '#') { + continue; + } + if (line.length() > 0 && line.charAt(0) == '@') { + delimiter = line.substring(1); + assertTrue( + "Unknown delimiter @" + delimiter + " in " + filename, + delimiter.startsWith(DELIM_LIB) || + delimiter.equals(DELIM_MAIN) || + delimiter.equals(DELIM_RESULT) || + delimiter.equals(DELIM_ERRORS) || + delimiter.equals(DELIM_FAILS)); + + skipEmpty = true; + + if (writer != null) { + try { + writer.close(); + } catch (IOException ignore) {} + writer = null; + } + + if (delimiter.equals(DELIM_FAILS)) { + shouldFail = true; + + } else if (!delimiter.equals(DELIM_ERRORS)) { + tempFile = new File(tempDir, String.format("%1$s%2$d_%3$s.xml", + this.getClass().getSimpleName(), + tempIndex++, + delimiter.replaceAll("[^a-zA-Z0-9_-]", "") + )); + tempFile.deleteOnExit(); + + if (delimiter.startsWith(DELIM_LIB)) { + libFiles.add(tempFile); + + } else if (delimiter.equals(DELIM_MAIN)) { + mainFile = tempFile; + + } else if (delimiter.equals(DELIM_RESULT)) { + actualResultFile = tempFile; + + } else { + fail("Unexpected data file delimiter @" + delimiter + + " in " + filename); + } + + if (!delimiter.equals(DELIM_RESULT)) { + writer = new BufferedWriter(new FileWriter(tempFile)); + } + } + + continue; + } + if (delimiter != null && + skipEmpty && + line.length() > 0 && + line.charAt(0) != '#' && + line.charAt(0) != '@') { + skipEmpty = false; + } + if (writer != null) { + writer.write(line); + writer.write('\n'); + } else if (DELIM_RESULT.equals(delimiter)) { + expectedResult.append(line).append('\n'); + } else if (DELIM_ERRORS.equals(delimiter)) { + expectedErrors.append(line).append('\n'); + } + } + + assertNotNull("Missing @" + DELIM_MAIN + " in " + filename, mainFile); + assertNotNull("Missing @" + DELIM_RESULT + " in " + filename, actualResultFile); + + return new TestFiles( + shouldFail, + mainFile, + libFiles.toArray(new File[libFiles.size()]), + actualResultFile, + expectedResult.toString(), + expectedErrors.toString()); + + } catch (UnsupportedEncodingException e) { + // BufferedReader failed to decode UTF-8, O'RLY? + throw e; + + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException ignore) {} + } + if (reader != null) { + try { + reader.close(); + } catch (IOException ignore) {} + } + if (is != null) { + try { + is.close(); + } catch (IOException ignore) {} + } + } + } + + /** + * Loads the data test files using {@link #loadTestData()} and then + * invokes {@link #processTestFiles(TestFiles)} to test them. + * + * @see #loadTestData() + * @see #processTestFiles(TestFiles) + */ + void processTestFiles() throws Exception { + processTestFiles(loadTestData()); + } + + /** + * Processes the data from the given {@link TestFiles} by + * invoking {@link ManifestMerger#process(File, File, File[])}: + * the given library files are applied consecutively to the main XML + * document and the output is generated. + *

+ * Then the expected and actual outputs are loaded into a DOM, + * dumped again to a String using an XML transform and compared. + * This makes sure only the structure is checked and that any + * formatting is ignored in the comparison. + * + * @param testFiles The test files to process. Must not be null. + * @throws Exception when this go wrong. + */ + void processTestFiles(TestFiles testFiles) throws Exception { + MockLog log = new MockLog(); + ManifestMerger merger = new ManifestMerger(log); + boolean processOK = merger.process(testFiles.getActualResult(), + testFiles.getMain(), + testFiles.getLibs()); + + String expectedErrors = testFiles.getExpectedErrors().trim(); + StringBuilder actualErrors = new StringBuilder(); + for (String s : log.getMessages()) { + actualErrors.append(s); + if (!s.endsWith("\n")) { + actualErrors.append('\n'); + } + } + assertEquals("Error generated during merging", + expectedErrors, actualErrors.toString().trim()); + + if (testFiles.getShouldFail()) { + assertFalse("Merge process() returned true, expected false", processOK); + } else { + assertTrue("Merge process() returned false, expected true", processOK); + } + + // Test result XML. There should always be one created + // since the process action does not stop on errors. + log.clear(); + String actual = XmlUtils.printXmlString( + XmlUtils.parseDocument(testFiles.getActualResult(), log), + log); + assertEquals("Error parsing actual result XML", "[]", log.toString()); + log.clear(); + String expected = XmlUtils.printXmlString( + XmlUtils.parseDocument(testFiles.getExpectedResult(), log), + log); + assertEquals("Error parsing expected result XML", "[]", log.toString()); + assertEquals("Error comparing expected to actual result", expected, actual); + + testFiles.cleanup(); + } + +} diff --git a/manifmerger/tests/src/com/android/manifmerger/data/00_noop.xml b/manifmerger/tests/src/com/android/manifmerger/data/00_noop.xml new file mode 100755 index 0000000..fa0cac0 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/00_noop.xml @@ -0,0 +1,225 @@ +# +# Syntax: +# - Lines starting with # are ignored (anywhere, as long as # is the first char). +# - Lines before the first @delimiter are ignored. +# - Empty lines just after the @delimiter and before the first < XML line are ignored. +# - Valid delimiters are @main for the XML of the main app manifest. +# - Following delimiters are @libXYZ, read in the order of definition. The name can be +# anything as long as it starts with "@lib". +# - Last delimiter should be @result. +# + +@main + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + +@lib2 + +# An empty library is not supported. It must be a valid XML file. + + +@result + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@errors + diff --git a/manifmerger/tests/src/com/android/manifmerger/data/01_ignore_app_attr.xml b/manifmerger/tests/src/com/android/manifmerger/data/01_ignore_app_attr.xml new file mode 100755 index 0000000..b8d02e6 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/01_ignore_app_attr.xml @@ -0,0 +1,68 @@ +# +# Test: +# - Attributes from the application element in a library are ignored. +# - Comments from nodes ignored in libraries are not merged either. +# + +@main + + + + + + + + + +@lib1 + + + + + + + + + +@result + + + + + + + + + +@errors + diff --git a/manifmerger/tests/src/com/android/manifmerger/data/02_ignore_instrumentation.xml b/manifmerger/tests/src/com/android/manifmerger/data/02_ignore_instrumentation.xml new file mode 100755 index 0000000..ed0dbbc --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/02_ignore_instrumentation.xml @@ -0,0 +1,62 @@ +# +# Test: +# - Instrumentation element from libraries are not merged in main manifest. +# + +@main + + + + + + + + +@lib1 + + + + + + + + +@result + + + + + + + + +@errors + diff --git a/manifmerger/tests/src/com/android/manifmerger/data/10_activity_merge.xml b/manifmerger/tests/src/com/android/manifmerger/data/10_activity_merge.xml new file mode 100755 index 0000000..56c0b53 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/10_activity_merge.xml @@ -0,0 +1,378 @@ +# +# Test: +# - Activities from libraries are merged in the main manifest. +# - Acts on activity / activity-alias / service / receiver / provider. +# - Elements are merged as-is with the first comment element preceding them. +# - Whitespace preceding the merged elements is transfered over too. +# +# Note: +# - New elements are always merged at the end of the application element. +# - It's an error if an element with the same @name attribute is defined +# or merged more than once unless the definition is *exactly* the same, +# the "same" being defined by the exact XML elements, whitespace excluded. +# +# This tests that a normal merge is done as expected. +# There's a warning because one of the activities from lib2 is already defined +# in the main but it's purely identical so it's not an error. +# + +@main + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1_widget + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib2_activity + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib3_alias + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@result + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +# from @lib1_widget + + + + + + + + + + + + + + + + + +# from @lib2_activity + + + + + + + + + + + +# from @lib3_alias + + + + + + + + + + + + + + + +@errors + +P [ManifestMergerTest2_lib2_activity.xml:6, ManifestMergerTest0_main.xml:31] Skipping identical /manifest/application/activity[@name=com.example.LibActivity] element. +P [ManifestMergerTest3_lib3_alias.xml:19, ManifestMergerTest0_main.xml] Skipping identical /manifest/application/activity[@name=com.example.LibActivity2] element. diff --git a/manifmerger/tests/src/com/android/manifmerger/data/11_activity_dup.xml b/manifmerger/tests/src/com/android/manifmerger/data/11_activity_dup.xml new file mode 100755 index 0000000..48c7b27 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/11_activity_dup.xml @@ -0,0 +1,387 @@ +# +# Test: +# - Activities from libraries are merged in the main manifest. +# - Acts on activity / activity-alias / service / receiver / provider. +# - Elements are merged as-is with the first comment element preceding them. +# - Whitespace preceding the merged elements is transfered over too. +# +# Note: +# - New elements are always merged at the end of the application element. +# - It's an error if an element with the same @name attribute is defined +# or merged more than once unless the definition is *exactly* the same, +# the "same" being defined by the exact XML elements, whitespace excluded. +# +# This tests that an error is generated because the libraries define +# activities which are already in the main, with slightly different XML content: +# number and *order* of elements must match, attributes must match. +# + +@fails + +@main + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1_widget + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib2_activity + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib3_alias + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@result + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +# from @lib1_widget + + + + + + + + + + +# from @lib2_activity + + + + + + + + + + + +# from @lib3_alias + + + + + + + + + + + + + + + +@errors + +E [ManifestMergerTest1_lib1_widget.xml:16, ManifestMergerTest0_main.xml:32] Trying to merge incompatible /manifest/application/activity[@name=com.example.WidgetConfigurationUI] element: + +-- +++ (end reached) +E [ManifestMergerTest2_lib2_activity.xml:6, ManifestMergerTest0_main.xml:38] Trying to merge incompatible /manifest/application/activity[@name=com.example.LibActivity] element: + + @android:icon = @drawable/lib_activity_icon + @android:label = @string/lib_activity_name + @android:name = com.example.LibActivity +-- @android:theme = @style/Lib.Theme +E [ManifestMergerTest3_lib3_alias.xml:19, ManifestMergerTest0_main.xml] Trying to merge incompatible /manifest/application/activity[@name=com.example.LibActivity2] element: + + + + +-- +++ (end reached) diff --git a/manifmerger/tests/src/com/android/manifmerger/data/12_alias_dup.xml b/manifmerger/tests/src/com/android/manifmerger/data/12_alias_dup.xml new file mode 100755 index 0000000..e96c8a2 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/12_alias_dup.xml @@ -0,0 +1,205 @@ +# +# Test: +# - Activities from libraries are merged in the main manifest. +# - Acts on activity / activity-alias / service / receiver / provider. +# - Elements are merged as-is with the first comment element preceding them. +# - Whitespace preceding the merged elements is transfered over too. +# +# Note: +# - New elements are always merged at the end of the application element. +# - It's an error if an element with the same @name attribute is defined +# or merged more than once unless the definition is *exactly* the same, +# the "same" being defined by the exact XML elements, whitespace excluded. +# +# This tests that an error is generated because the libraries define +# aliases which are already defined differently. +# + +@fails + +@main + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + + + + + +@result + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@errors + +P [ManifestMergerTest1_lib1.xml:6, ManifestMergerTest0_main.xml:6] Skipping identical /manifest/application/activity-alias[@name=com.example.alias.MyActivity1] element. +E [ManifestMergerTest1_lib1.xml:14, ManifestMergerTest0_main.xml:13] Trying to merge incompatible /manifest/application/activity-alias[@name=com.example.alias.MyActivity2] element: + +++ @android:icon = @drawable/alias_icon2 +++ @android:label = @string/alias_name2 + @android:name = com.example.alias.MyActivity2 + @android:targetActivity = com.example.MainActivity2 +E [ManifestMergerTest2_lib2.xml:6, ManifestMergerTest0_main.xml] Trying to merge incompatible /manifest/application/activity-alias[@name=com.example.alias.MyActivity3] element: + + @android:icon = @drawable/alias_icon3 + @android:label = @string/alias_name3 + @android:name = com.example.alias.MyActivity3 +++ @android:targetActivity = com.example.MainActivity3 diff --git a/manifmerger/tests/src/com/android/manifmerger/data/13_service_dup.xml b/manifmerger/tests/src/com/android/manifmerger/data/13_service_dup.xml new file mode 100755 index 0000000..bc7c0c9 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/13_service_dup.xml @@ -0,0 +1,155 @@ +# +# Test: +# - Activities from libraries are merged in the main manifest. +# - Acts on activity / activity-alias / service / receiver / provider. +# - Elements are merged as-is with the first comment element preceding them. +# - Whitespace preceding the merged elements is transfered over too. +# +# Note: +# - New elements are always merged at the end of the application element. +# - It's an error if an element with the same @name attribute is defined +# or merged more than once unless the definition is *exactly* the same, +# the "same" being defined by the exact XML elements, whitespace excluded. +# +# This tests that an error is generated because the libraries define +# services which are already defined differently. +# + +@fails + +@main + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + + +@result + + + + + + + + + + + + + + + + + + + + + + +@errors + +P [ManifestMergerTest1_lib1.xml:6, ManifestMergerTest0_main.xml:6] Skipping identical /manifest/application/service[@name=com.example.AppService1] element. +E [ManifestMergerTest1_lib1.xml:9, ManifestMergerTest0_main.xml:8] Trying to merge incompatible /manifest/application/service[@name=com.example.AppService2] element: + +-- +++ (end reached) +E [ManifestMergerTest2_lib2.xml:6, ManifestMergerTest0_main.xml] Trying to merge incompatible /manifest/application/service[@name=com.example.AppService3] element: + +-- (end reached) +++ diff --git a/manifmerger/tests/src/com/android/manifmerger/data/14_receiver_dup.xml b/manifmerger/tests/src/com/android/manifmerger/data/14_receiver_dup.xml new file mode 100755 index 0000000..328f1fc --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/14_receiver_dup.xml @@ -0,0 +1,176 @@ +# +# Test: +# - Activities from libraries are merged in the main manifest. +# - Acts on activity / activity-alias / service / receiver / provider. +# - Elements are merged as-is with the first comment element preceding them. +# - Whitespace preceding the merged elements is transfered over too. +# +# Note: +# - New elements are always merged at the end of the application element. +# - It's an error if an element with the same @name attribute is defined +# or merged more than once unless the definition is *exactly* the same, +# the "same" being defined by the exact XML elements, whitespace excluded. +# +# This tests that an error is generated because the libraries define +# receivers which are already defined differently. +# + +@fails + +@main + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + + + + + +@result + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@errors + +P [ManifestMergerTest1_lib1.xml:6, ManifestMergerTest0_main.xml:6] Skipping identical /manifest/application/receiver[@name=com.example.AppReceiver1] element. +E [ManifestMergerTest1_lib1.xml:13, ManifestMergerTest0_main.xml:12] Trying to merge incompatible /manifest/application/receiver[@name=com.example.AppReceiver2] element: + +++ @android:icon = @drawable/app_icon + @android:name = com.example.AppReceiver2 +E [ManifestMergerTest2_lib2.xml:6, ManifestMergerTest0_main.xml] Trying to merge incompatible /manifest/application/receiver[@name=com.example.AppReceiver3] element: + + + +-- @android:name = com.example.action.ACTION_CUSTOM +++ @android:name = com.example.action.ACTION_CUSTOM1 diff --git a/manifmerger/tests/src/com/android/manifmerger/data/15_provider_dup.xml b/manifmerger/tests/src/com/android/manifmerger/data/15_provider_dup.xml new file mode 100755 index 0000000..91fe270 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/15_provider_dup.xml @@ -0,0 +1,145 @@ +# +# Test: +# - Activities from libraries are merged in the main manifest. +# - Acts on activity / activity-alias / service / receiver / provider. +# - Elements are merged as-is with the first comment element preceding them. +# - Whitespace preceding the merged elements is transfered over too. +# +# Note: +# - New elements are always merged at the end of the application element. +# - It's an error if an element with the same @name attribute is defined +# or merged more than once unless the definition is *exactly* the same, +# the "same" being defined by the exact XML elements, whitespace excluded. +# +# This tests that an error is generated because the libraries define +# providers which are already defined differently. +# + +@fails + +@main + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + +@result + + + + + + + + + + + + + + + + + +@errors + +P [ManifestMergerTest1_lib1.xml:6, ManifestMergerTest0_main.xml:6] Skipping identical /manifest/application/provider[@name=com.example.Provider1] element. +E [ManifestMergerTest1_lib1.xml:9, ManifestMergerTest0_main.xml:8] Trying to merge incompatible /manifest/application/provider[@name=com.example.Provider2] element: + +-- @android:authorities = com.example.android.apis.app.thingy2 +-- @android:enabled = @bool/someConditionalValue2 + @android:name = com.example.Provider2 +E [ManifestMergerTest2_lib2.xml:6, ManifestMergerTest0_main.xml] Trying to merge incompatible /manifest/application/provider[@name=com.example.Provider3] element: + + @android:authorities = com.example.android.apis.app.thingy3 +++ @android:enabled = @bool/someConditionalValue + @android:name = com.example.Provider3 diff --git a/manifmerger/tests/src/com/android/manifmerger/data/20_uses_lib_merge.xml b/manifmerger/tests/src/com/android/manifmerger/data/20_uses_lib_merge.xml new file mode 100755 index 0000000..9e37577 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/20_uses_lib_merge.xml @@ -0,0 +1,176 @@ +# +# Test merge of uses-library: +# - Merge is OK if destination already has one with the same @name. +# - required defaults to "true" +# - when merging, a required=true (explicit or implicit) overwrites a required=false. +# + +@main + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + + + + + + + +@result + + + + + + + + + +# required=false from lib1 is ignored, it stays at the default + + + + + + +# lib1 keeps it required=false but lib2 makes it switch to required=true + + + + + +# new from lib1 + + + +# new from lib1, but lib2 makes it switch to required=true + + + + + + + + +@errors + diff --git a/manifmerger/tests/src/com/android/manifmerger/data/21_uses_lib_errors.xml b/manifmerger/tests/src/com/android/manifmerger/data/21_uses_lib_errors.xml new file mode 100755 index 0000000..da244c6 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/21_uses_lib_errors.xml @@ -0,0 +1,202 @@ +# +# Test merge of uses-library: +# - Merge is OK if destination already has one with the same @name. +# - required defaults to "true" +# - when merging, a required=true (explicit or implicit) overwrites a required=false. +# + +@fails + +@main + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + + + + + + + + +@result + + + + + + + + + + + + + + + +# lib1 keeps it required=false but lib2 makes it switch to required=true + + + + + + + + +# new from lib1 + + + +# new from lib1, but lib2 makes it switch to required=true + + + + + + + + +@errors + +E [ManifestMergerTest1_lib1.xml:6] Undefined 'name' attribute in /manifest/application/uses-library. +E [ManifestMergerTest1_lib1.xml:7] Undefined 'name' attribute in /manifest/application/uses-library. +E [ManifestMergerTest1_lib1.xml:8] Undefined 'name' attribute in /manifest/application/uses-library. +W [ManifestMergerTest0_main.xml:12] Invalid attribute 'required' in /manifest/application/uses-library[@name=com.example.SomeLibrary2_RequiredTrue] element: +Expected 'true' or 'false' but found 'booh!'. +W [ManifestMergerTest0_main.xml:15] has more than one /manifest/application/uses-library[@name=com.example.SomeLibrary3_RequiredFalse] element. +W [ManifestMergerTest1_lib1.xml:17] Invalid attribute 'required' in /manifest/application/uses-library[@name=com.example.SomeLibrary4_RequiredFalse] element: +Expected 'true' or 'false' but found 'foo'. +W [ManifestMergerTest0_main.xml:15] has more than one /manifest/application/uses-library[@name=com.example.SomeLibrary3_RequiredFalse] element. diff --git a/manifmerger/tests/src/com/android/manifmerger/data/25_permission_merge.xml b/manifmerger/tests/src/com/android/manifmerger/data/25_permission_merge.xml new file mode 100755 index 0000000..26782b8 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/25_permission_merge.xml @@ -0,0 +1,255 @@ +# +# Text permission, permission-group and permission-tree: +# - Libraries can add any of these elements as long as they don't conflict +# with the destination: either the element must not be at all in the destination +# (as identified by the name) or it must match exactly. +# + +@main + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + + + + + + + + +@result + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +# Added by lib1 + + + + + + + +# Added by lib2 + + + + + + + + + + +@errors diff --git a/manifmerger/tests/src/com/android/manifmerger/data/26_permission_dup.xml b/manifmerger/tests/src/com/android/manifmerger/data/26_permission_dup.xml new file mode 100755 index 0000000..e4be0e2 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/26_permission_dup.xml @@ -0,0 +1,303 @@ +# +# Text permission, permission-group and permission-tree: +# - Libraries can add any of these elements as long as they don't conflict +# with the destination: either the element must not be at all in the destination +# (as identified by the name) or it must match exactly. +# +# This one tests that duplicate definitions that are strictly equal generate errors +# with some (hopefully useful) diff. +# + +@fails + +@main + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + + + + + + + + +@result + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +# Added by lib1 + + + + + + + +# Added by lib2 + + + + + + + + + + +@errors + +E [ManifestMergerTest1_lib1.xml:4, ManifestMergerTest0_main.xml:12] Trying to merge incompatible /manifest/permission[@name=com.example.DangerWillRobinson] element: + +-- @android:description = Different description here +++ @android:description = Insert boring description here +-- @android:icon = @drawable/not_the_same_icon +++ @android:icon = @drawable/robot + @android:label = Danger, Will Robinson! + @android:name = com.example.DangerWillRobinson + @android:permissionGroup = com.example.MasterControlPermission + @android:protectionLevel = dangerous +E [ManifestMergerTest1_lib1.xml:8, ManifestMergerTest0_main.xml:14] Trying to merge incompatible /manifest/permission[@name=com.example.WhatWereYouThinking] element: + + @android:name = com.example.WhatWereYouThinking + @android:permissionGroup = com.example.MasterControlPermission +-- @android:protectionLevel = normal +++ @android:protectionLevel = signatureOrSystem +E [ManifestMergerTest1_lib1.xml:5, ManifestMergerTest0_main.xml:16] Trying to merge incompatible /manifest/permission-group[@name=com.example.MasterControlPermission] element: + + @android:description = Nobody expects... +++ @android:icon = @drawable/ignored_icon + @android:label = the Spanish Inquisition + @android:name = com.example.MasterControlPermission +E [ManifestMergerTest1_lib1.xml:6, ManifestMergerTest0_main.xml:18] Trying to merge incompatible /manifest/permission-tree[@name=com.example.PermTree] element: + +++ @android:label = This is not a label +-- @android:label = This is not the same label + @android:name = com.example.PermTree +E [ManifestMergerTest2_lib2.xml:6, ManifestMergerTest0_main.xml] Trying to merge incompatible /manifest/permission[@name=com.example.Permission1] element: + + @android:name = com.example.Permission1 + @android:permissionGroup = com.example.Permission1 +++ @android:protectionLevel = normal +-- @android:protectionLevel = system +E [ManifestMergerTest2_lib2.xml:7, ManifestMergerTest0_main.xml] Trying to merge incompatible /manifest/permission-tree[@name=com.example.PermTree1] element: + +-- @android:description = Extra description + @android:name = com.example.PermTree1 diff --git a/manifmerger/tests/src/com/android/manifmerger/data/28_uses_perm_merge.xml b/manifmerger/tests/src/com/android/manifmerger/data/28_uses_perm_merge.xml new file mode 100755 index 0000000..42f79e2 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/28_uses_perm_merge.xml @@ -0,0 +1,152 @@ +# +# Text uses-permission: +# - Libraries can add any of these elements as long as they don't conflict +# with the destination: either the element must not be at all in the destination +# (as identified by the name) or it must match exactly. +# + +@main + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + + + +@result + + + + + + + + + + + + + + + + + + + + + + + +# Added by lib1 + + + + + + +# Added by lib2 + + + + + + +@errors + diff --git a/manifmerger/tests/src/com/android/manifmerger/data/30_uses_sdk_ok.xml b/manifmerger/tests/src/com/android/manifmerger/data/30_uses_sdk_ok.xml new file mode 100755 index 0000000..2d3670d --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/30_uses_sdk_ok.xml @@ -0,0 +1,80 @@ +# +# Test uses-sdk: add a uses-sdk from an app that doesn't define one. +# + +@main + + + + + + + + + + +@lib1 + + + + + + + + + +@lib2 + + + + + + + + + +@lib3 + + + + + + + + + +@result + + + + + + + + + + +@errors + diff --git a/manifmerger/tests/src/com/android/manifmerger/data/32_uses_sdk_minsdk_ok.xml b/manifmerger/tests/src/com/android/manifmerger/data/32_uses_sdk_minsdk_ok.xml new file mode 100755 index 0000000..ffe7353 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/32_uses_sdk_minsdk_ok.xml @@ -0,0 +1,64 @@ +# +# Test uses-sdk: it's ok for a library to have a smaller minSdkVersion than the main manifest. +# + +@main + + + + + + + + + + +@lib1 + + + + + + + + + +@lib2 + + + + + + + + +@lib3 + + + + + + + + +@result + + + + + + + + + + +@errors + diff --git a/manifmerger/tests/src/com/android/manifmerger/data/33_uses_sdk_minsdk_conflict.xml b/manifmerger/tests/src/com/android/manifmerger/data/33_uses_sdk_minsdk_conflict.xml new file mode 100755 index 0000000..0d0ab87 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/33_uses_sdk_minsdk_conflict.xml @@ -0,0 +1,110 @@ +# +# Test uses-sdk: it's an error for a library to require a minSdkVersion higher than the +# one defined in the main manifest. +# +# Also a uses-sdk with a lack of minSdkVersion is equivalent to using version=1. +# + +@fails + +@main + + + + + + + + + + + +@lib1 + + + + + + + + + +@lib2 + + + + + + + + +@lib3 + + + + + + + + +@lib4_parsingError + + + + + + + + + +@lib5_parsingError + + + + + + + + + +@lib6_parsingError + + + + + + + + + +@result + + + + + + + + + + + +@errors + +E [ManifestMergerTest1_lib1.xml:4, ManifestMergerTest0_main.xml:4] Main manifest has but library uses minSdkVersion='4' +Note: main manifest lacks a declaration, which defaults to value 1. +E [ManifestMergerTest2_lib2.xml:3, ManifestMergerTest0_main.xml:4] Main manifest has but library uses minSdkVersion='10' +Note: main manifest lacks a declaration, which defaults to value 1. +E [ManifestMergerTest3_lib3.xml:3, ManifestMergerTest0_main.xml:4] Main manifest has but library uses minSdkVersion='11' +Note: main manifest lacks a declaration, which defaults to value 1. +E [ManifestMergerTest4_lib4_parsingError.xml:4] Failed to parse : must be an integer number. +E [ManifestMergerTest5_lib5_parsingError.xml:4] Failed to parse : must be an integer number. +E [ManifestMergerTest6_lib6_parsingError.xml:4] Failed to parse : must be an integer number. diff --git a/manifmerger/tests/src/com/android/manifmerger/data/36_uses_sdk_targetsdk_warning.xml b/manifmerger/tests/src/com/android/manifmerger/data/36_uses_sdk_targetsdk_warning.xml new file mode 100755 index 0000000..2e87e19 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/36_uses_sdk_targetsdk_warning.xml @@ -0,0 +1,73 @@ + +# Test uses-sdk: there's a warning if the main manifest defines a targetSdkVersion that +# . +# + +@fails + +@main + + + + + + + + + + + +@lib1 + + + + + + + + + +@lib2 + + + + + + + + +@result + + + + + + + + + + + +@errors + +W [ManifestMergerTest1_lib1.xml:4, ManifestMergerTest0_main.xml:4] Main manifest has but library uses targetSdkVersion='11' diff --git a/manifmerger/tests/src/com/android/manifmerger/data/40_uses_feat_merge.xml b/manifmerger/tests/src/com/android/manifmerger/data/40_uses_feat_merge.xml new file mode 100755 index 0000000..1e88473 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/40_uses_feat_merge.xml @@ -0,0 +1,178 @@ +# +# Test merge of uses-feature: +# - Merge is OK if destination already has one with the same @name. +# - required defaults to "true" +# - when merging, a required=true (explicit or implicit) overwrites a required=false. +# +# Note: uses-feature with android:glEsVersion is dealt with in another test case. +# + +@main + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + + + + + + + +@result + + + + + + + +# required=false from lib1 is ignored, it stays at the default + + + + + + +# lib1 keeps it required=false but lib2 makes it switch to required=true + + + + + + + + + +# new from lib1 + + + +# new from lib1, but lib2 makes it switch to required=true + + + + + + +@errors + diff --git a/manifmerger/tests/src/com/android/manifmerger/data/41_uses_feat_errors.xml b/manifmerger/tests/src/com/android/manifmerger/data/41_uses_feat_errors.xml new file mode 100755 index 0000000..85eaf1d --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/41_uses_feat_errors.xml @@ -0,0 +1,205 @@ +# +# Test merge of uses-feature: +# - Merge is OK if destination already has one with the same @name. +# - required defaults to "true" +# - when merging, a required=true (explicit or implicit) overwrites a required=false. +# +# Note: uses-feature with android:glEsVersion is dealt with in another test case. +# + +@fails + +@main + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + + + + + + + + +@result + + + + + + + + + + + + + +# lib1 keeps it required=false but lib2 makes it switch to required=true + + + +# in case of duplicated name, they are all modified. + + + + + + + + + +# new from lib1 + + + +# new from lib1, but lib2 makes it switch to required=true + + + + + + +@errors + +E [ManifestMergerTest1_lib1.xml:4] Undefined 'name' attribute in /manifest/uses-feature. +E [ManifestMergerTest1_lib1.xml:5] Undefined 'name' attribute in /manifest/uses-feature. +E [ManifestMergerTest1_lib1.xml:6] Undefined 'name' attribute in /manifest/uses-feature. +W [ManifestMergerTest0_main.xml:10] Invalid attribute 'required' in /manifest/uses-feature[@name=com.example.SomeFeature2_RequiredTrue] element: +Expected 'true' or 'false' but found 'booh!'. +W [ManifestMergerTest0_main.xml:13] has more than one /manifest/uses-feature[@name=com.example.SomeFeature3_RequiredFalse] element. +W [ManifestMergerTest1_lib1.xml:15] Invalid attribute 'required' in /manifest/uses-feature[@name=com.example.SomeFeature4_RequiredFalse] element: +Expected 'true' or 'false' but found 'foo'. +W [ManifestMergerTest0_main.xml:13] has more than one /manifest/uses-feature[@name=com.example.SomeFeature3_RequiredFalse] element. diff --git a/manifmerger/tests/src/com/android/manifmerger/data/45_uses_feat_gles_once.xml b/manifmerger/tests/src/com/android/manifmerger/data/45_uses_feat_gles_once.xml new file mode 100755 index 0000000..8928ae4 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/45_uses_feat_gles_once.xml @@ -0,0 +1,124 @@ +# +# Test merge of uses-feature with android:glEsVersion: +# - Error if defined in lib+dest with dest < lib. +# - Never automatically change dest. +# - Default implied value is 1.0 (0x00010000). +# +# This tests a case that works. Also checks that glEsVersion attributes are stripped +# when merging uses-feature with the name attribute. +# + +@main + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + +@result + + + + + + + + + + + + +# lib1 adds this new node. Note how the glEsVersion=2.1 is stripped out. + + + +# lib2 adds this new node. Note how the glEsVersion=2.0 is stripped out. + + + + + +@errors + diff --git a/manifmerger/tests/src/com/android/manifmerger/data/47_uses_feat_gles_conflict.xml b/manifmerger/tests/src/com/android/manifmerger/data/47_uses_feat_gles_conflict.xml new file mode 100755 index 0000000..05f468f --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/47_uses_feat_gles_conflict.xml @@ -0,0 +1,160 @@ +# +# Test merge of uses-feature with android:glEsVersion: +# - Error if defined in lib+dest with dest < lib. +# - Never automatically change dest. +# - Default implied value is 1.0 (0x00010000). +# +# This tests a case that doesn't works because the main manifest doesn't declare +# the value and thus defaults to 1.0, so libraries with higher requirements will +# conflict. +# + +@fails + +@main + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + + + + + + + + + + + + + + + +@result + + + + + + + + + + + +# lib1 adds this new node. Note how the glEsVersion=2.1 is stripped out. + + + +# lib2 adds this new node. Note how the glEsVersion=2.0 is stripped out. + + + + + +@errors + +W [ManifestMergerTest1_lib1.xml:4, ManifestMergerTest0_main.xml:1] Main manifest has but library uses glEsVersion='0x00020001' +Note: main manifest lacks a declaration, and thus defaults to glEsVersion=0x00010000. +W [ManifestMergerTest2_lib2.xml:12] Ignoring because it's smaller than 1.0. +W [ManifestMergerTest2_lib2.xml:15] Ignoring because it's smaller than 1.0. +E [ManifestMergerTest2_lib2.xml:21] Failed to parse : must be an integer in the form 0x00020001. +W [ManifestMergerTest2_lib2.xml:18, ManifestMergerTest0_main.xml:1] Main manifest has but library uses glEsVersion='0xffffffff' +Note: main manifest lacks a declaration, and thus defaults to glEsVersion=0x00010000. diff --git a/manifmerger/tests/src/com/android/manifmerger/data/50_uses_conf_warning.xml b/manifmerger/tests/src/com/android/manifmerger/data/50_uses_conf_warning.xml new file mode 100755 index 0000000..4b79dc6 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/50_uses_conf_warning.xml @@ -0,0 +1,158 @@ +# +# Test uses-configuration: +# - it's OK if a library defines one or multiple times an element already in the application. +# - it's a warning if the library defines an element not in the application. +# - this does not actually merge anything. The XML is not changed at all. +# + +@main + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + +@lib2 + + + + + + + + +@result + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@errors + +W [ManifestMergerTest2_lib2.xml:4] /manifest/uses-configuration missing from ManifestMergerTest0_main.xml: + + @android:reqFiveWayNav = false + @android:reqNavigation = trackball + @android:reqTouchScreen = finger diff --git a/manifmerger/tests/src/com/android/manifmerger/data/52_support_screens_warning.xml b/manifmerger/tests/src/com/android/manifmerger/data/52_support_screens_warning.xml new file mode 100755 index 0000000..737144a --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/52_support_screens_warning.xml @@ -0,0 +1,158 @@ +# +# Test supports-screens: +# - it's OK if a library defines one or multiple times an element already in the application. +# - it's a warning if the library defines an element not in the application. +# - this does not actually merge anything. The XML is not changed at all. +# + +@main + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + +@lib2 + + + + + + + + + +@result + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@errors + +W [ManifestMergerTest2_lib2.xml:4] /manifest/supports-screens missing from ManifestMergerTest0_main.xml: + + @android:resizeable = false + @android:smallScreens = false diff --git a/manifmerger/tests/src/com/android/manifmerger/data/54_compat_screens_warning.xml b/manifmerger/tests/src/com/android/manifmerger/data/54_compat_screens_warning.xml new file mode 100755 index 0000000..35085fd --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/54_compat_screens_warning.xml @@ -0,0 +1,200 @@ +# +# Test compatible-screens: +# - it's OK if a library defines one or multiple times an element already in the application. +# - it's a warning if the library defines an element not in the application. +# - this does not actually merge anything. The XML is not changed at all. +# + +@main + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + + + + + +@lib2 + + + + + + + + + + + + + + + + + + + + + + + +@result + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@errors + +W [ManifestMergerTest2_lib2.xml:4] /manifest/compatible-screens missing from ManifestMergerTest0_main.xml: + + + @android:screenDensity = ldpi + @android:screenSize = small + + @android:screenDensity = mdpi + @android:screenSize = normal +W [ManifestMergerTest2_lib2.xml:9] /manifest/compatible-screens missing from ManifestMergerTest0_main.xml: + + + @android:screenDensity = ldpi + @android:screenSize = small +W [ManifestMergerTest2_lib2.xml:13] /manifest/compatible-screens missing from ManifestMergerTest0_main.xml: + + + @android:screenDensity = ldpi + @android:screenSize = normal + + @android:screenDensity = mdpi + @android:screenSize = normal + + @android:screenDensity = hdpi + @android:screenSize = normal + + @android:screenDensity = xhdpi + @android:screenSize = normal diff --git a/manifmerger/tests/src/com/android/manifmerger/data/56_support_gltext_warning.xml b/manifmerger/tests/src/com/android/manifmerger/data/56_support_gltext_warning.xml new file mode 100755 index 0000000..9cc5089 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/56_support_gltext_warning.xml @@ -0,0 +1,148 @@ +# +# Test supports-gl-texture: +# - it's OK if a library defines one or multiple times an element already in the application. +# - it's a warning if the library defines an element not in the application. +# - this does not actually merge anything. The XML is not changed at all. +# + +@main + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@lib1 + + + + + + + + + +@lib2 + + + + + + + + + +@result + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@errors + +W [ManifestMergerTest2_lib2.xml:4] /manifest/supports-gl-texture missing from ManifestMergerTest0_main.xml: + + @android:name = some.gl.texture3 -- cgit v1.1