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