/* * Copyright (C) 2009 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.SdkConstants; import com.android.sdklib.internal.build.SymbolLoader; import com.android.sdklib.internal.build.SymbolWriter; import com.android.xml.AndroidXPathFactory; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.taskdefs.ExecTask; import org.apache.tools.ant.types.Path; import org.xml.sax.InputSource; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpressionException; /** * Task to execute aapt. * * <p>It does not follow the exec task format, instead it has its own parameters, which maps * directly to aapt.</p> * <p>It is able to run aapt several times if library setup requires generating several * R.java files. * <p>The following map shows how to use the task for each supported aapt command line * parameter.</p> * * <table border="1"> * <tr><td><b>Aapt Option</b></td><td><b>Ant Name</b></td><td><b>Type</b></td></tr> * <tr><td>path to aapt</td><td>executable</td><td>attribute (Path)</td> * <tr><td>command</td><td>command</td><td>attribute (String)</td> * <tr><td>-v</td><td>verbose</td><td>attribute (boolean)</td></tr> * <tr><td>-f</td><td>force</td><td>attribute (boolean)</td></tr> * <tr><td>-M AndroidManifest.xml</td><td>manifest</td><td>attribute (Path)</td></tr> * <tr><td>-I base-package</td><td>androidjar</td><td>attribute (Path)</td></tr> * <tr><td>-A asset-source-dir</td><td>assets</td><td>attribute (Path</td></tr> * <tr><td>-S resource-sources</td><td><res path=""></td><td>nested element(s)<br>with attribute (Path)</td></tr> * <tr><td>-0 extension</td><td><nocompress extension=""><br><nocompress></td><td>nested element(s)<br>with attribute (String)</td></tr> * <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> * <tr><td>-J R-file-dir</td><td>rfolder</td><td>attribute (Path)<br>-m always enabled</td></tr> * <tr><td>--rename-manifest-package package-name</td><td>manifestpackage</td><td>attribute (String)</td></tr> * <tr><td></td><td></td><td></td></tr> * </table> */ public final class AaptExecTask extends SingleDependencyTask { /** * Class representing a <nocompress> node in the main task XML. * This let the developers prevent compression of some files in assets/ and res/raw/ * by extension. * If the extension is null, this will disable compression for all files in assets/ and * res/raw/ */ public static final class NoCompress { String mExtension; /** * Sets the value of the "extension" attribute. * @param extention the extension. */ public void setExtension(String extention) { mExtension = extention; } } private String mExecutable; private String mCommand; private boolean mForce = true; // true due to legacy reasons private boolean mDebug = false; private boolean mVerbose = false; private boolean mUseCrunchCache = false; private int mVersionCode = 0; private String mVersionName; private String mManifestFile; private String mManifestPackage; private String mOriginalManifestPackage; private ArrayList<Path> mResources; private String mAssets; private String mAndroidJar; private String mApkFolder; private String mApkName; private String mResourceFilter; private String mRFolder; private final ArrayList<NoCompress> mNoCompressList = new ArrayList<NoCompress>(); private String mLibraryResFolderPathRefid; private String mLibraryPackagesRefid; private String mLibraryRFileRefid; private boolean mNonConstantId; private String mIgnoreAssets; private String mBinFolder; private String mProguardFile; /** * Input path that ignores the same folders/files that aapt does. */ private static class ResFolderInputPath extends InputPath { public ResFolderInputPath(File file, Set<String> extensionsToCheck) { super(file, extensionsToCheck); } @Override public boolean ignores(File file) { String name = file.getName(); char firstChar = name.charAt(0); if (firstChar == '.' || (firstChar == '_' && file.isDirectory()) || name.charAt(name.length()-1) == '~') { return true; } if ("CVS".equals(name) || "thumbs.db".equalsIgnoreCase(name) || "picasa.ini".equalsIgnoreCase(name)) { return true; } String ext = getExtension(name); if ("scc".equalsIgnoreCase(ext)) { return true; } return false; } } private static final InputPathFactory sPathFactory = new InputPathFactory() { @Override public InputPath createPath(File file, Set<String> extensionsToCheck) { return new ResFolderInputPath(file, extensionsToCheck); } }; /** * Sets the value of the "executable" attribute. * @param executable the value. */ public void setExecutable(Path executable) { mExecutable = TaskHelper.checkSinglePath("executable", executable); } /** * Sets the value of the "command" attribute. * @param command the value. */ public void setCommand(String command) { mCommand = command; } /** * Sets the value of the "force" attribute. * @param force the value. */ public void setForce(boolean force) { mForce = force; } /** * Sets the value of the "verbose" attribute. * @param verbose the value. */ public void setVerbose(boolean verbose) { mVerbose = verbose; } /** * Sets the value of the "usecrunchcache" attribute * @param usecrunch whether to use the crunch cache. */ public void setNoCrunch(boolean usecrunch) { mUseCrunchCache = usecrunch; } public void setNonConstantId(boolean nonConstantId) { mNonConstantId = nonConstantId; } public void setIgnoreAssets(String ignoreAssets) { mIgnoreAssets = ignoreAssets; } public void setVersioncode(String versionCode) { if (!versionCode.isEmpty()) { try { mVersionCode = Integer.decode(versionCode); } catch (NumberFormatException e) { System.out.println(String.format( "WARNING: Ignoring invalid version code value '%s'.", versionCode)); } } } /** * Sets the value of the "versionName" attribute * @param versionName the value */ public void setVersionname(String versionName) { mVersionName = versionName; } public void setDebug(boolean value) { mDebug = value; } /** * Sets the value of the "manifest" attribute. * @param manifest the value. */ public void setManifest(Path manifest) { mManifestFile = TaskHelper.checkSinglePath("manifest", manifest); } /** * Sets a custom manifest package ID to be used during packaging.<p> * The manifest will be rewritten so that its package ID becomes the value given here. * Relative class names in the manifest (e.g. ".Foo") will be rewritten to absolute names based * on the existing package name, meaning that no code changes need to be made. * * @param packageName The package ID the APK should have. */ public void setManifestpackage(String packageName) { if (packageName != null && !packageName.isEmpty()) { mManifestPackage = packageName; } } /** * Sets the original package name found in the manifest. This is the package name where * the R class is created. * * This is merely a shortcut in case the package is known when calling the aapt task. If not * provided (and needed) this task will recompute it. * @param packageName the package name declared in the manifest. */ public void setOriginalManifestPackage(String packageName) { mOriginalManifestPackage = packageName; } /** * Sets the value of the "resources" attribute. * @param resources the 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>(); } mResources.add(new Path(getProject(), resources.toString())); } /** * Sets the value of the "assets" attribute. * @param assets the value. */ public void setAssets(Path assets) { mAssets = TaskHelper.checkSinglePath("assets", assets); } /** * Sets the value of the "androidjar" attribute. * @param androidJar the value. */ public void setAndroidjar(Path androidJar) { mAndroidJar = TaskHelper.checkSinglePath("androidjar", androidJar); } /** * Sets the value of the "outfolder" attribute. * @param outFolder the value. * @deprecated use {@link #setApkfolder(Path)} */ @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); } /** * Sets the value of the "apkfolder" attribute. * @param apkFolder the value. */ public void setApkfolder(Path apkFolder) { mApkFolder = TaskHelper.checkSinglePath("apkfolder", apkFolder); } /** * Sets the value of the resourcefilename attribute * @param apkName the value */ public void setResourcefilename(String apkName) { mApkName = apkName; } /** * Sets the value of the "rfolder" attribute. * @param rFolder the value. */ public void setRfolder(Path rFolder) { mRFolder = TaskHelper.checkSinglePath("rfolder", rFolder); } public void setresourcefilter(String filter) { if (filter != null && !filter.isEmpty()) { mResourceFilter = filter; } } /** * Set the property name of the property that contains the list of res folder for * Library Projects. This sets the name and not the value itself to handle the case where * it doesn't exist. * @param libraryResFolderPathRefid */ public void setLibraryResFolderPathRefid(String libraryResFolderPathRefid) { mLibraryResFolderPathRefid = libraryResFolderPathRefid; } public void setLibraryPackagesRefid(String libraryPackagesRefid) { mLibraryPackagesRefid = libraryPackagesRefid; } public void setLibraryRFileRefid(String libraryRFileRefid) { mLibraryRFileRefid = libraryRFileRefid; } public void setBinFolder(Path binFolder) { mBinFolder = TaskHelper.checkSinglePath("binFolder", binFolder); } public void setProguardFile(Path proguardFile) { mProguardFile = TaskHelper.checkSinglePath("proguardFile", proguardFile); } /** * Returns an object representing a nested <var>nocompress</var> element. */ public Object createNocompress() { NoCompress nc = new NoCompress(); mNoCompressList.add(nc); return nc; } /** * Returns an object representing a nested <var>res</var> element. */ public Object createRes() { if (mResources == null) { mResources = new ArrayList<Path>(); } Path path = new Path(getProject()); mResources.add(path); return path; } @Override protected String getExecTaskName() { return "aapt"; } /* * (non-Javadoc) * * Executes the loop. Based on the values inside project.properties, this will * create alternate temporary ap_ files. * * @see org.apache.tools.ant.Task#execute() */ @SuppressWarnings("deprecation") @Override public void execute() throws BuildException { if (mLibraryResFolderPathRefid == null) { throw new BuildException("Missing attribute libraryResFolderPathRefid"); } if (mLibraryPackagesRefid == null) { throw new BuildException("Missing attribute libraryPackagesRefid"); } if (mLibraryRFileRefid == null) { throw new BuildException("Missing attribute libraryRFileRefid"); } Project taskProject = getProject(); String libPkgProp = null; Path libRFileProp = null; // if the parameters indicate generation of the R class, check if // more R classes need to be created for libraries, only if this project itself // is not a library if (mNonConstantId == false && mRFolder != null && new File(mRFolder).isDirectory()) { libPkgProp = taskProject.getProperty(mLibraryPackagesRefid); Object rFilePath = taskProject.getReference(mLibraryRFileRefid); // if one is null, both should be if ((libPkgProp == null || rFilePath == null) && rFilePath != libPkgProp) { throw new BuildException(String.format( "Both %1$s and %2$s should resolve to valid values.", mLibraryPackagesRefid, mLibraryRFileRefid)); } if (rFilePath instanceof Path) { libRFileProp = (Path) rFilePath; } else if (rFilePath != null) { throw new BuildException("attribute libraryRFileRefid must reference a Path object."); } } final boolean generateRClass = mRFolder != null && new File(mRFolder).isDirectory(); // Get whether we have libraries Object libResRef = taskProject.getReference(mLibraryResFolderPathRefid); // Set up our input paths that matter for dependency checks ArrayList<File> paths = new ArrayList<File>(); // the project res folder is an input path of course for (Path pathList : mResources) { for (String path : pathList.list()) { paths.add(new File(path)); } } // and if libraries exist, their res folders folders too. if (libResRef instanceof Path) { for (String path : ((Path)libResRef).list()) { paths.add(new File(path)); } } // Now we figure out what we need to do if (generateRClass) { // in this case we only want to run aapt if an XML file was touched, or if any // file is added/removed List<InputPath> inputPaths = getInputPaths(paths, Collections.singleton("xml"), sPathFactory); // let's not forget the manifest as an input path (with no extension restrictions). if (mManifestFile != null) { inputPaths.add(new InputPath(new File(mManifestFile))); } // Check to see if our dependencies have changed. If not, then skip if (initDependencies(mRFolder + File.separator + "R.java.d", inputPaths) && dependenciesHaveChanged() == false) { System.out.println("No changed resources. R.java and Manifest.java untouched."); return; } else { System.out.println("Generating resource IDs..."); } } else { // in this case we want to run aapt if any file was updated/removed/added in any of the // input paths List<InputPath> inputPaths = getInputPaths(paths, null /*extensionsToCheck*/, sPathFactory); // let's not forget the manifest as an input path. if (mManifestFile != null) { inputPaths.add(new InputPath(new File(mManifestFile))); } // If we're here to generate a .ap_ file we need to use assets as an input path as well. if (mAssets != null) { File assetsDir = new File(mAssets); if (assetsDir.isDirectory()) { inputPaths.add(new InputPath(assetsDir)); } } // Find our dependency file. It should have the same name as our target .ap_ but // with a .d extension String dependencyFilePath = mApkFolder + File.separator + mApkName; dependencyFilePath += ".d"; // Check to see if our dependencies have changed if (initDependencies(dependencyFilePath, inputPaths) && dependenciesHaveChanged() == false) { System.out.println("No changed resources or assets. " + mApkName + " remains untouched"); return; } if (mResourceFilter == null) { System.out.println("Creating full resource package..."); } else { System.out.println(String.format( "Creating resource package with filter: (%1$s)...", mResourceFilter)); } } // create a task for the default apk. ExecTask task = new ExecTask(); task.setExecutable(mExecutable); task.setFailonerror(true); task.setTaskName(getExecTaskName()); // aapt command. Only "package" is supported at this time really. task.createArg().setValue(mCommand); // No crunch flag if (mUseCrunchCache) { task.createArg().setValue("--no-crunch"); } if (mNonConstantId) { task.createArg().setValue("--non-constant-id"); } // force flag if (mForce) { task.createArg().setValue("-f"); } // verbose flag if (mVerbose) { task.createArg().setValue("-v"); } if (mDebug) { task.createArg().setValue("--debug-mode"); } if (generateRClass) { task.createArg().setValue("-m"); } // filters if needed if (mResourceFilter != null && !mResourceFilter.isEmpty()) { task.createArg().setValue("-c"); task.createArg().setValue(mResourceFilter); } // no compress flag // first look to see if there's a NoCompress object with no specified extension boolean compressNothing = false; for (NoCompress nc : mNoCompressList) { if (nc.mExtension == null) { task.createArg().setValue("-0"); task.createArg().setValue(""); compressNothing = true; break; } } // never compress apks. task.createArg().setValue("-0"); task.createArg().setValue("apk"); if (compressNothing == false) { for (NoCompress nc : mNoCompressList) { task.createArg().setValue("-0"); task.createArg().setValue(nc.mExtension); } } // if this is a library or there are library dependencies if (mNonConstantId || (libPkgProp != null && !libPkgProp.isEmpty())) { if (mBinFolder == null) { throw new BuildException( "Missing attribute binFolder when compiling libraries or projects with libraries."); } task.createArg().setValue("--output-text-symbols"); task.createArg().setValue(mBinFolder); } // if the project contains libraries, force auto-add-overlay if (libResRef != null) { task.createArg().setValue("--auto-add-overlay"); } if (mVersionCode != 0) { task.createArg().setValue("--version-code"); task.createArg().setValue(Integer.toString(mVersionCode)); } if (mVersionName != null && !mVersionName.isEmpty()) { task.createArg().setValue("--version-name"); task.createArg().setValue(mVersionName); } // manifest location if (mManifestFile != null && !mManifestFile.isEmpty()) { task.createArg().setValue("-M"); task.createArg().setValue(mManifestFile); } // Rename manifest package if (mManifestPackage != null) { task.createArg().setValue("--rename-manifest-package"); task.createArg().setValue(mManifestPackage); } // resources locations. if (!mResources.isEmpty()) { for (Path pathList : mResources) { for (String path : pathList.list()) { // This may not exists, and aapt doesn't like it, so we check first. File res = new File(path); if (res.isDirectory()) { task.createArg().setValue("-S"); task.createArg().setValue(path); } } } } // add other resources coming from library project if (libResRef instanceof Path) { for (String path : ((Path)libResRef).list()) { // This may not exists, and aapt doesn't like it, so we check first. File res = new File(path); if (res.isDirectory()) { task.createArg().setValue("-S"); task.createArg().setValue(path); } } } // assets location. This may not exists, and aapt doesn't like it, so we check first. if (mAssets != null && new File(mAssets).isDirectory()) { task.createArg().setValue("-A"); task.createArg().setValue(mAssets); } // android.jar if (mAndroidJar != null) { task.createArg().setValue("-I"); task.createArg().setValue(mAndroidJar); } // apk file. This is based on the apkFolder, apkBaseName, and the configName (if applicable) String filename = null; if (mApkName != null) { filename = mApkName; } if (filename != null) { File file = new File(mApkFolder, filename); task.createArg().setValue("-F"); task.createArg().setValue(file.getAbsolutePath()); } // R class generation if (generateRClass) { task.createArg().setValue("-J"); task.createArg().setValue(mRFolder); } // ignore assets flag if (mIgnoreAssets != null && !mIgnoreAssets.isEmpty()) { task.createArg().setValue("--ignore-assets"); task.createArg().setValue(mIgnoreAssets); } // Use dependency generation task.createArg().setValue("--generate-dependencies"); // use the proguard file if (mProguardFile != null && !mProguardFile.isEmpty()) { task.createArg().setValue("-G"); task.createArg().setValue(mProguardFile); } // final setup of the task task.setProject(taskProject); task.setOwningTarget(getOwningTarget()); // execute it. task.execute(); // now if the project has libraries, R needs to be created for each libraries // but only if the project is not a library. try { if (!mNonConstantId && libPkgProp != null && !libPkgProp.isEmpty()) { File rFile = new File(mBinFolder, SdkConstants.FN_RESOURCE_TEXT); if (rFile.isFile()) { // Load the full symbols from the full R.txt file. SymbolLoader fullSymbolValues = new SymbolLoader(rFile); fullSymbolValues.load(); // we have two props which contains list of items. Both items represent // 2 data of a single property. // Don't want to use guava's splitter because it doesn't provide a list of the // result. but we know the list starts with a ; so strip it. if (libPkgProp.startsWith(";")) { libPkgProp = libPkgProp.substring(1).trim(); } String[] packages = libPkgProp.split(";"); String[] rFiles = libRFileProp.list(); if (packages.length != rFiles.length) { throw new BuildException(String.format( "%1$s and %2$s must contain the same number of items.", mLibraryPackagesRefid, mLibraryRFileRefid)); } if (mOriginalManifestPackage == null) { mOriginalManifestPackage = getPackageName(mManifestFile); } Multimap<String, SymbolLoader> libMap = ArrayListMultimap.create(); // First pass processing the libraries, collecting them by packageName, // and ignoring the ones that have the same package name as the application // (since that R class was already created). for (int i = 0 ; i < packages.length ; i++) { String libPackage = packages[i]; // skip libraries that have the same package name as the application. if (mOriginalManifestPackage.equals(libPackage)) { continue; } File rText = new File(rFiles[i]); if (rText.isFile()) { // load the lib symbols SymbolLoader libSymbols = new SymbolLoader(rText); libSymbols.load(); // store these symbols by associating them with the package name. libMap.put(libPackage, libSymbols); } } // now loop on all the package names, merge all the symbols to write, // and write them for (String packageName : libMap.keySet()) { Collection<SymbolLoader> symbols = libMap.get(packageName); SymbolWriter writer = new SymbolWriter(mRFolder, packageName, fullSymbolValues); for (SymbolLoader symbolLoader : symbols) { writer.addSymbolsToWrite(symbolLoader); } writer.write(); } } } } catch (Exception e) { // HACK alert. // in order for this step to happen again when this part fails, we delete // the dependency file. File f = new File(mRFolder, "R.java.d"); f.delete(); throw (e instanceof BuildException) ? (BuildException)e : new BuildException(e); } } private String getPackageName(String manifest) { XPath xpath = AndroidXPathFactory.newXPath(); try { String s = xpath.evaluate("/manifest/@package", new InputSource(new FileInputStream(manifest))); return s; } catch (XPathExpressionException e) { throw new BuildException(e); } catch (FileNotFoundException e) { throw new BuildException(e); } } }