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("icu.locale.default", Locale.getDefault().toLanguageTag()); 79 if (Build.VERSION.SDK_INT >= O) { 80 maybeCopyFonts(extractDirectory); 81 } 82 maybeCopyIcuData(extractDirectory); 83 loadLibrary(extractDirectory); 84 }); 85 } catch (IOException e) { 86 throw new AssertionError("Unable to load Robolectric native runtime library", e); 87 } 88 } 89 90 /** Attempts to load the ICU dat file. This is only relevant for native graphics. */ maybeCopyIcuData(TempDirectory tempDirectory)91 private void maybeCopyIcuData(TempDirectory tempDirectory) throws IOException { 92 URL icuDatUrl; 93 try { 94 icuDatUrl = Resources.getResource("icu/icudt68l.dat"); 95 } catch (IllegalArgumentException e) { 96 return; 97 } 98 Path icuPath = tempDirectory.create("icu"); 99 Path icuDatPath = icuPath.resolve("icudt68l.dat"); 100 Resources.asByteSource(icuDatUrl).copyTo(Files.asByteSink(icuDatPath.toFile())); 101 System.setProperty("icu.data.path", icuDatPath.toAbsolutePath().toString()); 102 } 103 104 /** 105 * Attempts to copy the system fonts to a temporary directory. This is only relevant for native 106 * graphics. 107 */ maybeCopyFonts(TempDirectory tempDirectory)108 private void maybeCopyFonts(TempDirectory tempDirectory) throws IOException { 109 URI fontsUri = null; 110 try { 111 fontsUri = Resources.getResource("fonts/").toURI(); 112 } catch (IllegalArgumentException | URISyntaxException e) { 113 return; 114 } 115 116 FileSystem zipfs = null; 117 118 if ("jar".equals(fontsUri.getScheme())) { 119 zipfs = FileSystems.newFileSystem(fontsUri, ImmutableMap.of("create", "true")); 120 } 121 122 Path fontsInputPath = Paths.get(fontsUri); 123 Path fontsOutputPath = tempDirectory.create("fonts"); 124 125 try (Stream<Path> pathStream = java.nio.file.Files.walk(fontsInputPath)) { 126 Iterator<Path> fileIterator = pathStream.iterator(); 127 while (fileIterator.hasNext()) { 128 Path path = fileIterator.next(); 129 // Avoid copying parent directory. 130 if ("fonts".equals(path.getFileName().toString())) { 131 continue; 132 } 133 String fontPath = "fonts/" + path.getFileName(); 134 URL resource = Resources.getResource(fontPath); 135 Path outputPath = tempDirectory.getBasePath().resolve(fontPath); 136 Resources.asByteSource(resource).copyTo(Files.asByteSink(outputPath.toFile())); 137 } 138 } 139 System.setProperty( 140 "robolectric.nativeruntime.fontdir", 141 // Android's FontListParser expects a trailing slash for the base font directory. 142 fontsOutputPath.toAbsolutePath() + File.separator); 143 if (zipfs != null) { 144 zipfs.close(); 145 } 146 } 147 loadLibrary(TempDirectory tempDirectory)148 private void loadLibrary(TempDirectory tempDirectory) throws IOException { 149 String libraryName = System.mapLibraryName("robolectric-nativeruntime"); 150 Path libraryPath = tempDirectory.getBasePath().resolve(libraryName); 151 URL libraryResource = Resources.getResource(nativeLibraryPath()); 152 Resources.asByteSource(libraryResource).copyTo(Files.asByteSink(libraryPath.toFile())); 153 System.load(libraryPath.toAbsolutePath().toString()); 154 } 155 isSupported()156 private static boolean isSupported() { 157 return ("mac".equals(osName()) && ("aarch64".equals(arch()) || "x86_64".equals(arch()))) 158 || ("linux".equals(osName()) && "x86_64".equals(arch())) 159 || ("windows".equals(osName()) && "x86_64".equals(arch())); 160 } 161 nativeLibraryPath()162 private static String nativeLibraryPath() { 163 String os = osName(); 164 String arch = arch(); 165 return String.format( 166 "native/%s/%s/%s", os, arch, System.mapLibraryName("robolectric-nativeruntime")); 167 } 168 osName()169 private static String osName() { 170 String osName = OS_NAME.value().toLowerCase(Locale.US); 171 if (osName.contains("linux")) { 172 return "linux"; 173 } else if (osName.contains("mac")) { 174 return "mac"; 175 } else if (osName.contains("win")) { 176 return "windows"; 177 } 178 return "unknown"; 179 } 180 arch()181 private static String arch() { 182 String arch = OS_ARCH.value().toLowerCase(Locale.US); 183 if (arch.equals("x86_64") || arch.equals("amd64")) { 184 return "x86_64"; 185 } 186 return arch; 187 } 188 189 @VisibleForTesting isLoaded()190 static boolean isLoaded() { 191 return loaded.get(); 192 } 193 194 @VisibleForTesting getDirectory()195 Path getDirectory() { 196 return extractDirectory == null ? null : extractDirectory.getBasePath(); 197 } 198 199 @VisibleForTesting resetLoaded()200 static void resetLoaded() { 201 loaded.set(false); 202 } 203 } 204