• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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