/*
* Copyright 2012-present Facebook, Inc.
*
* 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.facebook.buck.android;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.environment.Platform;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import java.io.File;
import java.io.FilenameFilter;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Represents a platform to target for Android. Eventually, it should be possible to construct an
* arbitrary platform target, but currently, we only recognize a fixed set of targets.
*/
public class AndroidPlatformTarget {
public static final String DEFAULT_ANDROID_PLATFORM_TARGET = "android-23";
/** {@link Supplier} for an {@link AndroidPlatformTarget} that always throws. */
public static final Supplier<AndroidPlatformTarget> EXPLODING_ANDROID_PLATFORM_TARGET_SUPPLIER =
() -> {
throw new HumanReadableException(
"Must set ANDROID_SDK to point to the absolute path of your Android SDK directory.");
};
@VisibleForTesting
static final Pattern PLATFORM_TARGET_PATTERN =
Pattern.compile("(?:Google Inc\\.:Google APIs:|android-)(.+)");
private final String name;
private final Path androidJar;
private final List<Path> bootclasspathEntries;
private final Path aaptExecutable;
private final Path aapt2Executable;
private final Path adbExecutable;
private final Path aidlExecutable;
private final Path zipalignExecutable;
private final Path dxExecutable;
private final Path androidFrameworkIdlFile;
private final Path proguardJar;
private final Path proguardConfig;
private final Path optimizedProguardConfig;
private final AndroidDirectoryResolver androidDirectoryResolver;
private AndroidPlatformTarget(
String name,
Path androidJar,
List<Path> bootclasspathEntries,
Path aaptExecutable,
Path aapt2Executable,
Path adbExecutable,
Path aidlExecutable,
Path zipalignExecutable,
Path dxExecutable,
Path androidFrameworkIdlFile,
Path proguardJar,
Path proguardConfig,
Path optimizedProguardConfig,
AndroidDirectoryResolver androidDirectoryResolver) {
this.name = name;
this.androidJar = androidJar;
this.bootclasspathEntries = ImmutableList.copyOf(bootclasspathEntries);
this.aaptExecutable = aaptExecutable;
this.aapt2Executable = aapt2Executable;
this.adbExecutable = adbExecutable;
this.aidlExecutable = aidlExecutable;
this.zipalignExecutable = zipalignExecutable;
this.dxExecutable = dxExecutable;
this.androidFrameworkIdlFile = androidFrameworkIdlFile;
this.proguardJar = proguardJar;
this.proguardConfig = proguardConfig;
this.optimizedProguardConfig = optimizedProguardConfig;
this.androidDirectoryResolver = androidDirectoryResolver;
}
/** This is likely something like {@code "Google Inc.:Google APIs:21"}. */
public String getName() {
return name;
}
@Override
public String toString() {
return getName();
}
public Path getAndroidJar() {
return androidJar;
}
/** @return bootclasspath entries as absolute {@link Path}s */
public List<Path> getBootclasspathEntries() {
return bootclasspathEntries;
}
public Path getAaptExecutable() {
return aaptExecutable;
}
public Path getAapt2Executable() {
return aapt2Executable;
}
public Path getAdbExecutable() {
return adbExecutable;
}
public Path getAidlExecutable() {
return aidlExecutable;
}
public Path getZipalignExecutable() {
return zipalignExecutable;
}
public Path getDxExecutable() {
return dxExecutable;
}
public Path getAndroidFrameworkIdlFile() {
return androidFrameworkIdlFile;
}
public Path getProguardJar() {
return proguardJar;
}
public Path getProguardConfig() {
return proguardConfig;
}
public Path getOptimizedProguardConfig() {
return optimizedProguardConfig;
}
public Optional<Path> getNdkDirectory() {
return androidDirectoryResolver.getNdkOrAbsent();
}
public Path checkNdkDirectory() {
return androidDirectoryResolver.getNdkOrThrow();
}
public Optional<Path> getSdkDirectory() {
return androidDirectoryResolver.getSdkOrAbsent();
}
public Path checkSdkDirectory() {
return androidDirectoryResolver.getSdkOrThrow();
}
/** @param platformId for the platform, such as "Google Inc.:Google APIs:16" */
public static AndroidPlatformTarget getTargetForId(
String platformId,
AndroidDirectoryResolver androidDirectoryResolver,
Optional<Path> aaptOverride,
Optional<Path> aapt2Override) {
Matcher platformMatcher = PLATFORM_TARGET_PATTERN.matcher(platformId);
if (platformMatcher.matches()) {
String apiLevel = platformMatcher.group(1);
Factory platformTargetFactory;
if (platformId.contains("Google APIs")) {
platformTargetFactory = new AndroidWithGoogleApisFactory();
} else {
platformTargetFactory = new AndroidWithoutGoogleApisFactory();
}
return platformTargetFactory.newInstance(
androidDirectoryResolver, apiLevel, aaptOverride, aapt2Override);
} else {
String messagePrefix =
String.format("The Android SDK for '%s' could not be found. ", platformId);
throw new HumanReadableException(
messagePrefix
+ "Must set ANDROID_SDK to point to the absolute path of your Android SDK directory.");
}
}
public static AndroidPlatformTarget getDefaultPlatformTarget(
AndroidDirectoryResolver androidDirectoryResolver,
Optional<Path> aaptOverride,
Optional<Path> aapt2Override) {
return getTargetForId(
DEFAULT_ANDROID_PLATFORM_TARGET, androidDirectoryResolver, aaptOverride, aapt2Override);
}
private interface Factory {
AndroidPlatformTarget newInstance(
AndroidDirectoryResolver androidDirectoryResolver,
String apiLevel,
Optional<Path> aaptOverride,
Optional<Path> aapt2Override);
}
/**
* Given the path to the Android SDK as well as the platform path within the Android SDK, find all
* the files needed to create the {@link AndroidPlatformTarget}, assuming that the organization of
* the Android SDK conforms to the ordinary directory structure.
*/
@VisibleForTesting
static AndroidPlatformTarget createFromDefaultDirectoryStructure(
String name,
AndroidDirectoryResolver androidDirectoryResolver,
String platformDirectoryPath,
Set<Path> additionalJarPaths,
Optional<Path> aaptOverride,
Optional<Path> aapt2Override) {
Path androidSdkDir = androidDirectoryResolver.getSdkOrThrow();
if (!androidSdkDir.isAbsolute()) {
throw new HumanReadableException(
"Path to Android SDK must be absolute but was: %s.", androidSdkDir);
}
Path platformDirectory = androidSdkDir.resolve(platformDirectoryPath);
Path androidJar = platformDirectory.resolve("android.jar");
// Add any libraries found in the optional directory under the Android SDK directory. These
// go at the head of the bootclasspath before any additional jars.
File optionalDirectory = platformDirectory.resolve("optional").toFile();
if (optionalDirectory.exists() && optionalDirectory.isDirectory()) {
String[] optionalDirList = optionalDirectory.list(new AddonFilter());
if (optionalDirList != null) {
Arrays.sort(optionalDirList);
ImmutableSet.Builder<Path> additionalJars = ImmutableSet.builder();
for (String file : optionalDirList) {
additionalJars.add(optionalDirectory.toPath().resolve(file));
}
additionalJars.addAll(additionalJarPaths);
additionalJarPaths = additionalJars.build();
}
}
LinkedList<Path> bootclasspathEntries = Lists.newLinkedList(additionalJarPaths);
// Make sure android.jar is at the front of the bootclasspath.
bootclasspathEntries.addFirst(androidJar);
// This is the directory under the Android SDK directory that contains the dx script, jack,
// jill, and binaries.
Path buildToolsDir = androidDirectoryResolver.getBuildToolsOrThrow();
// This is the directory under the Android SDK directory that contains the aapt, aidl, and
// zipalign binaries. Before Android SDK Build-tools 23.0.0_rc1, this was the same as
// buildToolsDir above.
Path buildToolsBinDir;
if (buildToolsDir.resolve("bin").toFile().exists()) {
// Android SDK Build-tools >= 23.0.0_rc1 have executables under a new bin directory.
buildToolsBinDir = buildToolsDir.resolve("bin");
} else {
// Android SDK Build-tools < 23.0.0_rc1 have executables under the build-tools directory.
buildToolsBinDir = buildToolsDir;
}
Path zipAlignExecutable = androidSdkDir.resolve("tools/zipalign").toAbsolutePath();
if (!zipAlignExecutable.toFile().exists()) {
// Android SDK Build-tools >= 19.1.0 have zipalign under the build-tools directory.
zipAlignExecutable =
androidSdkDir.resolve(buildToolsBinDir).resolve("zipalign").toAbsolutePath();
}
Path androidFrameworkIdlFile = platformDirectory.resolve("framework.aidl");
Path proguardJar = androidSdkDir.resolve("tools/proguard/lib/proguard.jar");
Path proguardConfig = androidSdkDir.resolve("tools/proguard/proguard-android.txt");
Path optimizedProguardConfig =
androidSdkDir.resolve("tools/proguard/proguard-android-optimize.txt");
return new AndroidPlatformTarget(
name,
androidJar.toAbsolutePath(),
bootclasspathEntries,
aaptOverride.orElse(
androidSdkDir.resolve(buildToolsBinDir).resolve("aapt").toAbsolutePath()),
aapt2Override.orElse(
androidSdkDir.resolve(buildToolsBinDir).resolve("aapt2").toAbsolutePath()),
androidSdkDir.resolve("platform-tools/adb").toAbsolutePath(),
androidSdkDir.resolve(buildToolsBinDir).resolve("aidl").toAbsolutePath(),
zipAlignExecutable,
buildToolsDir
.resolve(Platform.detect() == Platform.WINDOWS ? "dx.bat" : "dx")
.toAbsolutePath(),
androidFrameworkIdlFile,
proguardJar,
proguardConfig,
optimizedProguardConfig,
androidDirectoryResolver);
}
/** Factory to build an AndroidPlatformTarget that corresponds to a given Google API level. */
private static class AndroidWithGoogleApisFactory implements Factory {
private static final String API_DIR_SUFFIX = "(?:-([0-9]+))*";
@Override
public AndroidPlatformTarget newInstance(
final AndroidDirectoryResolver androidDirectoryResolver,
final String apiLevel,
Optional<Path> aaptOverride,
Optional<Path> aapt2Override) {
// TODO(natthu): Use Paths instead of Strings everywhere in this file.
Path androidSdkDir = androidDirectoryResolver.getSdkOrThrow();
File addonsParentDir = androidSdkDir.resolve("add-ons").toFile();
String apiDirPrefix = "addon-google_apis-google-" + apiLevel;
final Pattern apiDirPattern = Pattern.compile(apiDirPrefix + API_DIR_SUFFIX);
if (addonsParentDir.isDirectory()) {
String[] addonsApiDirs =
addonsParentDir.list((dir, name1) -> apiDirPattern.matcher(name1).matches());
Arrays.sort(
addonsApiDirs,
new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return getVersion(o1) - getVersion(o2);
}
private int getVersion(String dirName) {
Matcher matcher = apiDirPattern.matcher(dirName);
Preconditions.checkState(matcher.matches());
if (matcher.group(1) != null) {
return Integer.parseInt(matcher.group(1));
}
return 0;
}
});
ImmutableSet.Builder<Path> additionalJarPaths = ImmutableSet.builder();
for (String dir : addonsApiDirs) {
File libsDir = new File(addonsParentDir, dir + "/libs");
String[] addonFiles;
if (libsDir.isDirectory()
&& (addonFiles = libsDir.list(new AddonFilter())) != null
&& addonFiles.length != 0) {
Arrays.sort(addonFiles);
for (String addonJar : addonFiles) {
additionalJarPaths.add(libsDir.toPath().resolve(addonJar));
}
return createFromDefaultDirectoryStructure(
"Google Inc.:Google APIs:" + apiLevel,
androidDirectoryResolver,
"platforms/android-" + apiLevel,
additionalJarPaths.build(),
aaptOverride,
aapt2Override);
}
}
}
throw new HumanReadableException(
"Google APIs not found in %s.\n"
+ "Please run '%s/tools/android sdk' and select both 'SDK Platform' and "
+ "'Google APIs' under Android (API %s)",
new File(addonsParentDir, apiDirPrefix + "/libs").getAbsolutePath(),
androidSdkDir,
apiLevel);
}
}
private static class AndroidWithoutGoogleApisFactory implements Factory {
@Override
public AndroidPlatformTarget newInstance(
final AndroidDirectoryResolver androidDirectoryResolver,
final String apiLevel,
Optional<Path> aaptOverride,
Optional<Path> aapt2Override) {
return createFromDefaultDirectoryStructure(
"android-" + apiLevel,
androidDirectoryResolver,
"platforms/android-" + apiLevel,
/* additionalJarPaths */ ImmutableSet.of(),
aaptOverride,
aapt2Override);
}
}
private static class AddonFilter implements FilenameFilter {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jar");
}
}
}