Home | History | Annotate | Download | only in ant
      1 /*
      2  * Copyright (C) 2009 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.AndroidVersion;
     20 import com.android.sdklib.IAndroidTarget;
     21 import com.android.sdklib.ISdkLog;
     22 import com.android.sdklib.SdkConstants;
     23 import com.android.sdklib.SdkManager;
     24 import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
     25 import com.android.sdklib.internal.project.ProjectProperties;
     26 import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
     27 import com.android.sdklib.io.FileWrapper;
     28 import com.android.sdklib.xml.AndroidManifest;
     29 import com.android.sdklib.xml.AndroidXPathFactory;
     30 
     31 import org.apache.tools.ant.BuildException;
     32 import org.apache.tools.ant.Project;
     33 import org.apache.tools.ant.taskdefs.ImportTask;
     34 import org.apache.tools.ant.types.Path;
     35 import org.apache.tools.ant.types.Path.PathElement;
     36 import org.xml.sax.InputSource;
     37 
     38 import java.io.File;
     39 import java.io.FileInputStream;
     40 import java.io.FileNotFoundException;
     41 import java.io.FilenameFilter;
     42 import java.io.IOException;
     43 import java.util.ArrayList;
     44 import java.util.HashSet;
     45 import java.util.Properties;
     46 
     47 import javax.xml.xpath.XPath;
     48 import javax.xml.xpath.XPathExpressionException;
     49 
     50 /**
     51  * Setup/Import Ant task. This task accomplishes:
     52  * <ul>
     53  * <li>Gets the project target hash string from {@link ProjectProperties#PROPERTY_TARGET},
     54  * and resolves it to get the project's {@link IAndroidTarget}.</li>
     55  * <li>Sets up properties so that aapt can find the android.jar in the resolved target.</li>
     56  * <li>Sets up the boot classpath ref so that the <code>javac</code> task knows where to find
     57  * the libraries. This includes the default android.jar from the resolved target but also optional
     58  * libraries provided by the target (if any, when the target is an add-on).</li>
     59  * <li>Imports the build rules located in the resolved target so that the build actually does
     60  * something. This can be disabled with the attribute <var>import</var> set to <code>false</code>
     61  * </li></ul>
     62  *
     63  * This is used in build.xml/template.
     64  *
     65  */
     66 public final class SetupTask extends ImportTask {
     67     /** current max version of the Ant rules that is supported */
     68     private final static int ANT_RULES_MAX_VERSION = 2;
     69 
     70     // legacy main rules file.
     71     private final static String RULES_LEGACY_MAIN = "android_rules.xml";
     72     // legacy test rules file - depends on android_rules.xml
     73     private final static String RULES_LEGACY_TEST = "android_test_rules.xml";
     74 
     75     // main rules file
     76     private final static String RULES_MAIN = "ant_rules_r%1$d.xml";
     77     // test rules file - depends on android_rules.xml
     78     private final static String RULES_TEST = "ant_test_rules_r%1$d.xml";
     79     // library rules file.
     80     private final static String RULES_LIBRARY = "ant_lib_rules_r%1$d.xml";
     81 
     82     // ant property with the path to the android.jar
     83     private final static String PROPERTY_ANDROID_JAR = "android.jar";
     84     // LEGACY - compatibility with 1.6 and before
     85     private final static String PROPERTY_ANDROID_JAR_LEGACY = "android-jar";
     86 
     87     // ant property with the path to the framework.jar
     88     private final static String PROPERTY_ANDROID_AIDL = "android.aidl";
     89     // LEGACY - compatibility with 1.6 and before
     90     private final static String PROPERTY_ANDROID_AIDL_LEGACY = "android-aidl";
     91 
     92     // ant property with the path to the aapt tool
     93     private final static String PROPERTY_AAPT = "aapt";
     94     // ant property with the path to the aidl tool
     95     private final static String PROPERTY_AIDL = "aidl";
     96     // ant property with the path to the dx tool
     97     private final static String PROPERTY_DX = "dx";
     98     // ref id to the <path> object containing all the boot classpaths.
     99     private final static String REF_CLASSPATH = "android.target.classpath";
    100 
    101     private boolean mDoImport = true;
    102 
    103     @Override
    104     public void execute() throws BuildException {
    105         Project antProject = getProject();
    106 
    107         // get the SDK location
    108         String sdkLocation = antProject.getProperty(ProjectProperties.PROPERTY_SDK);
    109 
    110         // check if it's valid and exists
    111         if (sdkLocation == null || sdkLocation.length() == 0) {
    112             // LEGACY support: project created with 1.6 or before may be using a different
    113             // property to declare the location of the SDK. At this point, we cannot
    114             // yet check which target is running so we check both always.
    115             sdkLocation = antProject.getProperty(ProjectProperties.PROPERTY_SDK_LEGACY);
    116             if (sdkLocation == null || sdkLocation.length() == 0) {
    117                 throw new BuildException("SDK Location is not set.");
    118             }
    119         }
    120 
    121         File sdk = new File(sdkLocation);
    122         if (sdk.isDirectory() == false) {
    123             throw new BuildException(String.format("SDK Location '%s' is not valid.", sdkLocation));
    124         }
    125 
    126         // display SDK Tools revision
    127         int toolsRevison = getToolsRevision(sdk);
    128         if (toolsRevison != -1) {
    129             System.out.println("Android SDK Tools Revision " + toolsRevison);
    130         }
    131 
    132         // get the target property value
    133         String targetHashString = antProject.getProperty(ProjectProperties.PROPERTY_TARGET);
    134 
    135         boolean isTestProject = false;
    136 
    137         if (antProject.getProperty("tested.project.dir") != null) {
    138             isTestProject = true;
    139         }
    140 
    141         if (targetHashString == null) {
    142             throw new BuildException("Android Target is not set.");
    143         }
    144 
    145         // load up the sdk targets.
    146         final ArrayList<String> messages = new ArrayList<String>();
    147         SdkManager manager = SdkManager.createManager(sdkLocation, new ISdkLog() {
    148             public void error(Throwable t, String errorFormat, Object... args) {
    149                 if (errorFormat != null) {
    150                     messages.add(String.format("Error: " + errorFormat, args));
    151                 }
    152                 if (t != null) {
    153                     messages.add("Error: " + t.getMessage());
    154                 }
    155             }
    156 
    157             public void printf(String msgFormat, Object... args) {
    158                 messages.add(String.format(msgFormat, args));
    159             }
    160 
    161             public void warning(String warningFormat, Object... args) {
    162                 messages.add(String.format("Warning: " + warningFormat, args));
    163             }
    164         });
    165 
    166         if (manager == null) {
    167             // since we failed to parse the SDK, lets display the parsing output.
    168             for (String msg : messages) {
    169                 System.out.println(msg);
    170             }
    171             throw new BuildException("Failed to parse SDK content.");
    172         }
    173 
    174         // resolve it
    175         IAndroidTarget androidTarget = manager.getTargetFromHashString(targetHashString);
    176 
    177         if (androidTarget == null) {
    178             throw new BuildException(String.format(
    179                     "Unable to resolve target '%s'", targetHashString));
    180         }
    181 
    182         // check that this version of the custom Ant task can build this target
    183         int antBuildVersion = androidTarget.getProperty(SdkConstants.PROP_SDK_ANT_BUILD_REVISION,
    184                 1);
    185         if (antBuildVersion > ANT_RULES_MAX_VERSION) {
    186             throw new BuildException(String.format(
    187                     "The project target (%1$s) requires a more recent version of the tools. Please update.",
    188                     androidTarget.getName()));
    189         }
    190 
    191         // check if the project is a library
    192         boolean isLibrary = false;
    193 
    194         String libraryProp = antProject.getProperty(ProjectProperties.PROPERTY_LIBRARY);
    195         if (libraryProp != null) {
    196             isLibrary = Boolean.valueOf(libraryProp).booleanValue();
    197         }
    198 
    199         // look for referenced libraries.
    200         processReferencedLibraries(antProject, androidTarget);
    201 
    202         // display the project info
    203         System.out.println("Project Target: " + androidTarget.getName());
    204         if (isLibrary) {
    205             System.out.println("Type: Android Library");
    206         }
    207         if (androidTarget.isPlatform() == false) {
    208             System.out.println("Vendor: " + androidTarget.getVendor());
    209             System.out.println("Platform Version: " + androidTarget.getVersionName());
    210         }
    211         System.out.println("API level: " + androidTarget.getVersion().getApiString());
    212 
    213         // do a quick check to make sure the target supports library.
    214         if (isLibrary &&
    215                 androidTarget.getProperty(SdkConstants.PROP_SDK_SUPPORT_LIBRARY, false) == false) {
    216             throw new BuildException(String.format(
    217                     "Project target '%1$s' does not support building libraries.",
    218                     androidTarget.getFullName()));
    219         }
    220 
    221         // always check the manifest minSdkVersion.
    222         checkManifest(antProject, androidTarget.getVersion());
    223 
    224         // sets up the properties to find android.jar/framework.aidl/target tools
    225         String androidJar = androidTarget.getPath(IAndroidTarget.ANDROID_JAR);
    226         antProject.setProperty(PROPERTY_ANDROID_JAR, androidJar);
    227 
    228         String androidAidl = androidTarget.getPath(IAndroidTarget.ANDROID_AIDL);
    229         antProject.setProperty(PROPERTY_ANDROID_AIDL, androidAidl);
    230 
    231         antProject.setProperty(PROPERTY_AAPT, androidTarget.getPath(IAndroidTarget.AAPT));
    232         antProject.setProperty(PROPERTY_AIDL, androidTarget.getPath(IAndroidTarget.AIDL));
    233         antProject.setProperty(PROPERTY_DX, androidTarget.getPath(IAndroidTarget.DX));
    234 
    235         // sets up the boot classpath
    236 
    237         // create the Path object
    238         Path bootclasspath = new Path(antProject);
    239 
    240         // create a PathElement for the framework jar
    241         PathElement element = bootclasspath.createPathElement();
    242         element.setPath(androidJar);
    243 
    244         // create PathElement for each optional library.
    245         IOptionalLibrary[] libraries = androidTarget.getOptionalLibraries();
    246         if (libraries != null) {
    247             HashSet<String> visitedJars = new HashSet<String>();
    248             for (IOptionalLibrary library : libraries) {
    249                 String jarPath = library.getJarPath();
    250                 if (visitedJars.contains(jarPath) == false) {
    251                     visitedJars.add(jarPath);
    252 
    253                     element = bootclasspath.createPathElement();
    254                     element.setPath(library.getJarPath());
    255                 }
    256             }
    257         }
    258 
    259         // finally sets the path in the project with a reference
    260         antProject.addReference(REF_CLASSPATH, bootclasspath);
    261 
    262         // LEGACY support. android_rules.xml in older platforms expects properties with
    263         // older names. This sets those properties to make sure the rules will work.
    264         if (androidTarget.getVersion().getApiLevel() <= 4) { // 1.6 and earlier
    265             antProject.setProperty(PROPERTY_ANDROID_JAR_LEGACY, androidJar);
    266             antProject.setProperty(PROPERTY_ANDROID_AIDL_LEGACY, androidAidl);
    267             antProject.setProperty(ProjectProperties.PROPERTY_SDK_LEGACY, sdkLocation);
    268             String appPackage = antProject.getProperty(ProjectProperties.PROPERTY_APP_PACKAGE);
    269             if (appPackage != null && appPackage.length() > 0) {
    270                 antProject.setProperty(ProjectProperties.PROPERTY_APP_PACKAGE_LEGACY, appPackage);
    271             }
    272         }
    273 
    274         // Now the import section. This is only executed if the task actually has to import a file.
    275         if (mDoImport) {
    276             // find the folder containing the file to import
    277             int folderID = antBuildVersion == 1 ? IAndroidTarget.TEMPLATES : IAndroidTarget.ANT;
    278             String rulesOSPath = androidTarget.getPath(folderID);
    279 
    280             // make sure the file exists.
    281             File rulesFolder = new File(rulesOSPath);
    282 
    283             if (rulesFolder.isDirectory() == false) {
    284                 throw new BuildException(String.format("Rules directory '%s' is missing.",
    285                         rulesOSPath));
    286             }
    287 
    288             String importedRulesFileName;
    289             if (antBuildVersion == 1) {
    290                 // legacy mode
    291                 importedRulesFileName = isTestProject ? RULES_LEGACY_TEST : RULES_LEGACY_MAIN;
    292             } else {
    293                 importedRulesFileName = String.format(
    294                         isLibrary ? RULES_LIBRARY : isTestProject ? RULES_TEST : RULES_MAIN,
    295                         antBuildVersion);;
    296             }
    297 
    298             // now check the rules file exists.
    299             File rules = new File(rulesFolder, importedRulesFileName);
    300 
    301             if (rules.isFile() == false) {
    302                 throw new BuildException(String.format("Build rules file '%s' is missing.",
    303                         rules));
    304             }
    305 
    306             // display the file being imported.
    307             // figure out the path relative to the SDK
    308             String rulesLocation = rules.getAbsolutePath();
    309             if (rulesLocation.startsWith(sdkLocation)) {
    310                 rulesLocation = rulesLocation.substring(sdkLocation.length());
    311                 if (rulesLocation.startsWith(File.separator)) {
    312                     rulesLocation = rulesLocation.substring(1);
    313                 }
    314             }
    315             System.out.println("Importing rules file: " + rulesLocation);
    316 
    317             // set the file location to import
    318             setFile(rules.getAbsolutePath());
    319 
    320             // and import
    321             super.execute();
    322         }
    323     }
    324 
    325     /**
    326      * Sets the value of the "import" attribute.
    327      * @param value the value.
    328      */
    329     public void setImport(boolean value) {
    330         mDoImport = value;
    331     }
    332 
    333     /**
    334      * Checks the manifest <code>minSdkVersion</code> attribute.
    335      * @param antProject the ant project
    336      * @param androidVersion the version of the platform the project is compiling against.
    337      */
    338     private void checkManifest(Project antProject, AndroidVersion androidVersion) {
    339         try {
    340             File manifest = new File(antProject.getBaseDir(), SdkConstants.FN_ANDROID_MANIFEST_XML);
    341 
    342             XPath xPath = AndroidXPathFactory.newXPath();
    343 
    344             // check the package name.
    345             String value = xPath.evaluate(
    346                     "/"  + AndroidManifest.NODE_MANIFEST +
    347                     "/@" + AndroidManifest.ATTRIBUTE_PACKAGE,
    348                     new InputSource(new FileInputStream(manifest)));
    349             if (value != null) { // aapt will complain if it's missing.
    350                 // only need to check that the package has 2 segments
    351                 if (value.indexOf('.') == -1) {
    352                     throw new BuildException(String.format(
    353                             "Application package '%1$s' must have a minimum of 2 segments.",
    354                             value));
    355                 }
    356             }
    357 
    358             // check the minSdkVersion value
    359             value = xPath.evaluate(
    360                     "/"  + AndroidManifest.NODE_MANIFEST +
    361                     "/"  + AndroidManifest.NODE_USES_SDK +
    362                     "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX + ":" +
    363                             AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
    364                     new InputSource(new FileInputStream(manifest)));
    365 
    366             if (androidVersion.isPreview()) {
    367                 // in preview mode, the content of the minSdkVersion must match exactly the
    368                 // platform codename.
    369                 String codeName = androidVersion.getCodename();
    370                 if (codeName.equals(value) == false) {
    371                     throw new BuildException(String.format(
    372                             "For '%1$s' SDK Preview, attribute minSdkVersion in AndroidManifest.xml must be '%1$s'",
    373                             codeName));
    374                 }
    375             } else if (value.length() > 0) {
    376                 // for normal platform, we'll only display warnings if the value is lower or higher
    377                 // than the target api level.
    378                 // First convert to an int.
    379                 int minSdkValue = -1;
    380                 try {
    381                     minSdkValue = Integer.parseInt(value);
    382                 } catch (NumberFormatException e) {
    383                     // looks like it's not a number: error!
    384                     throw new BuildException(String.format(
    385                             "Attribute %1$s in AndroidManifest.xml must be an Integer!",
    386                             AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION));
    387                 }
    388 
    389                 int projectApiLevel = androidVersion.getApiLevel();
    390                 if (minSdkValue < projectApiLevel) {
    391                     System.out.println(String.format(
    392                             "WARNING: Attribute %1$s in AndroidManifest.xml (%2$d) is lower than the project target API level (%3$d)",
    393                             AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
    394                             minSdkValue, projectApiLevel));
    395                 } else if (minSdkValue > androidVersion.getApiLevel()) {
    396                     System.out.println(String.format(
    397                             "WARNING: Attribute %1$s in AndroidManifest.xml (%2$d) is higher than the project target API level (%3$d)",
    398                             AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
    399                             minSdkValue, projectApiLevel));
    400                 }
    401             } else {
    402                 // no minSdkVersion? display a warning
    403                 System.out.println(
    404                         "WARNING: No minSdkVersion value set. Application will install on all Android versions.");
    405             }
    406 
    407         } catch (XPathExpressionException e) {
    408             throw new BuildException(e);
    409         } catch (FileNotFoundException e) {
    410             throw new BuildException(e);
    411         }
    412     }
    413 
    414     private void processReferencedLibraries(Project antProject, IAndroidTarget androidTarget) {
    415         // prepare several paths for future tasks
    416         Path sourcePath = new Path(antProject);
    417         Path resPath = new Path(antProject);
    418         Path libsPath = new Path(antProject);
    419         Path jarsPath = new Path(antProject);
    420         StringBuilder sb = new StringBuilder();
    421 
    422         FilenameFilter filter = new FilenameFilter() {
    423             public boolean accept(File dir, String name) {
    424                 return name.toLowerCase().endsWith(".jar");
    425             }
    426         };
    427 
    428         // get the build version for the current target. It'll be tested if there's at least
    429         // one library.
    430         boolean supportLibrary = androidTarget.getProperty(SdkConstants.PROP_SDK_SUPPORT_LIBRARY,
    431                 false);
    432 
    433         int index = 1;
    434         while (true) {
    435             String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++);
    436             String rootPath = antProject.getProperty(propName);
    437 
    438             if (rootPath == null) {
    439                 break;
    440             }
    441 
    442             if (supportLibrary == false) {
    443                 throw new BuildException(String.format(
    444                         "The build system for this project target (%1$s) does not support libraries",
    445                         androidTarget.getFullName()));
    446             }
    447 
    448             // get the source path. default is src but can be overriden by the property
    449             // "source.dir" in build.properties.
    450             PathElement element = sourcePath.createPathElement();
    451             ProjectProperties prop = ProjectProperties.load(rootPath, PropertyType.BUILD);
    452             String sourceDir = SdkConstants.FD_SOURCES;
    453             if (prop != null) {
    454                 String value = prop.getProperty(ProjectProperties.PROPERTY_BUILD_SOURCE_DIR);
    455                 if (value != null) {
    456                     sourceDir = value;
    457                 }
    458             }
    459 
    460             element.setPath(rootPath + "/" + sourceDir);
    461 
    462             // get the res path. Always $PROJECT/res
    463             element = resPath.createPathElement();
    464             element.setPath(rootPath + "/" + SdkConstants.FD_RESOURCES);
    465 
    466             // get the libs path. Always $PROJECT/libs
    467             element = libsPath.createPathElement();
    468             element.setPath(rootPath + "/" + SdkConstants.FD_NATIVE_LIBS);
    469 
    470             // get the jars from it too
    471             File libsFolder = new File(rootPath, SdkConstants.FD_NATIVE_LIBS);
    472             File[] jarFiles = libsFolder.listFiles(filter);
    473             for (File jarFile : jarFiles) {
    474                 element = jarsPath.createPathElement();
    475                 element.setPath(jarFile.getAbsolutePath());
    476             }
    477 
    478             // get the package from the manifest.
    479             FileWrapper manifest = new FileWrapper(rootPath, SdkConstants.FN_ANDROID_MANIFEST_XML);
    480             try {
    481                 String value = AndroidManifest.getPackage(manifest);
    482                 if (value != null) { // aapt will complain if it's missing.
    483                     sb.append(';');
    484                     sb.append(value);
    485                 }
    486             } catch (Exception e) {
    487                 throw new BuildException(e);
    488             }
    489         }
    490 
    491         // even with no libraries, always setup these so that various tasks in Ant don't complain
    492         // (the task themselves can handle a ref to an empty Path)
    493         antProject.addReference("android.libraries.src", sourcePath);
    494         antProject.addReference("android.libraries.jars", jarsPath);
    495         antProject.addReference("android.libraries.libs", libsPath);
    496 
    497         // the rest is done only if there's a library.
    498         if (sourcePath.list().length > 0) {
    499             antProject.addReference("android.libraries.res", resPath);
    500             antProject.setProperty("android.libraries.package", sb.toString());
    501         }
    502     }
    503 
    504     /**
    505      * Returns the revision of the tools for a given SDK.
    506      * @param sdkFile the {@link File} for the root folder of the SDK
    507      * @return the tools revision or -1 if not found.
    508      */
    509     private int getToolsRevision(File sdkFile) {
    510         Properties p = new Properties();
    511         try{
    512             // tools folder must exist, or this custom task wouldn't run!
    513             File toolsFolder= new File(sdkFile, SdkConstants.FD_TOOLS);
    514             File sourceProp = new File(toolsFolder, SdkConstants.FN_SOURCE_PROP);
    515             p.load(new FileInputStream(sourceProp));
    516             String value = p.getProperty("Pkg.Revision"); //$NON-NLS-1$
    517             if (value != null) {
    518                 return Integer.parseInt(value);
    519             }
    520         } catch (FileNotFoundException e) {
    521             // couldn't find the file? return -1 below.
    522         } catch (IOException e) {
    523             // couldn't find the file? return -1 below.
    524         }
    525 
    526         return -1;
    527     }
    528 }
    529