diff options
author | Xavier Ducrohet <xav@android.com> | 2010-04-28 16:28:35 -0700 |
---|---|---|
committer | Xavier Ducrohet <xav@android.com> | 2010-05-03 18:01:55 -0700 |
commit | 114ca22329320d7f43af219b473d4c2461e82e7d (patch) | |
tree | 798e22bf687d807b8162d7d21788173cbb015752 /anttasks | |
parent | 74ab7fbdb2cbe6f9df3ee627a7751b0a3eafbb2a (diff) | |
download | sdk-114ca22329320d7f43af219b473d4c2461e82e7d.zip sdk-114ca22329320d7f43af219b473d4c2461e82e7d.tar.gz sdk-114ca22329320d7f43af219b473d4c2461e82e7d.tar.bz2 |
First step of multiple apk support.
- New export type project to handle exporting a single app represented
by multiple projects (for different minSdkVersion for instance).
- Cleaned-up the current Ant tasks and Eclipse builder to not
deal with ApkSettings anymore. It's not possible to generate
more than one APK in a single pass. The export project will
handle this and call out the normal build system to export
each variant. Make older (deprecated) attribute actually
output a warning.
- Ant rules r3 now with support for multi apk export. Lots
of clean up, add new properties to be overriden by the
multi-apk export task, make older one overrideable too.
- Better handling of older rules, older tasks, etc...
in the SetupTask used by the normal build.
- Add split by ABI to ApkSettings and use it in the
new multi apk export.
- New custom task for multi-apk export. Replaces
SetupTask for export-type projects. Calls out to the
project's build.xml after setting/overriding properties.
This also override the project's versionCode with a
new composite one made of different values.
Also uses a different build.xml, so added a new template:
build.export.template.
Very much a work in progress. Still to do:
Add other types of APK split, create/update export projects,
export the build log, ...
Also, we need to refactor the normal build rules to avoid
so much duplication!
Change-Id: I57a565c60d097a5eabb40108ae1fa8cb209f2380
Diffstat (limited to 'anttasks')
-rw-r--r-- | anttasks/src/com/android/ant/AaptExecLoopTask.java | 88 | ||||
-rw-r--r-- | anttasks/src/com/android/ant/ApkBuilderTask.java | 161 | ||||
-rw-r--r-- | anttasks/src/com/android/ant/MultiApkExportTask.java | 454 | ||||
-rw-r--r-- | anttasks/src/com/android/ant/SetupTask.java | 114 | ||||
-rw-r--r-- | anttasks/src/com/android/ant/TaskHelper.java | 65 |
5 files changed, 659 insertions, 223 deletions
diff --git a/anttasks/src/com/android/ant/AaptExecLoopTask.java b/anttasks/src/com/android/ant/AaptExecLoopTask.java index 09354c0..d9ec6bc 100644 --- a/anttasks/src/com/android/ant/AaptExecLoopTask.java +++ b/anttasks/src/com/android/ant/AaptExecLoopTask.java @@ -16,10 +16,6 @@ package com.android.ant; -import com.android.sdklib.internal.project.ApkSettings; -import com.android.sdklib.internal.project.ProjectProperties; -import com.android.sdklib.internal.project.ProjectProperties.PropertyType; - import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; @@ -28,8 +24,6 @@ import org.apache.tools.ant.types.Path; import java.io.File; import java.util.ArrayList; -import java.util.Map; -import java.util.Map.Entry; /** * Task able to run an Exec task on aapt several times. @@ -85,7 +79,9 @@ public final class AaptExecLoopTask extends Task { private String mAssets; private String mAndroidJar; private String mApkFolder; - private String mApkBaseName; + @Deprecated private String mApkBaseName; + private String mApkName; + private String mResourceFilter; private String mRFolder; private final ArrayList<NoCompress> mNoCompressList = new ArrayList<NoCompress>(); @@ -144,10 +140,12 @@ public final class AaptExecLoopTask extends Task { * Sets the value of the "resources" attribute. * @param resources the value. * - * @deprecated Uses nested element(s) <res path="value" /> + * @deprecated Use nested element(s) <res path="value" /> */ @Deprecated public void setResources(Path resources) { + System.out.println("WARNNG: Using deprecated 'resources' attribute in AaptExecLoopTask." + + "Use nested element(s) <res path=\"value\" /> instead."); if (mResources == null) { mResources = new ArrayList<Path>(); } @@ -178,6 +176,8 @@ public final class AaptExecLoopTask extends Task { */ @Deprecated public void setOutfolder(Path outFolder) { + System.out.println("WARNNG: Using deprecated 'outfolder' attribute in AaptExecLoopTask." + + "Use 'apkfolder' (path) instead."); mApkFolder = TaskHelper.checkSinglePath("outfolder", outFolder); } @@ -196,6 +196,8 @@ public final class AaptExecLoopTask extends Task { */ @Deprecated public void setBasename(String baseName) { + System.out.println("WARNNG: Using deprecated 'basename' attribute in AaptExecLoopTask." + + "Use 'resourceFilename' (string) instead."); mApkBaseName = baseName; } @@ -204,10 +206,20 @@ public final class AaptExecLoopTask extends Task { * @param apkbaseName the value. */ public void setApkbasename(String apkbaseName) { + System.out.println("WARNNG: Using deprecated 'apkbasename' attribute in AaptExecLoopTask." + + "Use 'resourceFilename' (string) instead."); mApkBaseName = apkbaseName; } /** + * Sets the value of the apkname attribute + * @param apkName the value + */ + public void setResourceFilename(String apkName) { + mApkName = apkName; + } + + /** * Sets the value of the "rfolder" attribute. * @param rFolder the value. */ @@ -215,6 +227,12 @@ public final class AaptExecLoopTask extends Task { mRFolder = TaskHelper.checkSinglePath("rfolder", rFolder); } + public void setresourceFilter(String filter) { + if (filter != null && filter.length() > 0) { + mResourceFilter = filter; + } + } + /** * Returns an object representing a nested <var>nocompress</var> element. */ @@ -251,8 +269,10 @@ public final class AaptExecLoopTask extends Task { Project taskProject = getProject(); // first do a full resource package - createPackage(null /*configName*/, null /*resourceFilter*/, null /*customPackage*/); + callAapt(null /*customPackage*/); + // if the parameters indicate generation of the R class, check if + // more R classes need to be created for libraries. if (mRFolder != null && new File(mRFolder).isDirectory()) { String libPkgProp = taskProject.getProperty("android.libraries.package"); if (libPkgProp != null) { @@ -265,49 +285,31 @@ public final class AaptExecLoopTask extends Task { // FIXME: instead of recreating R.java from scratch, maybe copy // the files (R.java and manifest.java)? This would force to replace // the package line on the fly. - createPackage(null, null, libPkg); + callAapt(libPkg); } } } } - - // now see if we need to create file with filtered resources. - // Get the project base directory. - File baseDir = taskProject.getBaseDir(); - ProjectProperties properties = ProjectProperties.load(baseDir.getAbsolutePath(), - PropertyType.DEFAULT); - - - ApkSettings apkSettings = new ApkSettings(properties); - if (apkSettings != null) { - Map<String, String> apkFilters = apkSettings.getResourceFilters(); - if (apkFilters.size() > 0) { - for (Entry<String, String> entry : apkFilters.entrySet()) { - createPackage(entry.getKey(), entry.getValue(), null /*custom package*/); - } - } - } } /** - * Creates a resource package. - * @param configName the name of the filter config. Can be null in which case a full resource - * package will be generated. + * Calls aapt with the given parameters. * @param resourceFilter the resource configuration filter to pass to aapt (if configName is * non null) + * @param customPackage an optional custom package. */ - private void createPackage(String configName, String resourceFilter, String customPackage) { + private void callAapt(String customPackage) { Project taskProject = getProject(); final boolean generateRClass = mRFolder != null && new File(mRFolder).isDirectory(); if (generateRClass) { - } else if (configName == null || resourceFilter == null) { + } else if (mResourceFilter == null) { System.out.println("Creating full resource package..."); } else { System.out.println(String.format( - "Creating resource package for config '%1$s' (%2$s)...", - configName, resourceFilter)); + "Creating resource package with filter: (%1$s)...", + mResourceFilter)); } // create a task for the default apk. @@ -333,9 +335,9 @@ public final class AaptExecLoopTask extends Task { } // filters if needed - if (configName != null && resourceFilter != null) { + if (mResourceFilter != null) { task.createArg().setValue("-c"); - task.createArg().setValue(resourceFilter); + task.createArg().setValue(mResourceFilter); } // no compress flag @@ -419,14 +421,14 @@ public final class AaptExecLoopTask extends Task { } // apk file. This is based on the apkFolder, apkBaseName, and the configName (if applicable) - if (mApkBaseName != null && mApkBaseName != null) { - String filename; - if (configName != null && resourceFilter != null) { - filename = mApkBaseName + "-" + configName + ".ap_"; - } else { - filename = mApkBaseName + ".ap_"; - } + String filename = null; + if (mApkName != null) { + filename = mApkName; + } else if (mApkBaseName != null) { + filename = mApkBaseName + ".ap_"; + } + if (filename != null) { File file = new File(mApkFolder, filename); task.createArg().setValue("-F"); task.createArg().setValue(file.getAbsolutePath()); diff --git a/anttasks/src/com/android/ant/ApkBuilderTask.java b/anttasks/src/com/android/ant/ApkBuilderTask.java index 9067ec7..e34eb6e 100644 --- a/anttasks/src/com/android/ant/ApkBuilderTask.java +++ b/anttasks/src/com/android/ant/ApkBuilderTask.java @@ -19,30 +19,23 @@ package com.android.ant; import com.android.apkbuilder.ApkBuilder.ApkCreationException; import com.android.apkbuilder.internal.ApkBuilderImpl; import com.android.apkbuilder.internal.ApkBuilderImpl.ApkFile; -import com.android.sdklib.internal.project.ApkSettings; -import com.android.sdklib.internal.project.ProjectProperties; -import com.android.sdklib.internal.project.ProjectProperties.PropertyType; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; import org.apache.tools.ant.types.Path; -import org.apache.tools.ant.types.Path.PathElement; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.util.ArrayList; -import java.util.Map; -import java.util.Map.Entry; public class ApkBuilderTask extends Task { - // ref id to the <path> object containing all the boot classpaths. - private final static String REF_APK_PATH = "android.apks.path"; - private String mOutFolder; - private String mBaseName; + @Deprecated private String mBaseName; + private String mApkFilepath; + private String mResourceFile; private boolean mVerbose = false; private boolean mSigned = true; private boolean mDebug = false; @@ -72,12 +65,31 @@ public class ApkBuilderTask extends Task { /** * Sets the value of the "basename" attribute. * @param baseName the value. + * @deprecated */ public void setBasename(String baseName) { + System.out.println("WARNNG: Using deprecated 'basename' attribute in ApkBuilderTask." + + "Use 'apkFilename' (path) instead."); mBaseName = baseName; } /** + * Sets the full filepath to the apk to generate. + * @param filepath + */ + public void setApkFilepath(String filepath) { + mApkFilepath = filepath; + } + + /** + * Sets the + * @param resourceFile + */ + public void setResourceFile(String resourceFile) { + mResourceFile = resourceFile; + } + + /** * Sets the value of the "verbose" attribute. * @param verbose the value. */ @@ -228,57 +240,38 @@ public class ApkBuilderTask extends Task { } } - // create the Path item that will contain all the generated APKs - // for reuse by other targets (signing/zipaligning) - Path path = new Path(antProject); - - // The createApk method uses mBaseName for the base name of the packages (resources - // and apk files). - // The generated apk file name is - // debug: {base}[-{config}]-debug-unaligned.apk - // release: {base}[-{config}]-unsigned.apk - // Unfortunately for 1.5 projects and before the 'install' ant target expects the name - // of the default debug package to be {base}-debug.apk - // In order to support those package, we look for the 'out.debug.unaligned.package' - // property. If this exist, then we generate {base}[-{config}]-debug-unaligned.apk - // otherwise we generate {base}[-{config}]-debug.apk - // FIXME: Make apkbuilder export the package name used instead of - // having to keep apkbuilder and the rules file in sync - String debugPackageSuffix = "-debug-unaligned.apk"; - if (antProject.getProperty("out.debug.unaligned.package") == null - && antProject.getProperty("out-debug-unaligned-package") == null) { - debugPackageSuffix = "-debug.apk"; + // get the rules revision + String rulesRevStr = antProject.getProperty(TaskHelper.PROP_RULES_REV); + int rulesRev = 1; + try { + rulesRev = Integer.parseInt(rulesRevStr); + } catch (NumberFormatException e) { + // this shouldn't happen since setup task is the one setting up every time. } - // first do a full resource package - createApk(apkBuilder, null /*configName*/, null /*resourceFilter*/, path, - debugPackageSuffix); - - // now see if we need to create file with filtered resources. - // Get the project base directory. - File baseDir = antProject.getBaseDir(); - ProjectProperties properties = ProjectProperties.load(baseDir.getAbsolutePath(), - PropertyType.DEFAULT); - - ApkSettings apkSettings = new ApkSettings(properties); - if (apkSettings != null) { - Map<String, String> apkFilters = apkSettings.getResourceFilters(); - if (apkFilters.size() > 0) { - for (Entry<String, String> entry : apkFilters.entrySet()) { - createApk(apkBuilder, entry.getKey(), entry.getValue(), path, - debugPackageSuffix); - } + + File file; + if (mApkFilepath != null) { + file = new File(mApkFilepath); + } else if (rulesRev == 2) { + if (mSigned) { + file = new File(mOutFolder, mBaseName + "-debug-unaligned.apk"); + } else { + file = new File(mOutFolder, mBaseName + "-unsigned.apk"); } + } else { + throw new BuildException("missing attribute 'apkFilepath'"); } - // finally sets the path in the project with a reference - antProject.addReference(REF_APK_PATH, path); + // create the package. + createApk(apkBuilder, file); } catch (FileNotFoundException e) { throw new BuildException(e); } catch (IllegalArgumentException e) { throw new BuildException(e); } catch (ApkCreationException e) { + e.printStackTrace(); throw new BuildException(e); } } @@ -286,76 +279,28 @@ public class ApkBuilderTask extends Task { /** * Creates an application package. * @param apkBuilder - * @param configName the name of the filter config. Can be null in which case a full resource - * package will be generated. - * @param resourceFilter the resource configuration filter to pass to aapt (if configName is - * non null) - * @param path Ant {@link Path} to which add the generated APKs as {@link PathElement} - * @param debugPackageSuffix suffix for the debug packages. + * @param outputfile the file to generate * @throws FileNotFoundException * @throws ApkCreationException */ - private void createApk(ApkBuilderImpl apkBuilder, String configName, String resourceFilter, - Path path, String debugPackageSuffix) + private void createApk(ApkBuilderImpl apkBuilder, File outputfile) throws FileNotFoundException, ApkCreationException { - // All the files to be included in the archive have already been prep'ed up, except - // the resource package. - // figure out its name. - String filename; - if (configName != null && resourceFilter != null) { - filename = mBaseName + "-" + configName + ".ap_"; - } else { - filename = mBaseName + ".ap_"; - } - // now we add it to the list of zip archive (it's just a zip file). - - // it's used as a zip archive input - FileInputStream resoucePackageZipFile = new FileInputStream(new File(mOutFolder, filename)); + // add the resource pack file as a zip archive input. + FileInputStream resoucePackageZipFile = new FileInputStream( + new File(mOutFolder, mResourceFile)); mZipArchives.add(resoucePackageZipFile); - // prepare the filename to generate. Same thing as the resource file. - if (configName != null && resourceFilter != null) { - filename = mBaseName + "-" + configName; - } else { - filename = mBaseName; - } - if (mSigned) { - filename = filename + debugPackageSuffix; + System.out.println(String.format( + "Creating %s and signing it with a debug key...", outputfile.getName())); } else { - filename = filename + "-unsigned.apk"; + System.out.println(String.format( + "Creating %s for release...", outputfile.getName())); } - if (configName == null || resourceFilter == null) { - if (mSigned) { - System.out.println(String.format( - "Creating %s and signing it with a debug key...", filename)); - } else { - System.out.println(String.format( - "Creating %s for release...", filename)); - } - } else { - if (mSigned) { - System.out.println(String.format( - "Creating %1$s (with %2$s) and signing it with a debug key...", - filename, resourceFilter)); - } else { - System.out.println(String.format( - "Creating %1$s (with %2$s) for release...", - filename, resourceFilter)); - } - } - - // out File - File f = new File(mOutFolder, filename); - - // add it to the Path object - PathElement element = path.createPathElement(); - element.setLocation(f); - // and generate the apk - apkBuilder.createPackage(f.getAbsoluteFile(), mZipArchives, + apkBuilder.createPackage(outputfile.getAbsoluteFile(), mZipArchives, mArchiveFiles, mJavaResources, mResourcesJars, mNativeLibraries); // we are done. We need to remove the resource package from the list of zip archives diff --git a/anttasks/src/com/android/ant/MultiApkExportTask.java b/anttasks/src/com/android/ant/MultiApkExportTask.java new file mode 100644 index 0000000..1babcf7 --- /dev/null +++ b/anttasks/src/com/android/ant/MultiApkExportTask.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2010 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.ant; + +import com.android.sdklib.SdkConstants; +import com.android.sdklib.internal.project.ApkSettings; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.android.sdklib.io.FileWrapper; +import com.android.sdklib.io.StreamException; +import com.android.sdklib.xml.AndroidManifest; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.taskdefs.Property; +import org.apache.tools.ant.taskdefs.SubAnt; +import org.apache.tools.ant.types.FileSet; +import org.xml.sax.InputSource; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +/** + * Multiple APK export task. + * This task is meant to replace {@link SetupTask} as the main setup/export task, importing + * the rules and generating the export for all projects. + */ +public class MultiApkExportTask extends Task { + + private static class ExportData implements Comparable<ExportData> { + + String relativePath; + File project; + int buildInfo; + int minor; + + // the following are used to sort the export data and generate buildInfo + int minSdkVersion; + String abi; + int glVersion; + // screen size? + + ExportData() { + // do nothing. + } + + public ExportData(ExportData data) { + relativePath = data.relativePath; + project = data.project; + buildInfo = data.buildInfo; + minor = data.buildInfo; + minSdkVersion = data.minSdkVersion; + abi = data.abi; + glVersion = data.glVersion; + } + + public int compareTo(ExportData o) { + int minSdkDiff = minSdkVersion - o.minSdkVersion; + if (minSdkDiff != 0) { + return minSdkDiff; + } + + if (abi != null) { + if (o.abi != null) { + return abi.compareTo(o.abi); + } else { + return -1; + } + } else if (o.abi != null) { + return 1; + } + + if (glVersion != 0) { + if (o.glVersion != 0) { + return glVersion - o.glVersion; + } else { + return -1; + } + } else if (o.glVersion != 0) { + return 1; + } + + return 0; + } + } + + private static enum Target { + RELEASE("release"), CLEAN("clean"); + + private final String mName; + + Target(String name) { + mName = name; + } + + String getTarget() { + return mName; + } + + static Target getTarget(String value) { + for (Target t : values()) { + if (t.mName.equals(value)) { + return t; + } + + } + + return null; + } + } + + private Target mTarget; + + public void setTarget(String target) { + mTarget = Target.getTarget(target); + } + + @Override + public void execute() throws BuildException { + Project antProject = getProject(); + + if (mTarget == null) { + throw new BuildException("'target' attribute not set."); + } + + // get the SDK location + File sdk = TaskHelper.getSdkLocation(antProject); + + // display SDK Tools revision + int toolsRevison = TaskHelper.getToolsRevision(sdk); + if (toolsRevison != -1) { + System.out.println("Android SDK Tools Revision " + toolsRevison); + } + + String appPackage = getValidatedProperty(antProject, "package"); + System.out.println("Multi APK export for: " + appPackage); + String version = getValidatedProperty(antProject, "version"); + int versionCode; + try { + versionCode = Integer.parseInt(version); + } catch (NumberFormatException e) { + throw new BuildException("version value is not a valid integer.", e); + } + System.out.println("versionCode: " + version); + + ExportData[] projects = getProjects(antProject, appPackage); + HashSet<String> compiledProject = new HashSet<String>(); + + XPathFactory xPathFactory = XPathFactory.newInstance(); + + File exportProjectOutput = new File(getValidatedProperty(antProject, "out.absolute.dir")); + + for (ExportData projectData : projects) { + // this output is prepended by "[android-export] " (17 chars), so we put 61 stars + System.out.println("\n*************************************************************"); + System.out.println("Exporting project: " + projectData.relativePath); + + SubAnt subAnt = new SubAnt(); + subAnt.setTarget(mTarget.getTarget()); + subAnt.setProject(antProject); + + File subProjectFolder = new File(antProject.getBaseDir(), projectData.relativePath); + + FileSet fileSet = new FileSet(); + fileSet.setProject(antProject); + fileSet.setDir(subProjectFolder); + fileSet.setIncludes("build.xml"); + subAnt.addFileset(fileSet); + +// subAnt.setVerbose(true); + + if (mTarget == Target.RELEASE) { + // only do the compilation part if it's the first time we export + // this project. + // (projects can be export multiple time if some properties are set up to + // generate more than one APK (for instance ABI split). + if (compiledProject.contains(projectData.relativePath) == false) { + compiledProject.add(projectData.relativePath); + } else { + addProp(subAnt, "do.not.compile", "true"); + } + + // set the version code, and filtering + String compositeVersionCode = getVersionCodeString(versionCode, projectData); + addProp(subAnt, "version.code", compositeVersionCode); + System.out.println("Composite versionCode: " + compositeVersionCode); + if (projectData.abi != null) { + addProp(subAnt, "filter.abi", projectData.abi); + System.out.println("ABI Filter: " + projectData.abi); + } + + // end of the output by this task. Everything that follows will be output + // by the subant. + System.out.println("Calling to project's Ant file..."); + System.out.println("----------\n"); + + // set the output file names/paths. Keep all the temporary files in the project + // folder, and only put the final file (which is different depending on whether + // the file can be signed) locally. + boolean canSign = false; + + // read the base name from the build.xml file. + String name = null; + try { + File buildFile = new File(subProjectFolder, "build.xml"); + XPath xPath = xPathFactory.newXPath(); + name = xPath.evaluate("/project/@name", + new InputSource(new FileInputStream(buildFile))); + } catch (XPathExpressionException e) { + throw new BuildException("Failed to read build.xml", e); + } catch (FileNotFoundException e) { + throw new BuildException("build.xml is missing.", e); + } + + // override the resource pack file. + addProp(subAnt, "resource.package.file.name", + name + "-" + projectData.buildInfo + ".ap_"); + + if (canSign) { + // temporary file only get a filename change (still stored in the project + // bin folder). + addProp(subAnt, "out.unsigned.file.name", + name + "-" + projectData.buildInfo + "-unsigned.apk"); + addProp(subAnt, "out.unaligned.file", + name + "-" + projectData.buildInfo + "-unaligned.apk"); + + // final file is stored locally. + addProp(subAnt, "out.release.file", new File(exportProjectOutput, + name + "-" + projectData.buildInfo + "-release.apk").getAbsolutePath()); + } else { + // final file is the unsigned version. It gets stored locally. + addProp(subAnt, "out.unsigned.file", new File(exportProjectOutput, + name + "-" + projectData.buildInfo + "-unsigned.apk").getAbsolutePath()); + } + } + + subAnt.execute(); + } + + // TODO: export build log. + + } + + /** + * Gets, validates and returns a project property. + * The property must exist and be non empty. + * @param antProject the project + * @param name the name of the property to return. + * @return the property value always (cannot be null). + * @throws BuildException if the property is missing or not valid. + */ + private String getValidatedProperty(Project antProject, String name) { + String value = antProject.getProperty(name); + if (value == null || value.length() == 0) { + throw new BuildException(String.format("Property '%1$s' is not set or empty.", name)); + } + + return value; + } + + /** + * gets the projects to export from the property, checks they exist, validates them, + * loads their export info and return it. + * If a project does not exist or is not valid, this will throw a {@link BuildException}. + * @param antProject the Ant project. + * @param appPackage the application package. Projects' manifest must match this. + */ + private ExportData[] getProjects(Project antProject, String appPackage) { + String projects = antProject.getProperty("projects"); + String[] paths = projects.split("\\:"); + + ArrayList<ExportData> datalist = new ArrayList<ExportData>(); + + for (String path : paths) { + File projectFolder = new File(path); + + // resolve the path (to remove potential ..) + try { + projectFolder = projectFolder.getCanonicalFile(); + + // project folder must exist and be a directory + if (projectFolder.isDirectory() == false) { + throw new BuildException(String.format( + "Project folder '%1$s' is not a valid directory.", + projectFolder.getAbsolutePath())); + } + + // Check AndroidManifest.xml is present + FileWrapper androidManifest = new FileWrapper(projectFolder, + SdkConstants.FN_ANDROID_MANIFEST_XML); + + if (androidManifest.isFile() == false) { + throw new BuildException(String.format( + "%1$s is not a valid project (%2$s not found).", + projectFolder.getAbsolutePath(), + SdkConstants.FN_ANDROID_MANIFEST_XML)); + } + + ArrayList<ExportData> datalist2 = checkManifest(androidManifest, appPackage); + + // if the method returns without throwing, this is a good project to + // export. + for (ExportData data : datalist2) { + data.relativePath = path; + data.project = projectFolder; + } + + datalist.addAll(datalist2); + + } catch (IOException e) { + throw new BuildException(String.format("Failed to resolve path %1$s", path), e); + } + } + + // sort the projects and assign buildInfo + Collections.sort(datalist); + int buildInfo = 0; + for (ExportData data : datalist) { + data.buildInfo = buildInfo++; + } + + return datalist.toArray(new ExportData[datalist.size()]); + } + + private ArrayList<ExportData> checkManifest(FileWrapper androidManifest, String appPackage) { + try { + String manifestPackage = AndroidManifest.getPackage(androidManifest); + if (appPackage.equals(manifestPackage) == false) { + throw new BuildException(String.format( + "%1$s package value is not valid. Found '%2$s', expected '%3$s'.", + androidManifest.getPath(), manifestPackage, appPackage)); + } + + if (AndroidManifest.hasVersionCode(androidManifest)) { + throw new BuildException(String.format( + "%1$s is not valid: versionCode must not be set for multi-apk export.", + androidManifest.getPath())); + } + + int minSdkVersion = AndroidManifest.getMinSdkVersion(androidManifest); + if (minSdkVersion == -1) { + throw new BuildException( + "Codename in minSdkVersion is not supported by multi-apk export."); + } + + ArrayList<ExportData> dataList = new ArrayList<ExportData>(); + ExportData data = new ExportData(); + dataList.add(data); + data.minSdkVersion = minSdkVersion; + + // only look for more exports if the target is not clean. + if (mTarget != Target.CLEAN) { + // load the project properties + String projectPath = androidManifest.getParent(); + ProjectProperties projectProp = ProjectProperties.load(projectPath, + PropertyType.DEFAULT); + if (projectProp == null) { + throw new BuildException(String.format( + "%1$s is missing.", PropertyType.DEFAULT.getFilename())); + } + + ApkSettings apkSettings = new ApkSettings(projectProp); + if (apkSettings.isSplitByAbi()) { + // need to find the available ABIs. + List<String> abis = findAbis(projectPath); + ExportData current = data; + for (String abi : abis) { + if (current == null) { + current = new ExportData(data); + dataList.add(current); + } + + current.abi = abi; + current = null; + } + } + } + + return dataList; + } catch (XPathExpressionException e) { + throw new BuildException( + String.format("Failed to validate %1$s", androidManifest.getPath()), e); + } catch (StreamException e) { + throw new BuildException( + String.format("Failed to validate %1$s", androidManifest.getPath()), e); + } + } + + private List<String> findAbis(String projectPath) { + ArrayList<String> abiList = new ArrayList<String>(); + File libs = new File(projectPath, SdkConstants.FD_NATIVE_LIBS); + if (libs.isDirectory()) { + File[] abis = libs.listFiles(); + for (File abi : abis) { + if (abi.isDirectory()) { + // only add the abi folder if there are .so files in it. + String[] content = abi.list(new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.toLowerCase().endsWith(".so"); + } + }); + + if (content.length > 0) { + abiList.add(abi.getName()); + } + } + } + } + + return abiList; + } + + private void addProp(SubAnt task, String name, String value) { + Property prop = new Property(); + prop.setName(name); + prop.setValue(value); + task.addProperty(prop); + } + + private String getVersionCodeString(int versionCode, ExportData projectData) { + int trueVersionCode = versionCode * 10000; + trueVersionCode += projectData.buildInfo * 100; + trueVersionCode += projectData.minor; + + return Integer.toString(trueVersionCode); + } + +} diff --git a/anttasks/src/com/android/ant/SetupTask.java b/anttasks/src/com/android/ant/SetupTask.java index a840783..5263f12 100644 --- a/anttasks/src/com/android/ant/SetupTask.java +++ b/anttasks/src/com/android/ant/SetupTask.java @@ -39,10 +39,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FilenameFilter; -import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; -import java.util.Properties; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpressionException; @@ -81,13 +79,9 @@ public final class SetupTask extends ImportTask { // ant property with the path to the android.jar private final static String PROPERTY_ANDROID_JAR = "android.jar"; - // LEGACY - compatibility with 1.6 and before - private final static String PROPERTY_ANDROID_JAR_LEGACY = "android-jar"; // ant property with the path to the framework.jar private final static String PROPERTY_ANDROID_AIDL = "android.aidl"; - // LEGACY - compatibility with 1.6 and before - private final static String PROPERTY_ANDROID_AIDL_LEGACY = "android-aidl"; // ant property with the path to the aapt tool private final static String PROPERTY_AAPT = "aapt"; @@ -105,26 +99,11 @@ public final class SetupTask extends ImportTask { Project antProject = getProject(); // get the SDK location - String sdkLocation = antProject.getProperty(ProjectProperties.PROPERTY_SDK); - - // check if it's valid and exists - if (sdkLocation == null || sdkLocation.length() == 0) { - // LEGACY support: project created with 1.6 or before may be using a different - // property to declare the location of the SDK. At this point, we cannot - // yet check which target is running so we check both always. - sdkLocation = antProject.getProperty(ProjectProperties.PROPERTY_SDK_LEGACY); - if (sdkLocation == null || sdkLocation.length() == 0) { - throw new BuildException("SDK Location is not set."); - } - } - - File sdk = new File(sdkLocation); - if (sdk.isDirectory() == false) { - throw new BuildException(String.format("SDK Location '%s' is not valid.", sdkLocation)); - } + File sdk = TaskHelper.getSdkLocation(antProject); + String sdkLocation = sdk.getPath(); // display SDK Tools revision - int toolsRevison = getToolsRevision(sdk); + int toolsRevison = TaskHelper.getToolsRevision(sdk); if (toolsRevison != -1) { System.out.println("Android SDK Tools Revision " + toolsRevison); } @@ -179,6 +158,14 @@ public final class SetupTask extends ImportTask { "Unable to resolve target '%s'", targetHashString)); } + // display the project info + System.out.println("Project Target: " + androidTarget.getName()); + if (androidTarget.isPlatform() == false) { + System.out.println("Vendor: " + androidTarget.getVendor()); + System.out.println("Platform Version: " + androidTarget.getVersionName()); + } + System.out.println("API level: " + androidTarget.getVersion().getApiString()); + // check that this version of the custom Ant task can build this target int antBuildVersion = androidTarget.getProperty(SdkConstants.PROP_SDK_ANT_BUILD_REVISION, 1); @@ -192,6 +179,39 @@ public final class SetupTask extends ImportTask { + "***********************************************************\n\n\n"); } + if (antBuildVersion < 2) { + // these older rules are obselete, and not versioned, and therefore it's hard + // to maintain compatibility. + + // if the platform itself is obsolete, display a different warning + if (androidTarget.getVersion().getApiLevel() < 3 || + androidTarget.getVersion().getApiLevel() == 5 || + androidTarget.getVersion().getApiLevel() == 6) { + System.out.println("\n\n\n" + + "***********************************************************\n" + + "WARNING: This platform is obsolete and its Ant rules may not work properly.\n" + + "WARNING: It is recommended to develop against a newer version of Android.\n" + + "WARNING: For more information about active versions of Android see:\n" + + "WARNING: http://developer.android.com/resources/dashboard/platform-versions.html\n" + + "***********************************************************\n\n\n"); + } else { + IAndroidTarget baseTarget = + androidTarget.getParent() != null ? androidTarget.getParent() : androidTarget; + System.out.println(String.format("\n\n\n" + + "***********************************************************\n" + + "WARNING: Revision %1$d of %2$s uses obsolete Ant rules which may not work properly.\n" + + "WARNING: It is recommended that you download a newer revision if available.\n" + + "WARNING: For more information about updating your SDK, see:\n" + + "WARNING: http://developer.android.com/sdk/adding-components.html\n" + + "***********************************************************\n\n\n", + baseTarget.getRevision(), baseTarget.getFullName())); + } + } + + // set a property that contains the rules revision. This can be used by other custom + // tasks later. + antProject.setProperty(TaskHelper.PROP_RULES_REV, Integer.toString(antBuildVersion)); + // check if the project is a library boolean isLibrary = false; @@ -203,16 +223,9 @@ public final class SetupTask extends ImportTask { // look for referenced libraries. processReferencedLibraries(antProject, androidTarget); - // display the project info - System.out.println("Project Target: " + androidTarget.getName()); if (isLibrary) { - System.out.println("Type: Android Library"); - } - if (androidTarget.isPlatform() == false) { - System.out.println("Vendor: " + androidTarget.getVendor()); - System.out.println("Platform Version: " + androidTarget.getVersionName()); + System.out.println("Project Type: Android Library"); } - System.out.println("API level: " + androidTarget.getVersion().getApiString()); // do a quick check to make sure the target supports library. if (isLibrary && @@ -263,18 +276,6 @@ public final class SetupTask extends ImportTask { // finally sets the path in the project with a reference antProject.addReference(REF_CLASSPATH, bootclasspath); - // LEGACY support. android_rules.xml in older platforms expects properties with - // older names. This sets those properties to make sure the rules will work. - if (androidTarget.getVersion().getApiLevel() <= 4) { // 1.6 and earlier - antProject.setProperty(PROPERTY_ANDROID_JAR_LEGACY, androidJar); - antProject.setProperty(PROPERTY_ANDROID_AIDL_LEGACY, androidAidl); - antProject.setProperty(ProjectProperties.PROPERTY_SDK_LEGACY, sdkLocation); - String appPackage = antProject.getProperty(ProjectProperties.PROPERTY_APP_PACKAGE); - if (appPackage != null && appPackage.length() > 0) { - antProject.setProperty(ProjectProperties.PROPERTY_APP_PACKAGE_LEGACY, appPackage); - } - } - // Now the import section. This is only executed if the task actually has to import a file. if (mDoImport) { // find the folder containing the file to import @@ -504,29 +505,4 @@ public final class SetupTask extends ImportTask { antProject.setProperty("android.libraries.package", sb.toString()); } } - - /** - * Returns the revision of the tools for a given SDK. - * @param sdkFile the {@link File} for the root folder of the SDK - * @return the tools revision or -1 if not found. - */ - private int getToolsRevision(File sdkFile) { - Properties p = new Properties(); - try{ - // tools folder must exist, or this custom task wouldn't run! - File toolsFolder= new File(sdkFile, SdkConstants.FD_TOOLS); - File sourceProp = new File(toolsFolder, SdkConstants.FN_SOURCE_PROP); - p.load(new FileInputStream(sourceProp)); - String value = p.getProperty("Pkg.Revision"); //$NON-NLS-1$ - if (value != null) { - return Integer.parseInt(value); - } - } catch (FileNotFoundException e) { - // couldn't find the file? return -1 below. - } catch (IOException e) { - // couldn't find the file? return -1 below. - } - - return -1; - } } diff --git a/anttasks/src/com/android/ant/TaskHelper.java b/anttasks/src/com/android/ant/TaskHelper.java index 361725a..b79d274 100644 --- a/anttasks/src/com/android/ant/TaskHelper.java +++ b/anttasks/src/com/android/ant/TaskHelper.java @@ -16,12 +16,72 @@ package com.android.ant; +import com.android.sdklib.SdkConstants; +import com.android.sdklib.internal.project.ProjectProperties; + import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; import org.apache.tools.ant.types.Path; -public class TaskHelper { +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Properties; + +final class TaskHelper { + + public final static String PROP_RULES_REV = "android.ant.rules.revision"; + + static File getSdkLocation(Project antProject) { + // get the SDK location + String sdkLocation = antProject.getProperty(ProjectProperties.PROPERTY_SDK); + + // check if it's valid and exists + if (sdkLocation == null || sdkLocation.length() == 0) { + // LEGACY support: project created with 1.6 or before may be using a different + // property to declare the location of the SDK. At this point, we cannot + // yet check which target is running so we check both always. + sdkLocation = antProject.getProperty(ProjectProperties.PROPERTY_SDK_LEGACY); + if (sdkLocation == null || sdkLocation.length() == 0) { + throw new BuildException("SDK Location is not set."); + } + } + + File sdk = new File(sdkLocation); + if (sdk.isDirectory() == false) { + throw new BuildException(String.format("SDK Location '%s' is not valid.", sdkLocation)); + } - public static String checkSinglePath(String attribute, Path path) { + return sdk; + } + + /** + * Returns the revision of the tools for a given SDK. + * @param sdkFile the {@link File} for the root folder of the SDK + * @return the tools revision or -1 if not found. + */ + static int getToolsRevision(File sdkFile) { + Properties p = new Properties(); + try{ + // tools folder must exist, or this custom task wouldn't run! + File toolsFolder= new File(sdkFile, SdkConstants.FD_TOOLS); + File sourceProp = new File(toolsFolder, SdkConstants.FN_SOURCE_PROP); + p.load(new FileInputStream(sourceProp)); + String value = p.getProperty("Pkg.Revision"); //$NON-NLS-1$ + if (value != null) { + return Integer.parseInt(value); + } + } catch (FileNotFoundException e) { + // couldn't find the file? return -1 below. + } catch (IOException e) { + // couldn't find the file? return -1 below. + } + + return -1; + } + + static String checkSinglePath(String attribute, Path path) { String[] paths = path.list(); if (paths.length != 1) { throw new BuildException(String.format("Path value for '%1$s' is not valid.", attribute)); @@ -29,5 +89,4 @@ public class TaskHelper { return paths[0]; } - } |