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 org.apache.tools.ant.BuildException; 20 import org.apache.tools.ant.Project; 21 import org.apache.tools.ant.taskdefs.ExecTask; 22 import org.apache.tools.ant.types.Path; 23 24 import java.io.File; 25 import java.util.ArrayList; 26 import java.util.Collections; 27 import java.util.List; 28 import java.util.Set; 29 30 /** 31 * Task to execute aapt. 32 * 33 * <p>It does not follow the exec task format, instead it has its own parameters, which maps 34 * directly to aapt.</p> 35 * <p>It is able to run aapt several times if library setup requires generating several 36 * R.java files. 37 * <p>The following map shows how to use the task for each supported aapt command line 38 * parameter.</p> 39 * 40 * <table border="1"> 41 * <tr><td><b>Aapt Option</b></td><td><b>Ant Name</b></td><td><b>Type</b></td></tr> 42 * <tr><td>path to aapt</td><td>executable</td><td>attribute (Path)</td> 43 * <tr><td>command</td><td>command</td><td>attribute (String)</td> 44 * <tr><td>-v</td><td>verbose</td><td>attribute (boolean)</td></tr> 45 * <tr><td>-f</td><td>force</td><td>attribute (boolean)</td></tr> 46 * <tr><td>-M AndroidManifest.xml</td><td>manifest</td><td>attribute (Path)</td></tr> 47 * <tr><td>-I base-package</td><td>androidjar</td><td>attribute (Path)</td></tr> 48 * <tr><td>-A asset-source-dir</td><td>assets</td><td>attribute (Path</td></tr> 49 * <tr><td>-S resource-sources</td><td><res path=""></td><td>nested element(s)<br>with attribute (Path)</td></tr> 50 * <tr><td>-0 extension</td><td><nocompress extension=""><br><nocompress></td><td>nested element(s)<br>with attribute (String)</td></tr> 51 * <tr><td>-F apk-file</td><td>apkfolder<br>outfolder<br>apkbasename<br>basename</td><td>attribute (Path)<br>attribute (Path) deprecated<br>attribute (String)<br>attribute (String) deprecated</td></tr> 52 * <tr><td>-J R-file-dir</td><td>rfolder</td><td>attribute (Path)<br>-m always enabled</td></tr> 53 * <tr><td></td><td></td><td></td></tr> 54 * </table> 55 */ 56 public final class AaptExecTask extends SingleDependencyTask { 57 58 /** 59 * Class representing a <nocompress> node in the main task XML. 60 * This let the developers prevent compression of some files in assets/ and res/raw/ 61 * by extension. 62 * If the extension is null, this will disable compression for all files in assets/ and 63 * res/raw/ 64 */ 65 public final static class NoCompress { 66 String mExtension; 67 68 /** 69 * Sets the value of the "extension" attribute. 70 * @param extention the extension. 71 */ 72 public void setExtension(String extention) { 73 mExtension = extention; 74 } 75 } 76 77 private String mExecutable; 78 private String mCommand; 79 private boolean mForce = true; // true due to legacy reasons 80 private boolean mDebug = false; 81 private boolean mVerbose = false; 82 private boolean mUseCrunchCache = false; 83 private int mVersionCode = 0; 84 private String mVersionName; 85 private String mManifest; 86 private ArrayList<Path> mResources; 87 private String mAssets; 88 private String mAndroidJar; 89 private String mApkFolder; 90 private String mApkName; 91 private String mResourceFilter; 92 private String mRFolder; 93 private final ArrayList<NoCompress> mNoCompressList = new ArrayList<NoCompress>(); 94 private String mProjectLibrariesResName; 95 private String mProjectLibrariesPackageName; 96 private boolean mNonConstantId; 97 98 /** 99 * Input path that ignores the same folders/files that aapt does. 100 */ 101 private static class ResFolderInputPath extends InputPath { 102 public ResFolderInputPath(File file, Set<String> extensionsToCheck) { 103 super(file, extensionsToCheck); 104 } 105 106 @Override 107 public boolean ignores(File file) { 108 String name = file.getName(); 109 char firstChar = name.charAt(0); 110 111 if (firstChar == '.' || (firstChar == '_' && file.isDirectory()) || 112 name.charAt(name.length()-1) == '~') { 113 return true; 114 } 115 116 if ("CVS".equals(name) || 117 "thumbs.db".equalsIgnoreCase(name) || 118 "picasa.ini".equalsIgnoreCase(name)) { 119 return true; 120 } 121 122 String ext = getExtension(name); 123 if ("scc".equalsIgnoreCase(ext)) { 124 return true; 125 } 126 127 return false; 128 } 129 } 130 131 private final static InputPathFactory sPathFactory = new InputPathFactory() { 132 133 public InputPath createPath(File file, Set<String> extensionsToCheck) { 134 return new ResFolderInputPath(file, extensionsToCheck); 135 } 136 }; 137 138 /** 139 * Sets the value of the "executable" attribute. 140 * @param executable the value. 141 */ 142 public void setExecutable(Path executable) { 143 mExecutable = TaskHelper.checkSinglePath("executable", executable); 144 } 145 146 /** 147 * Sets the value of the "command" attribute. 148 * @param command the value. 149 */ 150 public void setCommand(String command) { 151 mCommand = command; 152 } 153 154 /** 155 * Sets the value of the "force" attribute. 156 * @param force the value. 157 */ 158 public void setForce(boolean force) { 159 mForce = force; 160 } 161 162 /** 163 * Sets the value of the "verbose" attribute. 164 * @param verbose the value. 165 */ 166 public void setVerbose(boolean verbose) { 167 mVerbose = verbose; 168 } 169 170 /** 171 * Sets the value of the "usecrunchcache" attribute 172 * @param usecrunch whether to use the crunch cache. 173 */ 174 public void setNoCrunch(boolean nocrunch) { 175 mUseCrunchCache = nocrunch; 176 } 177 178 public void setNonConstantId(boolean nonConstantId) { 179 mNonConstantId = nonConstantId; 180 } 181 182 public void setVersioncode(String versionCode) { 183 if (versionCode.length() > 0) { 184 try { 185 mVersionCode = Integer.decode(versionCode); 186 } catch (NumberFormatException e) { 187 System.out.println(String.format( 188 "WARNING: Ignoring invalid version code value '%s'.", versionCode)); 189 } 190 } 191 } 192 193 /** 194 * Sets the value of the "versionName" attribute 195 * @param versionName the value 196 */ 197 public void setVersionname(String versionName) { 198 mVersionName = versionName; 199 } 200 201 public void setDebug(boolean value) { 202 mDebug = value; 203 } 204 205 /** 206 * Sets the value of the "manifest" attribute. 207 * @param manifest the value. 208 */ 209 public void setManifest(Path manifest) { 210 mManifest = TaskHelper.checkSinglePath("manifest", manifest); 211 } 212 213 /** 214 * Sets the value of the "resources" attribute. 215 * @param resources the value. 216 * 217 * @deprecated Use nested element(s) <res path="value" /> 218 */ 219 @Deprecated 220 public void setResources(Path resources) { 221 System.out.println("WARNNG: Using deprecated 'resources' attribute in AaptExecLoopTask." + 222 "Use nested element(s) <res path=\"value\" /> instead."); 223 if (mResources == null) { 224 mResources = new ArrayList<Path>(); 225 } 226 227 mResources.add(new Path(getProject(), resources.toString())); 228 } 229 230 /** 231 * Sets the value of the "assets" attribute. 232 * @param assets the value. 233 */ 234 public void setAssets(Path assets) { 235 mAssets = TaskHelper.checkSinglePath("assets", assets); 236 } 237 238 /** 239 * Sets the value of the "androidjar" attribute. 240 * @param androidJar the value. 241 */ 242 public void setAndroidjar(Path androidJar) { 243 mAndroidJar = TaskHelper.checkSinglePath("androidjar", androidJar); 244 } 245 246 /** 247 * Sets the value of the "outfolder" attribute. 248 * @param outFolder the value. 249 * @deprecated use {@link #setApkfolder(Path)} 250 */ 251 @Deprecated 252 public void setOutfolder(Path outFolder) { 253 System.out.println("WARNNG: Using deprecated 'outfolder' attribute in AaptExecLoopTask." + 254 "Use 'apkfolder' (path) instead."); 255 mApkFolder = TaskHelper.checkSinglePath("outfolder", outFolder); 256 } 257 258 /** 259 * Sets the value of the "apkfolder" attribute. 260 * @param apkFolder the value. 261 */ 262 public void setApkfolder(Path apkFolder) { 263 mApkFolder = TaskHelper.checkSinglePath("apkfolder", apkFolder); 264 } 265 266 /** 267 * Sets the value of the resourcefilename attribute 268 * @param apkName the value 269 */ 270 public void setResourcefilename(String apkName) { 271 mApkName = apkName; 272 } 273 274 /** 275 * Sets the value of the "rfolder" attribute. 276 * @param rFolder the value. 277 */ 278 public void setRfolder(Path rFolder) { 279 mRFolder = TaskHelper.checkSinglePath("rfolder", rFolder); 280 } 281 282 public void setresourcefilter(String filter) { 283 if (filter != null && filter.length() > 0) { 284 mResourceFilter = filter; 285 } 286 } 287 288 public void setProjectLibrariesResName(String projectLibrariesResName) { 289 mProjectLibrariesResName = projectLibrariesResName; 290 } 291 292 public void setProjectLibrariesPackageName(String projectLibrariesPackageName) { 293 mProjectLibrariesPackageName = projectLibrariesPackageName; 294 } 295 296 297 /** 298 * Returns an object representing a nested <var>nocompress</var> element. 299 */ 300 public Object createNocompress() { 301 NoCompress nc = new NoCompress(); 302 mNoCompressList.add(nc); 303 return nc; 304 } 305 306 /** 307 * Returns an object representing a nested <var>res</var> element. 308 */ 309 public Object createRes() { 310 if (mResources == null) { 311 mResources = new ArrayList<Path>(); 312 } 313 314 Path path = new Path(getProject()); 315 mResources.add(path); 316 317 return path; 318 } 319 320 /* 321 * (non-Javadoc) 322 * 323 * Executes the loop. Based on the values inside project.properties, this will 324 * create alternate temporary ap_ files. 325 * 326 * @see org.apache.tools.ant.Task#execute() 327 */ 328 @Override 329 public void execute() throws BuildException { 330 if (mProjectLibrariesResName == null) { 331 throw new BuildException("Missing attribute projectLibrariesResName"); 332 } 333 if (mProjectLibrariesPackageName == null) { 334 throw new BuildException("Missing attribute projectLibrariesPackageName"); 335 } 336 337 Project taskProject = getProject(); 338 339 String libPkgProp = null; 340 341 // if the parameters indicate generation of the R class, check if 342 // more R classes need to be created for libraries. 343 if (mRFolder != null && new File(mRFolder).isDirectory()) { 344 libPkgProp = taskProject.getProperty(mProjectLibrariesPackageName); 345 if (libPkgProp != null) { 346 // Replace ";" with ":" since that's what aapt expects 347 libPkgProp = libPkgProp.replace(';', ':'); 348 } 349 } 350 // Call aapt. If there are libraries, we'll pass a non-null string of libs. 351 callAapt(libPkgProp); 352 } 353 354 @Override 355 protected String getExecTaskName() { 356 return "aapt"; 357 } 358 359 /** 360 * Calls aapt with the given parameters. 361 * @param resourceFilter the resource configuration filter to pass to aapt (if configName is 362 * non null) 363 * @param extraPackages an optional list of colon-separated packages. Can be null 364 * Ex: com.foo.one:com.foo.two:com.foo.lib 365 */ 366 private void callAapt(String extraPackages) { 367 Project taskProject = getProject(); 368 369 final boolean generateRClass = mRFolder != null && new File(mRFolder).isDirectory(); 370 371 // Get whether we have libraries 372 Object libResRef = taskProject.getReference(mProjectLibrariesResName); 373 374 // Set up our input paths that matter for dependency checks 375 ArrayList<File> paths = new ArrayList<File>(); 376 377 // the project res folder is an input path of course 378 for (Path pathList : mResources) { 379 for (String path : pathList.list()) { 380 paths.add(new File(path)); 381 } 382 } 383 384 // and if libraries exist, their res folders folders too. 385 if (libResRef instanceof Path) { 386 for (String path : ((Path)libResRef).list()) { 387 paths.add(new File(path)); 388 } 389 } 390 391 // Now we figure out what we need to do 392 if (generateRClass) { 393 // in this case we only want to run aapt if an XML file was touched, or if any 394 // file is added/removed 395 List<InputPath> inputPaths = getInputPaths(paths, Collections.singleton("xml"), 396 sPathFactory); 397 398 // let's not forget the manifest as an input path (with no extension restrictions). 399 if (mManifest != null) { 400 inputPaths.add(new InputPath(new File(mManifest))); 401 } 402 403 // Check to see if our dependencies have changed. If not, then skip 404 if (initDependencies(mRFolder + File.separator + "R.java.d", inputPaths) 405 && dependenciesHaveChanged() == false) { 406 System.out.println("No changed resources. R.java and Manifest.java untouched."); 407 return; 408 } else { 409 System.out.println("Generating resource IDs..."); 410 } 411 } else { 412 // in this case we want to run aapt if any file was updated/removed/added in any of the 413 // input paths 414 List<InputPath> inputPaths = getInputPaths(paths, null /*extensionsToCheck*/, 415 sPathFactory); 416 417 // let's not forget the manifest as an input path. 418 if (mManifest != null) { 419 inputPaths.add(new InputPath(new File(mManifest))); 420 } 421 422 // If we're here to generate a .ap_ file we need to use assets as an input path as well. 423 if (mAssets != null) { 424 File assetsDir = new File(mAssets); 425 if (assetsDir.isDirectory()) { 426 inputPaths.add(new InputPath(assetsDir)); 427 } 428 } 429 430 // Find our dependency file. It should have the same name as our target .ap_ but 431 // with a .d extension 432 String dependencyFilePath = mApkFolder + File.separator + mApkName; 433 dependencyFilePath += ".d"; 434 435 // Check to see if our dependencies have changed 436 if (initDependencies(dependencyFilePath, inputPaths) 437 && dependenciesHaveChanged() == false) { 438 System.out.println("No changed resources or assets. " + mApkName 439 + " remains untouched"); 440 return; 441 } 442 if (mResourceFilter == null) { 443 System.out.println("Creating full resource package..."); 444 } else { 445 System.out.println(String.format( 446 "Creating resource package with filter: (%1$s)...", 447 mResourceFilter)); 448 } 449 } 450 451 // create a task for the default apk. 452 ExecTask task = new ExecTask(); 453 task.setExecutable(mExecutable); 454 task.setFailonerror(true); 455 456 task.setTaskName(getExecTaskName()); 457 458 // aapt command. Only "package" is supported at this time really. 459 task.createArg().setValue(mCommand); 460 461 // No crunch flag 462 if (mUseCrunchCache) { 463 task.createArg().setValue("--no-crunch"); 464 } 465 466 if (mNonConstantId) { 467 task.createArg().setValue("--non-constant-id"); 468 } 469 470 // force flag 471 if (mForce) { 472 task.createArg().setValue("-f"); 473 } 474 475 // verbose flag 476 if (mVerbose) { 477 task.createArg().setValue("-v"); 478 } 479 480 if (mDebug) { 481 task.createArg().setValue("--debug-mode"); 482 } 483 484 if (generateRClass) { 485 task.createArg().setValue("-m"); 486 } 487 488 // filters if needed 489 if (mResourceFilter != null) { 490 task.createArg().setValue("-c"); 491 task.createArg().setValue(mResourceFilter); 492 } 493 494 // no compress flag 495 // first look to see if there's a NoCompress object with no specified extension 496 boolean compressNothing = false; 497 for (NoCompress nc : mNoCompressList) { 498 if (nc.mExtension == null) { 499 task.createArg().setValue("-0"); 500 task.createArg().setValue(""); 501 compressNothing = true; 502 break; 503 } 504 } 505 506 if (compressNothing == false) { 507 for (NoCompress nc : mNoCompressList) { 508 task.createArg().setValue("-0"); 509 task.createArg().setValue(nc.mExtension); 510 } 511 } 512 513 if (extraPackages != null) { 514 task.createArg().setValue("--extra-packages"); 515 task.createArg().setValue(extraPackages); 516 } 517 518 // if the project contains libraries, force auto-add-overlay 519 if (libResRef != null) { 520 task.createArg().setValue("--auto-add-overlay"); 521 } 522 523 if (mVersionCode != 0) { 524 task.createArg().setValue("--version-code"); 525 task.createArg().setValue(Integer.toString(mVersionCode)); 526 } 527 528 if ((mVersionName != null) && (mVersionName.length() > 0)) { 529 task.createArg().setValue("--version-name"); 530 task.createArg().setValue(mVersionName); 531 } 532 533 // manifest location 534 if (mManifest != null) { 535 task.createArg().setValue("-M"); 536 task.createArg().setValue(mManifest); 537 } 538 539 // resources locations. 540 if (mResources.size() > 0) { 541 for (Path pathList : mResources) { 542 for (String path : pathList.list()) { 543 // This may not exists, and aapt doesn't like it, so we check first. 544 File res = new File(path); 545 if (res.isDirectory()) { 546 task.createArg().setValue("-S"); 547 task.createArg().setValue(path); 548 } 549 } 550 } 551 } 552 553 // add other resources coming from library project 554 if (libResRef instanceof Path) { 555 for (String path : ((Path)libResRef).list()) { 556 // This may not exists, and aapt doesn't like it, so we check first. 557 File res = new File(path); 558 if (res.isDirectory()) { 559 task.createArg().setValue("-S"); 560 task.createArg().setValue(path); 561 } 562 } 563 } 564 565 // assets location. This may not exists, and aapt doesn't like it, so we check first. 566 if (mAssets != null && new File(mAssets).isDirectory()) { 567 task.createArg().setValue("-A"); 568 task.createArg().setValue(mAssets); 569 } 570 571 // android.jar 572 if (mAndroidJar != null) { 573 task.createArg().setValue("-I"); 574 task.createArg().setValue(mAndroidJar); 575 } 576 577 // apk file. This is based on the apkFolder, apkBaseName, and the configName (if applicable) 578 String filename = null; 579 if (mApkName != null) { 580 filename = mApkName; 581 } 582 583 if (filename != null) { 584 File file = new File(mApkFolder, filename); 585 task.createArg().setValue("-F"); 586 task.createArg().setValue(file.getAbsolutePath()); 587 } 588 589 // R class generation 590 if (generateRClass) { 591 task.createArg().setValue("-J"); 592 task.createArg().setValue(mRFolder); 593 } 594 595 // Use dependency generation 596 task.createArg().setValue("--generate-dependencies"); 597 598 // final setup of the task 599 task.setProject(taskProject); 600 task.setOwningTarget(getOwningTarget()); 601 602 // execute it. 603 task.execute(); 604 } 605 } 606