1 package org.robolectric.nativeruntime; 2 3 import static android.os.Build.VERSION_CODES.O; 4 import static com.google.common.base.StandardSystemProperty.OS_ARCH; 5 import static com.google.common.base.StandardSystemProperty.OS_NAME; 6 7 import android.database.CursorWindow; 8 import android.os.Build; 9 import com.google.auto.service.AutoService; 10 import com.google.common.annotations.VisibleForTesting; 11 import com.google.common.collect.ImmutableMap; 12 import com.google.common.io.Files; 13 import com.google.common.io.Resources; 14 import java.io.File; 15 import java.io.IOException; 16 import java.net.URI; 17 import java.net.URISyntaxException; 18 import java.net.URL; 19 import java.nio.file.FileSystem; 20 import java.nio.file.FileSystems; 21 import java.nio.file.Path; 22 import java.nio.file.Paths; 23 import java.util.Iterator; 24 import java.util.Locale; 25 import java.util.concurrent.atomic.AtomicBoolean; 26 import java.util.concurrent.atomic.AtomicReference; 27 import java.util.stream.Stream; 28 import javax.annotation.Priority; 29 import org.robolectric.pluginapi.NativeRuntimeLoader; 30 import org.robolectric.util.PerfStatsCollector; 31 import org.robolectric.util.TempDirectory; 32 import org.robolectric.util.inject.Injector; 33 34 /** Loads the Robolectric native runtime. */ 35 @AutoService(NativeRuntimeLoader.class) 36 @Priority(Integer.MIN_VALUE) 37 public class DefaultNativeRuntimeLoader implements NativeRuntimeLoader { 38 protected static final AtomicBoolean loaded = new AtomicBoolean(false); 39 40 private static final AtomicReference<NativeRuntimeLoader> nativeRuntimeLoader = 41 new AtomicReference<>(); 42 43 private TempDirectory extractDirectory; 44 injectAndLoad()45 public static void injectAndLoad() { 46 // Ensure a single instance. 47 synchronized (nativeRuntimeLoader) { 48 if (nativeRuntimeLoader.get() == null) { 49 Injector injector = new Injector.Builder(CursorWindow.class.getClassLoader()).build(); 50 NativeRuntimeLoader loader = injector.getInstance(NativeRuntimeLoader.class); 51 nativeRuntimeLoader.set(loader); 52 } 53 } 54 nativeRuntimeLoader.get().ensureLoaded(); 55 } 56 57 @Override ensureLoaded()58 public synchronized void ensureLoaded() { 59 if (loaded.get()) { 60 return; 61 } 62 63 if (!isSupported()) { 64 String errorMessage = 65 String.format( 66 "The Robolectric native runtime is not supported on %s (%s)", 67 OS_NAME.value(), OS_ARCH.value()); 68 throw new AssertionError(errorMessage); 69 } 70 loaded.set(true); 71 72 try { 73 PerfStatsCollector.getInstance() 74 .measure( 75 "loadNativeRuntime", 76 () -> { 77 extractDirectory = new TempDirectory("nativeruntime"); 78 System.setProperty( 79 "robolectric.nativeruntime.languageTag", Locale.getDefault().toLanguageTag()); 80 if (Build.VERSION.SDK_INT >= O) { 81 maybeCopyFonts(extractDirectory); 82 } 83 maybeCopyIcuData(extractDirectory); 84 loadLibrary(extractDirectory); 85 }); 86 } catch (IOException e) { 87 throw new AssertionError("Unable to load Robolectric native runtime library", e); 88 } 89 } 90 91 /** Attempts to load the ICU dat file. This is only relevant for native graphics. */ maybeCopyIcuData(TempDirectory tempDirectory)92 private void maybeCopyIcuData(TempDirectory tempDirectory) throws IOException { 93 URL icuDatUrl; 94 try { 95 icuDatUrl = Resources.getResource("icu/icudt68l.dat"); 96 } catch (IllegalArgumentException e) { 97 return; 98 } 99 Path icuPath = tempDirectory.create("icu"); 100 Path icuDatPath = tempDirectory.getBasePath().resolve("icu/icudt68l.dat"); 101 Resources.asByteSource(icuDatUrl).copyTo(Files.asByteSink(icuDatPath.toFile())); 102 System.setProperty("icu.dir", icuPath.toAbsolutePath().toString()); 103 } 104 105 /** 106 * Attempts to copy the system fonts to a temporary directory. This is only relevant for native 107 * graphics. 108 */ maybeCopyFonts(TempDirectory tempDirectory)109 private void maybeCopyFonts(TempDirectory tempDirectory) throws IOException { 110 URI fontsUri = null; 111 try { 112 fontsUri = Resources.getResource("fonts/").toURI(); 113 } catch (IllegalArgumentException | URISyntaxException e) { 114 return; 115 } 116 117 FileSystem zipfs = null; 118 119 if ("jar".equals(fontsUri.getScheme())) { 120 zipfs = FileSystems.newFileSystem(fontsUri, ImmutableMap.of("create", "true")); 121 } 122 123 Path fontsInputPath = Paths.get(fontsUri); 124 Path fontsOutputPath = tempDirectory.create("fonts"); 125 126 try (Stream<Path> pathStream = java.nio.file.Files.walk(fontsInputPath)) { 127 Iterator<Path> fileIterator = pathStream.iterator(); 128 while (fileIterator.hasNext()) { 129 Path path = fileIterator.next(); 130 // Avoid copying parent directory. 131 if ("fonts".equals(path.getFileName().toString())) { 132 continue; 133 } 134 String fontPath = "fonts/" + path.getFileName(); 135 URL resource = Resources.getResource(fontPath); 136 Path outputPath = tempDirectory.getBasePath().resolve(fontPath); 137 Resources.asByteSource(resource).copyTo(Files.asByteSink(outputPath.toFile())); 138 } 139 } 140 System.setProperty( 141 "robolectric.nativeruntime.fontdir", 142 // Android's FontListParser expects a trailing slash for the base font directory. 143 fontsOutputPath.toAbsolutePath() + File.separator); 144 if (zipfs != null) { 145 zipfs.close(); 146 } 147 } 148 loadLibrary(TempDirectory tempDirectory)149 private void loadLibrary(TempDirectory tempDirectory) throws IOException { 150 String libraryName = System.mapLibraryName("robolectric-nativeruntime"); 151 System.setProperty( 152 "robolectric.nativeruntime.languageTag", Locale.getDefault().toLanguageTag()); 153 Path libraryPath = tempDirectory.getBasePath().resolve(libraryName); 154 URL libraryResource = Resources.getResource(nativeLibraryPath()); 155 Resources.asByteSource(libraryResource).copyTo(Files.asByteSink(libraryPath.toFile())); 156 System.load(libraryPath.toAbsolutePath().toString()); 157 } 158 isSupported()159 private static boolean isSupported() { 160 return ("mac".equals(osName()) && ("aarch64".equals(arch()) || "x86_64".equals(arch()))) 161 || ("linux".equals(osName()) && "x86_64".equals(arch())) 162 || ("windows".equals(osName()) && "x86_64".equals(arch())); 163 } 164 nativeLibraryPath()165 private static String nativeLibraryPath() { 166 String os = osName(); 167 String arch = arch(); 168 return String.format( 169 "native/%s/%s/%s", os, arch, System.mapLibraryName("robolectric-nativeruntime")); 170 } 171 osName()172 private static String osName() { 173 String osName = OS_NAME.value().toLowerCase(Locale.US); 174 if (osName.contains("linux")) { 175 return "linux"; 176 } else if (osName.contains("mac")) { 177 return "mac"; 178 } else if (osName.contains("win")) { 179 return "windows"; 180 } 181 return "unknown"; 182 } 183 arch()184 private static String arch() { 185 String arch = OS_ARCH.value().toLowerCase(Locale.US); 186 if (arch.equals("x86_64") || arch.equals("amd64")) { 187 return "x86_64"; 188 } 189 return arch; 190 } 191 192 @VisibleForTesting isLoaded()193 static boolean isLoaded() { 194 return loaded.get(); 195 } 196 197 @VisibleForTesting getDirectory()198 Path getDirectory() { 199 return extractDirectory == null ? null : extractDirectory.getBasePath(); 200 } 201 202 @VisibleForTesting resetLoaded()203 static void resetLoaded() { 204 loaded.set(false); 205 } 206 } 207