• 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(
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