Home | History | Annotate | Download | only in ant
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ant;
     18 
     19 import com.android.sdklib.internal.export.ApkData;
     20 import com.android.sdklib.internal.export.MultiApkExportHelper;
     21 import com.android.sdklib.internal.export.ProjectConfig;
     22 import com.android.sdklib.internal.export.MultiApkExportHelper.ExportException;
     23 import com.android.sdklib.internal.export.MultiApkExportHelper.Target;
     24 import com.android.sdklib.internal.project.ProjectProperties;
     25 
     26 import org.apache.tools.ant.BuildException;
     27 import org.apache.tools.ant.Project;
     28 import org.apache.tools.ant.Task;
     29 import org.apache.tools.ant.taskdefs.Input;
     30 import org.apache.tools.ant.taskdefs.Property;
     31 import org.apache.tools.ant.taskdefs.SubAnt;
     32 import org.apache.tools.ant.types.FileSet;
     33 import org.xml.sax.InputSource;
     34 
     35 import java.io.File;
     36 import java.io.FileInputStream;
     37 import java.io.FileNotFoundException;
     38 import java.util.HashSet;
     39 import java.util.List;
     40 import java.util.Map;
     41 import java.util.Set;
     42 import java.util.Map.Entry;
     43 
     44 import javax.xml.xpath.XPath;
     45 import javax.xml.xpath.XPathExpressionException;
     46 import javax.xml.xpath.XPathFactory;
     47 
     48 /**
     49  * Multiple APK export task.
     50  * This task is meant to replace {@link SetupTask} as the main setup/export task, importing
     51  * the rules and generating the export for all projects.
     52  */
     53 public class MultiApkExportTask extends Task {
     54 
     55     private Target mTarget;
     56     private XPathFactory mXPathFactory;
     57 
     58     public void setTarget(String target) {
     59         mTarget = Target.getTarget(target);
     60     }
     61 
     62     @Override
     63     public void execute() throws BuildException {
     64         Project antProject = getProject();
     65 
     66         if (mTarget == null) {
     67             throw new BuildException("'target' attribute not set.");
     68         }
     69 
     70         // get the SDK location
     71         File sdk = TaskHelper.getSdkLocation(antProject);
     72 
     73         // display SDK Tools revision
     74         int toolsRevison = TaskHelper.getToolsRevision(sdk);
     75         if (toolsRevison != -1) {
     76             System.out.println("Android SDK Tools Revision " + toolsRevison);
     77         }
     78 
     79         String appPackage = getValidatedProperty(antProject, ProjectProperties.PROPERTY_PACKAGE);
     80         System.out.println("Multi APK export for: " + appPackage);
     81         String version = getValidatedProperty(antProject, ProjectProperties.PROPERTY_VERSIONCODE);
     82         int versionCode;
     83         try {
     84             versionCode = Integer.parseInt(version);
     85         } catch (NumberFormatException e) {
     86             throw new BuildException("version value is not a valid integer.", e);
     87         }
     88         System.out.println("versionCode: " + version);
     89 
     90         // get the list of projects
     91         String projectList = getValidatedProperty(antProject, "projects");
     92 
     93         File rootFolder = antProject.getBaseDir();
     94         MultiApkExportHelper helper = new MultiApkExportHelper(rootFolder.getAbsolutePath(),
     95                 appPackage, versionCode, mTarget, System.out);
     96 
     97         try {
     98             if (mTarget == Target.CLEAN) {
     99                 // for a clean, we don't need the list of ApkData, we only need the list of
    100                 // projects
    101                 List<ProjectConfig> projects = helper.getProjects(projectList);
    102                 for (ProjectConfig projectConfig : projects) {
    103                     executeCleanSubAnt(antProject, projectConfig);
    104                 }
    105             } else {
    106                 // checks whether the projects can be signed.
    107                 String value = antProject.getProperty(ProjectProperties.PROPERTY_KEY_STORE);
    108                 String keyStore = value != null && value.length() > 0 ? value : null;
    109                 value = antProject.getProperty(ProjectProperties.PROPERTY_KEY_ALIAS);
    110                 String keyAlias = value != null && value.length() > 0 ? value : null;
    111                 boolean canSign = keyStore != null && keyAlias != null;
    112 
    113                 List<ApkData> apks = helper.getApkData(projectList);
    114 
    115                 // some temp var used by the project loop
    116                 HashSet<String> compiledProject = new HashSet<String>();
    117                 mXPathFactory = XPathFactory.newInstance();
    118 
    119                 File exportProjectOutput = new File(
    120                         getValidatedProperty(antProject, "out.absolute.dir"));
    121 
    122                 // if there's no error, and we can sign, prompt for the passwords.
    123                 String keyStorePassword = null;
    124                 String keyAliasPassword = null;
    125                 if (canSign) {
    126                     System.out.println("Found signing keystore and key alias. Need passwords.");
    127 
    128                     Input input = new Input();
    129                     input.setProject(antProject);
    130                     input.setAddproperty("key.store.password");
    131                     input.setMessage(String.format("Please enter keystore password (store: %1$s):",
    132                             keyStore));
    133                     input.execute();
    134 
    135                     input = new Input();
    136                     input.setProject(antProject);
    137                     input.setAddproperty("key.alias.password");
    138                     input.setMessage(String.format("Please enter password for alias '%1$s':",
    139                             keyAlias));
    140                     input.execute();
    141 
    142                     // and now read the property so that they can be set into the sub ant task.
    143                     keyStorePassword = getValidatedProperty(antProject, "key.store.password");
    144                     keyAliasPassword = getValidatedProperty(antProject, "key.alias.password");
    145                 }
    146 
    147                 for (ApkData apk : apks) {
    148 
    149                     Map<String, String> variantMap = apk.getSoftVariantMap();
    150 
    151                     if (variantMap.size() > 0) {
    152                         // if there are soft variants, only export those.
    153                         for (Entry<String, String> entry : variantMap.entrySet()) {
    154                             executeReleaseSubAnt(antProject, appPackage, versionCode, apk, entry,
    155                                     exportProjectOutput, canSign, keyStore, keyAlias,
    156                                     keyStorePassword, keyAliasPassword, compiledProject);
    157                         }
    158                     } else {
    159                         // do the full export.
    160                         executeReleaseSubAnt(antProject, appPackage, versionCode, apk, null,
    161                                 exportProjectOutput, canSign, keyStore, keyAlias,
    162                                 keyStorePassword, keyAliasPassword, compiledProject);
    163 
    164                     }
    165                 }
    166 
    167                 helper.writeLogs();
    168             }
    169         } catch (ExportException e) {
    170             // we only want to have Ant display the message, not the stack trace, since
    171             // we use Exceptions to report errors, so we build the BuildException only
    172             // with the message and not the cause exception.
    173             throw new BuildException(e.getMessage());
    174         }
    175     }
    176 
    177     /**
    178      * Creates and execute a clean sub ant task.
    179      * @param antProject the current Ant project
    180      * @param projectConfig the project to clean.
    181      */
    182     private void executeCleanSubAnt(Project antProject, ProjectConfig projectConfig) {
    183 
    184         String relativePath = projectConfig.getRelativePath();
    185 
    186         // this output is prepended by "[android-export] " (17 chars), so we put 61 stars
    187         System.out.println("\n*************************************************************");
    188         System.out.println("Cleaning project: " + relativePath);
    189 
    190         SubAnt subAnt = new SubAnt();
    191         subAnt.setTarget(mTarget.getTarget());
    192         subAnt.setProject(antProject);
    193 
    194         File subProjectFolder = projectConfig.getProjectFolder();
    195 
    196         FileSet fileSet = new FileSet();
    197         fileSet.setProject(antProject);
    198         fileSet.setDir(subProjectFolder);
    199         fileSet.setIncludes("build.xml");
    200         subAnt.addFileset(fileSet);
    201 
    202         // TODO: send the verbose flag from the main build.xml to the subAnt project.
    203         //subAnt.setVerbose(true);
    204 
    205         // end of the output by this task. Everything that follows will be output
    206         // by the subant.
    207         System.out.println("Calling to project's Ant file...");
    208         System.out.println("----------\n");
    209 
    210         subAnt.execute();
    211     }
    212 
    213     /**
    214      * Creates and executes a release sub ant task.
    215      * @param antProject the current Ant project
    216      * @param appPackage the application package string.
    217      * @param versionCode the current version of the application
    218      * @param apk the {@link ApkData} being exported.
    219      * @param softVariant the soft variant being exported, or null, if this is a full export.
    220      * @param exportProjectOutput the folder in which the files must be exported.
    221      * @param canSign whether the application package can be signed. This is dependent on the
    222      * availability of some required values.
    223      * @param keyStore the path to the keystore for signing
    224      * @param keyAlias the alias of the key to be used for signing
    225      * @param keyStorePassword the password of the keystore for signing
    226      * @param keyAliasPassword the password of the key alias for signing
    227      * @param compiledProject a list of projects that have already been compiled.
    228      */
    229     private void executeReleaseSubAnt(Project antProject, String appPackage, int versionCode,
    230             ApkData apk, Entry<String, String> softVariant, File exportProjectOutput,
    231             boolean canSign, String keyStore, String keyAlias,
    232             String keyStorePassword, String keyAliasPassword, Set<String> compiledProject) {
    233 
    234         String relativePath = apk.getProjectConfig().getRelativePath();
    235 
    236         // this output is prepended by "[android-export] " (17 chars), so we put 61 stars
    237         System.out.println("\n*************************************************************");
    238         System.out.println("Exporting project: " + relativePath);
    239 
    240         SubAnt subAnt = new SubAnt();
    241         subAnt.setTarget(mTarget.getTarget());
    242         subAnt.setProject(antProject);
    243 
    244         File subProjectFolder = apk.getProjectConfig().getProjectFolder();
    245 
    246         FileSet fileSet = new FileSet();
    247         fileSet.setProject(antProject);
    248         fileSet.setDir(subProjectFolder);
    249         fileSet.setIncludes("build.xml");
    250         subAnt.addFileset(fileSet);
    251 
    252         // TODO: send the verbose flag from the main build.xml to the subAnt project.
    253         //subAnt.setVerbose(true);
    254 
    255         // only do the compilation part if it's the first time we export
    256         // this project.
    257         // (projects can be export multiple time if some properties are set up to
    258         // generate more than one APK (for instance ABI split).
    259         if (compiledProject.contains(relativePath) == false) {
    260             compiledProject.add(relativePath);
    261         } else {
    262             addProp(subAnt, "do.not.compile", "true");
    263         }
    264 
    265         // set the version code, and filtering
    266         int compositeVersionCode = apk.getCompositeVersionCode(versionCode);
    267         addProp(subAnt, "version.code", Integer.toString(compositeVersionCode));
    268         System.out.println("Composite versionCode: " + compositeVersionCode);
    269         String abi = apk.getAbi();
    270         if (abi != null) {
    271             addProp(subAnt, "filter.abi", abi);
    272             System.out.println("ABI Filter: " + abi);
    273         }
    274 
    275         // set the output file names/paths. Keep all the temporary files in the project
    276         // folder, and only put the final file (which is different depending on whether
    277         // the file can be signed) locally.
    278 
    279         // read the base name from the build.xml file.
    280         String name = null;
    281         try {
    282             File buildFile = new File(subProjectFolder, "build.xml");
    283             XPath xPath = mXPathFactory.newXPath();
    284             name = xPath.evaluate("/project/@name",
    285                     new InputSource(new FileInputStream(buildFile)));
    286         } catch (XPathExpressionException e) {
    287             throw new BuildException("Failed to read build.xml", e);
    288         } catch (FileNotFoundException e) {
    289             throw new BuildException("build.xml is missing.", e);
    290         }
    291 
    292         // override the resource pack file as well as the final name
    293         String pkgName = name + "-" + apk.getBuildInfo();
    294         String finalNameRoot = appPackage + "-" + compositeVersionCode;
    295         if (softVariant != null) {
    296             String tmp = "-" + softVariant.getKey();
    297             pkgName += tmp;
    298             finalNameRoot += tmp;
    299 
    300             // set the resource filter.
    301             addProp(subAnt, "aapt.resource.filter", softVariant.getValue());
    302             System.out.println("res Filter: " + softVariant.getValue());
    303         }
    304 
    305         // set the resource pack file name.
    306         addProp(subAnt, "resource.package.file.name", pkgName + ".ap_");
    307 
    308         if (canSign) {
    309             // set the properties for the password.
    310             addProp(subAnt, ProjectProperties.PROPERTY_KEY_STORE, keyStore);
    311             addProp(subAnt, ProjectProperties.PROPERTY_KEY_ALIAS, keyAlias);
    312             addProp(subAnt, "key.store.password", keyStorePassword);
    313             addProp(subAnt, "key.alias.password", keyAliasPassword);
    314 
    315             // temporary file only get a filename change (still stored in the project
    316             // bin folder).
    317             addProp(subAnt, "out.unsigned.file.name",
    318                     name + "-" + apk.getBuildInfo() + "-unsigned.apk");
    319             addProp(subAnt, "out.unaligned.file",
    320                     name + "-" + apk.getBuildInfo() + "-unaligned.apk");
    321 
    322             // final file is stored locally with a name based on the package
    323             String outputName = finalNameRoot + "-release.apk";
    324             apk.setOutputName(softVariant != null ? softVariant.getKey() : null, outputName);
    325             addProp(subAnt, "out.release.file",
    326                     new File(exportProjectOutput, outputName).getAbsolutePath());
    327 
    328         } else {
    329             // put some empty prop. This is to override possible ones defined in the
    330             // project. The reason is that if there's more than one project, we don't
    331             // want some to signed and some not to be (and we don't want each project
    332             // to prompt for password.)
    333             addProp(subAnt, ProjectProperties.PROPERTY_KEY_STORE, "");
    334             addProp(subAnt, ProjectProperties.PROPERTY_KEY_ALIAS, "");
    335             // final file is the unsigned version. It gets stored locally.
    336             String outputName = finalNameRoot + "-unsigned.apk";
    337             apk.setOutputName(softVariant != null ? softVariant.getKey() : null, outputName);
    338             addProp(subAnt, "out.unsigned.file",
    339                     new File(exportProjectOutput, outputName).getAbsolutePath());
    340         }
    341 
    342         // end of the output by this task. Everything that follows will be output
    343         // by the subant.
    344         System.out.println("Calling to project's Ant file...");
    345         System.out.println("----------\n");
    346 
    347         subAnt.execute();
    348     }
    349 
    350     /**
    351      * Gets, validates and returns a project property.
    352      * The property must exist and be non empty.
    353      * @param antProject the project
    354      * @param name the name of the property to return.
    355      * @return the property value always (cannot be null).
    356      * @throws BuildException if the property is missing or not valid.
    357      */
    358     private String getValidatedProperty(Project antProject, String name) {
    359         String value = antProject.getProperty(name);
    360         if (value == null || value.length() == 0) {
    361             throw new BuildException(String.format("Property '%1$s' is not set or empty.", name));
    362         }
    363 
    364         return value;
    365     }
    366 
    367     /**
    368      * Adds a property to a {@link SubAnt} task.
    369      * @param task the task.
    370      * @param name the name of the property.
    371      * @param value the value of the property.
    372      */
    373     private void addProp(SubAnt task, String name, String value) {
    374         Property prop = new Property();
    375         prop.setName(name);
    376         prop.setValue(value);
    377         task.addProperty(prop);
    378     }
    379 }
    380