blob: 5221a4b8ad74cf8ea808159d6c885cb3ec9cda29 [file] [log] [blame]
package org.robolectric.nativeruntime;
import static android.os.Build.VERSION_CODES.O;
import static com.google.common.base.StandardSystemProperty.OS_ARCH;
import static com.google.common.base.StandardSystemProperty.OS_NAME;
import android.database.CursorWindow;
import android.os.Build;
import com.google.auto.service.AutoService;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import javax.annotation.Priority;
import org.robolectric.pluginapi.NativeRuntimeLoader;
import org.robolectric.util.PerfStatsCollector;
import org.robolectric.util.TempDirectory;
import org.robolectric.util.inject.Injector;
/** Loads the Robolectric native runtime. */
@AutoService(NativeRuntimeLoader.class)
@Priority(Integer.MIN_VALUE)
public class DefaultNativeRuntimeLoader implements NativeRuntimeLoader {
protected static final AtomicBoolean loaded = new AtomicBoolean(false);
private static final AtomicReference<NativeRuntimeLoader> nativeRuntimeLoader =
new AtomicReference<>();
private TempDirectory extractDirectory;
public static void injectAndLoad() {
// Ensure a single instance.
synchronized (nativeRuntimeLoader) {
if (nativeRuntimeLoader.get() == null) {
Injector injector = new Injector.Builder(CursorWindow.class.getClassLoader()).build();
NativeRuntimeLoader loader = injector.getInstance(NativeRuntimeLoader.class);
nativeRuntimeLoader.set(loader);
}
}
nativeRuntimeLoader.get().ensureLoaded();
}
@Override
public synchronized void ensureLoaded() {
if (loaded.get()) {
return;
}
if (!isSupported()) {
String errorMessage =
String.format(
"The Robolectric native runtime is not supported on %s (%s)",
OS_NAME.value(), OS_ARCH.value());
throw new AssertionError(errorMessage);
}
loaded.set(true);
try {
PerfStatsCollector.getInstance()
.measure(
"loadNativeRuntime",
() -> {
extractDirectory = new TempDirectory("nativeruntime");
System.setProperty(
"robolectric.nativeruntime.languageTag", Locale.getDefault().toLanguageTag());
if (Build.VERSION.SDK_INT >= O) {
maybeCopyFonts(extractDirectory);
}
maybeCopyIcuData(extractDirectory);
loadLibrary(extractDirectory);
});
} catch (IOException e) {
throw new AssertionError("Unable to load Robolectric native runtime library", e);
}
}
/** Attempts to load the ICU dat file. This is only relevant for native graphics. */
private void maybeCopyIcuData(TempDirectory tempDirectory) throws IOException {
URL icuDatUrl;
try {
icuDatUrl = Resources.getResource("icu/icudt68l.dat");
} catch (IllegalArgumentException e) {
return;
}
Path icuPath = tempDirectory.create("icu");
Path icuDatPath = tempDirectory.getBasePath().resolve("icu/icudt68l.dat");
Resources.asByteSource(icuDatUrl).copyTo(Files.asByteSink(icuDatPath.toFile()));
System.setProperty("icu.dir", icuPath.toAbsolutePath().toString());
}
/**
* Attempts to copy the system fonts to a temporary directory. This is only relevant for native
* graphics.
*/
private void maybeCopyFonts(TempDirectory tempDirectory) throws IOException {
URI fontsUri = null;
try {
fontsUri = Resources.getResource("fonts/").toURI();
} catch (IllegalArgumentException | URISyntaxException e) {
return;
}
FileSystem zipfs = null;
if ("jar".equals(fontsUri.getScheme())) {
zipfs = FileSystems.newFileSystem(fontsUri, ImmutableMap.of("create", "true"));
}
Path fontsInputPath = Paths.get(fontsUri);
Path fontsOutputPath = tempDirectory.create("fonts");
try (Stream<Path> pathStream = java.nio.file.Files.walk(fontsInputPath)) {
Iterator<Path> fileIterator = pathStream.iterator();
while (fileIterator.hasNext()) {
Path path = fileIterator.next();
// Avoid copying parent directory.
if ("fonts".equals(path.getFileName().toString())) {
continue;
}
String fontPath = "fonts/" + path.getFileName();
URL resource = Resources.getResource(fontPath);
Path outputPath = tempDirectory.getBasePath().resolve(fontPath);
Resources.asByteSource(resource).copyTo(Files.asByteSink(outputPath.toFile()));
}
}
System.setProperty(
"robolectric.nativeruntime.fontdir", fontsOutputPath.toAbsolutePath().toString());
if (zipfs != null) {
zipfs.close();
}
}
private void loadLibrary(TempDirectory tempDirectory) throws IOException {
String libraryName = System.mapLibraryName("robolectric-nativeruntime");
System.setProperty(
"robolectric.nativeruntime.languageTag", Locale.getDefault().toLanguageTag());
Path libraryPath = tempDirectory.getBasePath().resolve(libraryName);
URL libraryResource = Resources.getResource(nativeLibraryPath());
Resources.asByteSource(libraryResource).copyTo(Files.asByteSink(libraryPath.toFile()));
System.load(libraryPath.toAbsolutePath().toString());
}
private static boolean isSupported() {
return ("mac".equals(osName()) && ("aarch64".equals(arch()) || "x86_64".equals(arch())))
|| ("linux".equals(osName()) && "x86_64".equals(arch()))
|| ("windows".equals(osName()) && "x86_64".equals(arch()));
}
private static String nativeLibraryPath() {
String os = osName();
String arch = arch();
return String.format(
"native/%s/%s/%s", os, arch, System.mapLibraryName("robolectric-nativeruntime"));
}
private static String osName() {
String osName = OS_NAME.value().toLowerCase(Locale.US);
if (osName.contains("linux")) {
return "linux";
} else if (osName.contains("mac")) {
return "mac";
} else if (osName.contains("win")) {
return "windows";
}
return "unknown";
}
private static String arch() {
String arch = OS_ARCH.value().toLowerCase(Locale.US);
if (arch.equals("x86_64") || arch.equals("amd64")) {
return "x86_64";
}
return arch;
}
@VisibleForTesting
static boolean isLoaded() {
return loaded.get();
}
@VisibleForTesting
Path getDirectory() {
return extractDirectory == null ? null : extractDirectory.getBasePath();
}
@VisibleForTesting
static void resetLoaded() {
loaded.set(false);
}
}