1 /*
2  * Copyright (C) 2010 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 vogar.android;
18 
19 import com.google.common.annotations.VisibleForTesting;
20 import java.io.File;
21 import java.io.FilenameFilter;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.List;
27 import vogar.Classpath;
28 import vogar.HostFileCache;
29 import vogar.Log;
30 import vogar.Md5Cache;
31 import vogar.ModeId;
32 import vogar.commands.Command;
33 import vogar.commands.Mkdir;
34 import vogar.util.Strings;
35 
36 /**
37  * Android SDK commands such as adb, aapt and dx.
38  */
39 public class AndroidSdk {
40 
41     private final Log log;
42     private final Mkdir mkdir;
43     private final File[] compilationClasspath;
44     private final String androidJarPath;
45     private final Md5Cache dexCache;
46 
defaultExpectations()47     public static Collection<File> defaultExpectations() {
48         File[] files = new File("libcore/expectations").listFiles(new FilenameFilter() {
49             // ignore obviously temporary files
50             public boolean accept(File dir, String name) {
51                 return !name.endsWith("~") && !name.startsWith(".");
52             }
53         });
54         return (files != null) ? Arrays.asList(files) : Collections.<File>emptyList();
55     }
56 
57     /**
58      * Create an {@link AndroidSdk}.
59      *
60      * <p>Searches the PATH used to run this and scans the file system in order to determine the
61      * compilation class path and android jar path.
62      */
createAndroidSdk( Log log, Mkdir mkdir, ModeId modeId, boolean useJack)63     public static AndroidSdk createAndroidSdk(
64             Log log, Mkdir mkdir, ModeId modeId, boolean useJack) {
65         List<String> path = new Command.Builder(log).args("which", "dx")
66                 .permitNonZeroExitStatus(true)
67                 .execute();
68         if (path.isEmpty()) {
69             throw new RuntimeException("dx not found");
70         }
71         File dx = new File(path.get(0)).getAbsoluteFile();
72         String parentFileName = getParentFileNOrLast(dx, 1).getName();
73 
74         List<String> adbPath = new Command.Builder(log)
75                 .args("which", "adb")
76                 .permitNonZeroExitStatus(true)
77                 .execute();
78 
79         File adb;
80         if (!adbPath.isEmpty()) {
81             adb = new File(adbPath.get(0));
82         } else {
83             adb = null;  // Could not find adb.
84         }
85 
86         /*
87          * Determine if we are running with a provided SDK or in the AOSP source tree.
88          *
89          * On Android SDK v23 (Marshmallow) the structure looks like:
90          *  <sdk>/build-tools/23.0.1/aapt
91          *  <sdk>/platform-tools/adb
92          *  <sdk>/build-tools/23.0.1/dx
93          *  <sdk>/platforms/android-23/android.jar
94          *
95          * Android build tree (target):
96          *  ${ANDROID_BUILD_TOP}/out/host/linux-x86/bin/aapt
97          *  ${ANDROID_BUILD_TOP}/out/host/linux-x86/bin/adb
98          *  ${ANDROID_BUILD_TOP}/out/host/linux-x86/bin/dx
99          *  ${ANDROID_BUILD_TOP}/out/target/common/obj/JAVA_LIBRARIES/core-libart_intermediates
100          *      /classes.jar
101          */
102 
103         File[] compilationClasspath;
104         String androidJarPath;
105 
106         // Accept that we are running in an SDK if the user has added the build-tools or
107         // platform-tools to their path.
108         boolean dxSdkPathValid = "build-tools".equals(getParentFileNOrLast(dx, 2).getName());
109         boolean isAdbPathValid = (adb != null) &&
110                 "platform-tools".equals(getParentFileNOrLast(adb, 1).getName());
111         if (dxSdkPathValid || isAdbPathValid) {
112             File sdkRoot = dxSdkPathValid ? getParentFileNOrLast(dx, 3)  // if dx path invalid then
113                                           : getParentFileNOrLast(adb, 2);  // adb must be valid.
114             File newestPlatform = getNewestPlatform(sdkRoot);
115             log.verbose("Using android platform: " + newestPlatform);
116             compilationClasspath = new File[] { new File(newestPlatform, "android.jar") };
117             androidJarPath = new File(newestPlatform.getAbsolutePath(), "android.jar")
118                     .getAbsolutePath();
119             log.verbose("using android sdk: " + sdkRoot);
120         } else if ("bin".equals(parentFileName)) {
121             log.verbose("Using android source build mode to find dependencies.");
122             String tmpJarPath = "prebuilts/sdk/current/android.jar";
123             String androidBuildTop = System.getenv("ANDROID_BUILD_TOP");
124             if (!com.google.common.base.Strings.isNullOrEmpty(androidBuildTop)) {
125                 tmpJarPath = androidBuildTop + "/prebuilts/sdk/current/android.jar";
126             } else {
127                 log.warn("Assuming current directory is android build tree root.");
128             }
129             androidJarPath = tmpJarPath;
130 
131             String outDir = System.getenv("OUT_DIR");
132             if (Strings.isNullOrEmpty(outDir)) {
133                 if (Strings.isNullOrEmpty(androidBuildTop)) {
134                     outDir = ".";
135                     log.warn("Assuming we are in android build tree root to find libraries.");
136                 } else {
137                     log.verbose("Using ANDROID_BUILD_TOP to find built libraries.");
138                     outDir = androidBuildTop;
139                 }
140                 outDir += "/out/";
141             } else {
142                 log.verbose("Using OUT_DIR environment variable for finding built libs.");
143                 outDir += "/";
144             }
145 
146             String pattern = outDir + "target/common/obj/JAVA_LIBRARIES/%s_intermediates/classes";
147             if (modeId.isHost()) {
148                 pattern = outDir + "host/common/obj/JAVA_LIBRARIES/%s_intermediates/classes";
149             }
150             pattern += ((useJack) ? ".jack" : ".jar");
151 
152             String[] jarNames = modeId.getJarNames();
153             compilationClasspath = new File[jarNames.length];
154             for (int i = 0; i < jarNames.length; i++) {
155                 String jar = jarNames[i];
156                 compilationClasspath[i] = new File(String.format(pattern, jar));
157             }
158         } else {
159             throw new RuntimeException("Couldn't derive Android home from " + dx);
160         }
161 
162         return new AndroidSdk(log, mkdir, compilationClasspath, androidJarPath,
163                 new HostFileCache(log, mkdir));
164     }
165 
166     @VisibleForTesting
AndroidSdk(Log log, Mkdir mkdir, File[] compilationClasspath, String androidJarPath, HostFileCache hostFileCache)167     AndroidSdk(Log log, Mkdir mkdir, File[] compilationClasspath, String androidJarPath,
168                HostFileCache hostFileCache) {
169         this.log = log;
170         this.mkdir = mkdir;
171         this.compilationClasspath = compilationClasspath;
172         this.androidJarPath = androidJarPath;
173         this.dexCache = new Md5Cache(log, "dex", hostFileCache);
174     }
175 
176     // Goes up N levels in the filesystem hierarchy. Return the last file that exists if this goes
177     // past /.
getParentFileNOrLast(File f, int n)178     private static File getParentFileNOrLast(File f, int n) {
179         File lastKnownExists = f;
180         for (int i = 0; i < n; i++) {
181             File parentFile = lastKnownExists.getParentFile();
182             if (parentFile == null) {
183                 return lastKnownExists;
184             }
185             lastKnownExists = parentFile;
186         }
187         return lastKnownExists;
188     }
189 
190     /**
191      * Returns the platform directory that has the highest API version. API
192      * platform directories are named like "android-9" or "android-11".
193      */
getNewestPlatform(File sdkRoot)194     private static File getNewestPlatform(File sdkRoot) {
195         File newestPlatform = null;
196         int newestPlatformVersion = 0;
197         File[] platforms = new File(sdkRoot, "platforms").listFiles();
198         if (platforms != null) {
199             for (File platform : platforms) {
200                 try {
201                     int version =
202                             Integer.parseInt(platform.getName().substring("android-".length()));
203                     if (version > newestPlatformVersion) {
204                         newestPlatform = platform;
205                         newestPlatformVersion = version;
206                     }
207                 } catch (NumberFormatException ignore) {
208                     // Ignore non-numeric preview versions like android-Honeycomb
209                 }
210             }
211         }
212         if (newestPlatform == null) {
213             throw new IllegalStateException("Cannot find newest platform in " + sdkRoot);
214         }
215         return newestPlatform;
216     }
217 
defaultSourcePath()218     public static Collection<File> defaultSourcePath() {
219         return filterNonExistentPathsFrom("libcore/support/src/test/java",
220                                           "external/mockwebserver/src/main/java/");
221     }
222 
filterNonExistentPathsFrom(String... paths)223     private static Collection<File> filterNonExistentPathsFrom(String... paths) {
224         ArrayList<File> result = new ArrayList<File>();
225         String buildRoot = System.getenv("ANDROID_BUILD_TOP");
226         for (String path : paths) {
227             File file = new File(buildRoot, path);
228             if (file.exists()) {
229                 result.add(file);
230             }
231         }
232         return result;
233     }
234 
getCompilationClasspath()235     public File[] getCompilationClasspath() {
236         return compilationClasspath;
237     }
238 
239     /**
240      * Converts all the .class files on 'classpath' into a dex file written to 'output'.
241      */
dex(File output, Classpath classpath)242     public void dex(File output, Classpath classpath) {
243         mkdir.mkdirs(output.getParentFile());
244 
245         String key = dexCache.makeKey(classpath);
246         if (key != null) {
247             boolean cacheHit = dexCache.getFromCache(output, key);
248             if (cacheHit) {
249                 log.verbose("dex cache hit for " + classpath);
250                 return;
251             }
252         }
253 
254         /*
255          * We pass --core-library so that we can write tests in the
256          * same package they're testing, even when that's a core
257          * library package. If you're actually just using this tool to
258          * execute arbitrary code, this has the unfortunate
259          * side-effect of preventing "dx" from protecting you from
260          * yourself.
261          *
262          * Memory options pulled from build/core/definitions.mk to
263          * handle large dx input when building dex for APK.
264          */
265         new Command.Builder(log)
266                 .args("dx")
267                 .args("-JXms16M")
268                 .args("-JXmx1536M")
269                 .args("--dex")
270                 .args("--output=" + output)
271                 .args("--core-library")
272                 .args((Object[]) Strings.objectsToStrings(classpath.getElements())).execute();
273         dexCache.insert(key, output);
274     }
275 
packageApk(File apk, File manifest)276     public void packageApk(File apk, File manifest) {
277         new Command(log, "aapt",
278                 "package",
279                 "-F", apk.getPath(),
280                 "-M", manifest.getPath(),
281                 "-I", androidJarPath,
282                 "--version-name", "1.0",
283                 "--version-code", "1").execute();
284     }
285 
addToApk(File apk, File dex)286     public void addToApk(File apk, File dex) {
287         new Command(log, "aapt", "add", "-k", apk.getPath(), dex.getPath()).execute();
288     }
289 
install(File apk)290     public void install(File apk) {
291         new Command(log, "adb", "install", "-r", apk.getPath()).execute();
292     }
293 
uninstall(String packageName)294     public void uninstall(String packageName) {
295         new Command.Builder(log)
296                 .args("adb", "uninstall", packageName)
297                 .permitNonZeroExitStatus(true)
298                 .execute();
299     }
300 }
301