2 * Copyright (C) 2009 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.ant;
19 import com.android.io.FileWrapper;
20 import com.android.io.FolderWrapper;
21 import com.android.sdklib.AndroidVersion;
22 import com.android.sdklib.IAndroidTarget;
23 import com.android.sdklib.ISdkLog;
24 import com.android.sdklib.SdkConstants;
25 import com.android.sdklib.SdkManager;
26 import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
27 import com.android.sdklib.internal.project.ProjectProperties;
28 import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
29 import com.android.sdklib.xml.AndroidManifest;
30 import com.android.sdklib.xml.AndroidXPathFactory;
32 import org.apache.tools.ant.BuildException;
33 import org.apache.tools.ant.Project;
34 import org.apache.tools.ant.Task;
35 import org.apache.tools.ant.types.Path;
36 import org.apache.tools.ant.types.Path.PathElement;
37 import org.apache.tools.ant.util.DeweyDecimal;
38 import org.xml.sax.InputSource;
41 import java.io.FileInputStream;
42 import java.io.FileNotFoundException;
43 import java.io.FilenameFilter;
44 import java.io.IOException;
45 import java.util.ArrayList;
46 import java.util.HashSet;
47 import java.util.List;
49 import javax.xml.xpath.XPath;
50 import javax.xml.xpath.XPathExpressionException;
53 * Setup Ant task. This task accomplishes:
55 * <li>Gets the project target hash string from {@link ProjectProperties#PROPERTY_TARGET},
56 * and resolves it to get the project's {@link IAndroidTarget}.</li>
58 * <li>Sets up properties so that aapt can find the android.jar and other files/folders in
59 * the resolved target.</li>
61 * <li>Sets up the boot classpath ref so that the <code>javac</code> task knows where to find
62 * the libraries. This includes the default android.jar from the resolved target but also optional
63 * libraries provided by the target (if any, when the target is an add-on).</li>
65 * <li>Resolve library dependencies and setup various Path references for them</li>
68 * This is used in the main rules file only.
71 public class NewSetupTask extends Task {
72 private final static String ANT_MIN_VERSION = "1.8.0";
74 private String mProjectTypeOut;
75 private String mAndroidJarFileOut;
76 private String mAndroidAidlFileOut;
77 private String mRenderScriptExeOut;
78 private String mRenderScriptIncludeDirOut;
79 private String mBootclasspathrefOut;
80 private String mProjectLibrariesRootOut;
81 private String mProjectLibrariesResOut;
82 private String mProjectLibrariesPackageOut;
83 private String mProjectLibrariesJarsOut;
84 private String mProjectLibrariesLibsOut;
85 private String mTargetApiOut;
87 public void setProjectTypeOut(String projectTypeOut) {
88 mProjectTypeOut = projectTypeOut;
91 public void setAndroidJarFileOut(String androidJarFileOut) {
92 mAndroidJarFileOut = androidJarFileOut;
95 public void setAndroidAidlFileOut(String androidAidlFileOut) {
96 mAndroidAidlFileOut = androidAidlFileOut;
99 public void setRenderScriptExeOut(String renderScriptExeOut) {
100 mRenderScriptExeOut = renderScriptExeOut;
103 public void setRenderScriptIncludeDirOut(String renderScriptIncludeDirOut) {
104 mRenderScriptIncludeDirOut = renderScriptIncludeDirOut;
107 public void setBootclasspathrefOut(String bootclasspathrefOut) {
108 mBootclasspathrefOut = bootclasspathrefOut;
111 public void setProjectLibrariesRootOut(String projectLibrariesRootOut) {
112 mProjectLibrariesRootOut = projectLibrariesRootOut;
115 public void setProjectLibrariesResOut(String projectLibrariesResOut) {
116 mProjectLibrariesResOut = projectLibrariesResOut;
119 public void setProjectLibrariesPackageOut(String projectLibrariesPackageOut) {
120 mProjectLibrariesPackageOut = projectLibrariesPackageOut;
123 public void setProjectLibrariesJarsOut(String projectLibrariesJarsOut) {
124 mProjectLibrariesJarsOut = projectLibrariesJarsOut;
127 public void setProjectLibrariesLibsOut(String projectLibrariesLibsOut) {
128 mProjectLibrariesLibsOut = projectLibrariesLibsOut;
131 public void setTargetApiOut(String targetApiOut) {
132 mTargetApiOut = targetApiOut;
136 public void execute() throws BuildException {
137 if (mProjectTypeOut == null) {
138 throw new BuildException("Missing attribute projectTypeOut");
140 if (mAndroidJarFileOut == null) {
141 throw new BuildException("Missing attribute androidJarFileOut");
143 if (mAndroidAidlFileOut == null) {
144 throw new BuildException("Missing attribute androidAidlFileOut");
146 if (mRenderScriptExeOut == null) {
147 throw new BuildException("Missing attribute renderScriptExeOut");
149 if (mRenderScriptIncludeDirOut == null) {
150 throw new BuildException("Missing attribute renderScriptIncludeDirOut");
152 if (mBootclasspathrefOut == null) {
153 throw new BuildException("Missing attribute bootclasspathrefOut");
155 if (mProjectLibrariesRootOut == null) {
156 throw new BuildException("Missing attribute projectLibrariesRootOut");
158 if (mProjectLibrariesResOut == null) {
159 throw new BuildException("Missing attribute projectLibrariesResOut");
161 if (mProjectLibrariesPackageOut == null) {
162 throw new BuildException("Missing attribute projectLibrariesPackageOut");
164 if (mProjectLibrariesJarsOut == null) {
165 throw new BuildException("Missing attribute projectLibrariesJarsOut");
167 if (mProjectLibrariesLibsOut == null) {
168 throw new BuildException("Missing attribute projectLibrariesLibsOut");
170 if (mTargetApiOut == null) {
171 throw new BuildException("Missing attribute targetApiOut");
175 Project antProject = getProject();
177 // check the Ant version
178 DeweyDecimal version = getVersion(antProject);
179 DeweyDecimal atLeast = new DeweyDecimal(ANT_MIN_VERSION);
180 if (atLeast.isGreaterThan(version)) {
181 throw new BuildException(
182 "The Android Ant-based build system requires Ant " +
184 " or later. Current version is " +
188 // get the SDK location
189 File sdkDir = TaskHelper.getSdkLocation(antProject);
190 String sdkOsPath = sdkDir.getPath();
192 // Make sure the OS sdk path ends with a directory separator
193 if (sdkOsPath.length() > 0 && !sdkOsPath.endsWith(File.separator)) {
194 sdkOsPath += File.separator;
197 // display SDK Tools revision
198 int toolsRevison = TaskHelper.getToolsRevision(sdkDir);
199 if (toolsRevison != -1) {
200 System.out.println("Android SDK Tools Revision " + toolsRevison);
203 // detect that the platform tools is there.
204 File platformTools = new File(sdkDir, SdkConstants.FD_PLATFORM_TOOLS);
205 if (platformTools.isDirectory() == false) {
206 throw new BuildException(String.format(
207 "SDK Platform Tools component is missing. " +
208 "Please install it with the SDK Manager (%1$s%2$c%3$s)",
209 SdkConstants.FD_TOOLS,
211 SdkConstants.androidCmdName()));
214 // get the target property value
215 String targetHashString = antProject.getProperty(ProjectProperties.PROPERTY_TARGET);
217 boolean isTestProject = false;
219 if (antProject.getProperty(ProjectProperties.PROPERTY_TESTED_PROJECT) != null) {
220 isTestProject = true;
223 if (targetHashString == null) {
224 throw new BuildException("Android Target is not set.");
227 // load up the sdk targets.
228 final ArrayList<String> messages = new ArrayList<String>();
229 SdkManager manager = SdkManager.createManager(sdkOsPath, new ISdkLog() {
230 public void error(Throwable t, String errorFormat, Object... args) {
231 if (errorFormat != null) {
232 messages.add(String.format("Error: " + errorFormat, args));
235 messages.add("Error: " + t.getMessage());
239 public void printf(String msgFormat, Object... args) {
240 messages.add(String.format(msgFormat, args));
243 public void warning(String warningFormat, Object... args) {
244 messages.add(String.format("Warning: " + warningFormat, args));
248 if (manager == null) {
249 // since we failed to parse the SDK, lets display the parsing output.
250 for (String msg : messages) {
251 System.out.println(msg);
253 throw new BuildException("Failed to parse SDK content.");
257 IAndroidTarget androidTarget = manager.getTargetFromHashString(targetHashString);
259 if (androidTarget == null) {
260 throw new BuildException(String.format(
261 "Unable to resolve target '%s'", targetHashString));
264 // display the project info
265 System.out.println("Project Target: " + androidTarget.getName());
266 if (androidTarget.isPlatform() == false) {
267 System.out.println("Vendor: " + androidTarget.getVendor());
268 System.out.println("Platform Version: " + androidTarget.getVersionName());
270 System.out.println("API level: " + androidTarget.getVersion().getApiString());
272 // check if the project is a library
273 boolean isLibrary = false;
275 String libraryProp = antProject.getProperty(ProjectProperties.PROPERTY_LIBRARY);
276 if (libraryProp != null) {
277 isLibrary = Boolean.valueOf(libraryProp).booleanValue();
281 System.out.println("Project Type: Android Library");
284 // look for referenced libraries.
285 processReferencedLibraries(antProject, androidTarget);
287 // always check the manifest minSdkVersion.
288 checkManifest(antProject, androidTarget.getVersion());
290 // sets up the properties to find android.jar/framework.aidl/target tools
291 String androidJar = androidTarget.getPath(IAndroidTarget.ANDROID_JAR);
292 antProject.setProperty(mAndroidJarFileOut, androidJar);
294 String androidAidl = androidTarget.getPath(IAndroidTarget.ANDROID_AIDL);
295 antProject.setProperty(mAndroidAidlFileOut, androidAidl);
297 Path includePath = new Path(antProject);
298 PathElement element = includePath.createPathElement();
299 element.setPath(androidTarget.getPath(IAndroidTarget.ANDROID_RS));
300 element = includePath.createPathElement();
301 element.setPath(androidTarget.getPath(IAndroidTarget.ANDROID_RS_CLANG));
302 antProject.setProperty(mRenderScriptIncludeDirOut, includePath.toString());
304 // TODO: figure out the actual compiler to use based on the minSdkVersion
305 antProject.setProperty(mRenderScriptExeOut,
306 sdkOsPath + SdkConstants.OS_SDK_PLATFORM_TOOLS_FOLDER +
307 SdkConstants.FN_RENDERSCRIPT);
309 // sets up the boot classpath
311 // create the Path object
312 Path bootclasspath = new Path(antProject);
314 // create a PathElement for the framework jar
315 element = bootclasspath.createPathElement();
316 element.setPath(androidJar);
318 // create PathElement for each optional library.
319 IOptionalLibrary[] libraries = androidTarget.getOptionalLibraries();
320 if (libraries != null) {
321 HashSet<String> visitedJars = new HashSet<String>();
322 for (IOptionalLibrary library : libraries) {
323 String jarPath = library.getJarPath();
324 if (visitedJars.contains(jarPath) == false) {
325 visitedJars.add(jarPath);
327 element = bootclasspath.createPathElement();
328 element.setPath(library.getJarPath());
333 // sets the path in the project with a reference
334 antProject.addReference(mBootclasspathrefOut, bootclasspath);
336 // finally set the project type.
338 antProject.setProperty(mProjectTypeOut, "library");
339 } else if (isTestProject) {
340 antProject.setProperty(mProjectTypeOut, "test");
342 antProject.setProperty(mProjectTypeOut, "project");
347 * Checks the manifest <code>minSdkVersion</code> attribute.
348 * @param antProject the ant project
349 * @param androidVersion the version of the platform the project is compiling against.
351 private void checkManifest(Project antProject, AndroidVersion androidVersion) {
353 File manifest = new File(antProject.getBaseDir(), SdkConstants.FN_ANDROID_MANIFEST_XML);
355 XPath xPath = AndroidXPathFactory.newXPath();
357 // check the package name.
358 String value = xPath.evaluate(
359 "/" + AndroidManifest.NODE_MANIFEST +
360 "/@" + AndroidManifest.ATTRIBUTE_PACKAGE,
361 new InputSource(new FileInputStream(manifest)));
362 if (value != null) { // aapt will complain if it's missing.
363 // only need to check that the package has 2 segments
364 if (value.indexOf('.') == -1) {
365 throw new BuildException(String.format(
366 "Application package '%1$s' must have a minimum of 2 segments.",
371 // check the minSdkVersion value
372 value = xPath.evaluate(
373 "/" + AndroidManifest.NODE_MANIFEST +
374 "/" + AndroidManifest.NODE_USES_SDK +
375 "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX + ":" +
376 AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
377 new InputSource(new FileInputStream(manifest)));
379 if (androidVersion.isPreview()) {
380 // in preview mode, the content of the minSdkVersion must match exactly the
381 // platform codename.
382 String codeName = androidVersion.getCodename();
383 if (codeName.equals(value) == false) {
384 throw new BuildException(String.format(
385 "For '%1$s' SDK Preview, attribute minSdkVersion in AndroidManifest.xml must be '%1$s' (current: %2$s)",
389 // set the API level to the previous API level (which is actually the value in
391 antProject.setProperty(mTargetApiOut,
392 Integer.toString(androidVersion.getApiLevel()));
394 } else if (value.length() > 0) {
395 // for normal platform, we'll only display warnings if the value is lower or higher
396 // than the target api level.
397 // First convert to an int.
398 int minSdkValue = -1;
400 minSdkValue = Integer.parseInt(value);
401 } catch (NumberFormatException e) {
402 // looks like it's not a number: error!
403 throw new BuildException(String.format(
404 "Attribute %1$s in AndroidManifest.xml must be an Integer!",
405 AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION));
408 // set the target api to the value
409 antProject.setProperty(mTargetApiOut, value);
411 int projectApiLevel = androidVersion.getApiLevel();
412 if (minSdkValue < projectApiLevel) {
413 System.out.println(String.format(
414 "WARNING: Attribute %1$s in AndroidManifest.xml (%2$d) is lower than the project target API level (%3$d)",
415 AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
416 minSdkValue, projectApiLevel));
417 } else if (minSdkValue > androidVersion.getApiLevel()) {
418 System.out.println(String.format(
419 "WARNING: Attribute %1$s in AndroidManifest.xml (%2$d) is higher than the project target API level (%3$d)",
420 AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
421 minSdkValue, projectApiLevel));
424 // no minSdkVersion? display a warning
426 "WARNING: No minSdkVersion value set. Application will install on all Android versions.");
428 // set the target api to 1
429 antProject.setProperty(mTargetApiOut, "1");
432 } catch (XPathExpressionException e) {
433 throw new BuildException(e);
434 } catch (FileNotFoundException e) {
435 throw new BuildException(e);
439 private void processReferencedLibraries(Project antProject, IAndroidTarget androidTarget) {
440 // prepare several paths for future tasks
441 Path rootPath = new Path(antProject);
442 Path resPath = new Path(antProject);
443 Path libsPath = new Path(antProject);
444 Path jarsPath = new Path(antProject);
445 StringBuilder packageStrBuilder = new StringBuilder();
447 FilenameFilter filter = new FilenameFilter() {
448 public boolean accept(File dir, String name) {
449 return name.toLowerCase().endsWith(".jar");
453 System.out.println("\n------------------\nResolving library dependencies:");
455 ArrayList<File> libraries = getProjectLibraries(antProject);
457 if (libraries.size() > 0) {
458 System.out.println("------------------\nOrdered libraries:");
460 for (File library : libraries) {
461 String libRootPath = library.getAbsolutePath();
462 System.out.println(libRootPath);
464 // get the root path.
465 PathElement element = rootPath.createPathElement();
466 element.setPath(libRootPath);
468 // get the res path. Always $PROJECT/res as well as the crunch cache.
469 element = resPath.createPathElement();
470 element.setPath(libRootPath + "/" + SdkConstants.FD_OUTPUT +
471 "/" + SdkConstants.FD_RES);
472 element = resPath.createPathElement();
473 element.setPath(libRootPath + "/" + SdkConstants.FD_RESOURCES);
475 // get the libs path. Always $PROJECT/libs
476 element = libsPath.createPathElement();
477 element.setPath(libRootPath + "/" + SdkConstants.FD_NATIVE_LIBS);
479 // get the jars from it too.
480 // 1. the library code jar
481 element = jarsPath.createPathElement();
482 element.setPath(libRootPath + "/" + SdkConstants.FD_OUTPUT +
483 "/" + SdkConstants.FN_CLASSES_JAR);
485 // 2. the 3rd party jar files
486 File libsFolder = new File(library, SdkConstants.FD_NATIVE_LIBS);
487 File[] jarFiles = libsFolder.listFiles(filter);
488 if (jarFiles != null) {
489 for (File jarFile : jarFiles) {
490 element = jarsPath.createPathElement();
491 element.setPath(jarFile.getAbsolutePath());
495 // get the package from the manifest.
496 FileWrapper manifest = new FileWrapper(library,
497 SdkConstants.FN_ANDROID_MANIFEST_XML);
500 String value = AndroidManifest.getPackage(manifest);
501 if (value != null) { // aapt will complain if it's missing.
502 packageStrBuilder.append(';');
503 packageStrBuilder.append(value);
505 } catch (Exception e) {
506 throw new BuildException(e);
510 System.out.println("No library dependencies.\n");
513 System.out.println("------------------\n");
515 // even with no libraries, always setup these so that various tasks in Ant don't complain
516 // (the task themselves can handle a ref to an empty Path)
517 antProject.addReference(mProjectLibrariesJarsOut, jarsPath);
518 antProject.addReference(mProjectLibrariesLibsOut, libsPath);
520 // the rest is done only if there's a library.
521 if (jarsPath.list().length > 0) {
522 antProject.addReference(mProjectLibrariesRootOut, rootPath);
523 antProject.addReference(mProjectLibrariesResOut, resPath);
524 antProject.setProperty(mProjectLibrariesPackageOut, packageStrBuilder.toString());
529 * Returns all the library dependencies of a given Ant project.
530 * @param antProject the Ant project
531 * @return a list of properties, sorted from highest priority to lowest.
533 private ArrayList<File> getProjectLibraries(final Project antProject) {
534 ArrayList<File> libraries = new ArrayList<File>();
535 File baseDir = antProject.getBaseDir();
537 // get the top level list of library dependencies.
538 List<File> topLevelLibraries = getDirectDependencies(baseDir, new IPropertySource() {
539 public String getProperty(String name) {
540 return antProject.getProperty(name);
544 // process the libraries in case they depend on other libraries.
545 resolveFullLibraryDependencies(topLevelLibraries, libraries);
551 * Resolves a given list of libraries, finds out if they depend on other libraries, and
552 * returns a full list of all the direct and indirect dependencies in the proper order (first
553 * is higher priority when calling aapt).
554 * @param inLibraries the libraries to resolve
555 * @param outLibraries where to store all the libraries.
557 private void resolveFullLibraryDependencies(List<File> inLibraries, List<File> outLibraries) {
558 // loop in the inverse order to resolve dependencies on the libraries, so that if a library
559 // is required by two higher level libraries it can be inserted in the correct place
560 for (int i = inLibraries.size() - 1 ; i >= 0 ; i--) {
561 File library = inLibraries.get(i);
563 // get the default.property file for it
564 final ProjectProperties projectProp = ProjectProperties.load(
565 new FolderWrapper(library), PropertyType.PROJECT);
568 List<File> dependencies = getDirectDependencies(library, new IPropertySource() {
569 public String getProperty(String name) {
570 return projectProp.getProperty(name);
574 // resolve the dependencies for those libraries
575 resolveFullLibraryDependencies(dependencies, outLibraries);
577 // and add the current one (if needed) in front (higher priority)
578 if (outLibraries.contains(library) == false) {
579 outLibraries.add(0, library);
584 public interface IPropertySource {
585 String getProperty(String name);
589 * Returns the top level library dependencies of a given <var>source</var> representing a
590 * project properties.
591 * @param baseFolder the base folder of the project (to resolve relative paths)
592 * @param source a source of project properties.
594 private List<File> getDirectDependencies(File baseFolder, IPropertySource source) {
595 ArrayList<File> libraries = new ArrayList<File>();
597 // first build the list. they are ordered highest priority first.
600 String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++);
601 String rootPath = source.getProperty(propName);
603 if (rootPath == null) {
608 File library = new File(baseFolder, rootPath).getCanonicalFile();
610 // check for validity
611 File projectProp = new File(library, PropertyType.PROJECT.getFilename());
612 if (projectProp.isFile() == false) {
614 throw new BuildException(String.format(
615 "%1$s resolve to a path with no %2$s file for project %3$s", rootPath,
616 PropertyType.PROJECT.getFilename(), baseFolder.getAbsolutePath()));
619 if (libraries.contains(library) == false) {
620 System.out.println(String.format("%1$s: %2$s => %3$s",
621 baseFolder.getAbsolutePath(), rootPath, library.getAbsolutePath()));
623 libraries.add(library);
625 } catch (IOException e) {
626 throw new BuildException("Failed to resolve library path: " + rootPath, e);
634 * Returns the Ant version as a {@link DeweyDecimal} object.
636 * This is based on the implementation of
637 * org.apache.tools.ant.taskdefs.condition.AntVersion.getVersion()
639 * @param antProject the current ant project.
640 * @return the ant version.
642 private DeweyDecimal getVersion(Project antProject) {
643 char[] versionString = antProject.getProperty("ant.version").toCharArray();
644 StringBuilder sb = new StringBuilder();
645 boolean foundFirstDigit = false;
646 for (int i = 0; i < versionString.length; i++) {
647 if (Character.isDigit(versionString[i])) {
648 sb.append(versionString[i]);
649 foundFirstDigit = true;
651 if (versionString[i] == '.' && foundFirstDigit) {
652 sb.append(versionString[i]);
654 if (Character.isLetter(versionString[i]) && foundFirstDigit) {
658 return new DeweyDecimal(sb.toString());