001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.util;
006
007import com.fasterxml.jackson.core.type.TypeReference;
008import com.fasterxml.jackson.databind.ObjectMapper;
009import java.io.File;
010import java.io.IOException;
011import java.nio.file.Files;
012import java.nio.file.Paths;
013import java.util.ArrayList;
014import java.util.HashMap;
015import java.util.List;
016import java.util.Map;
017import java.util.Objects;
018
019/** Loads dynamic libraries for all platforms. */
020public final class CombinedRuntimeLoader {
021  private CombinedRuntimeLoader() {}
022
023  private static String extractionDirectory;
024
025  /**
026   * Returns library extraction directory.
027   *
028   * @return Library extraction directory.
029   */
030  public static synchronized String getExtractionDirectory() {
031    return extractionDirectory;
032  }
033
034  private static synchronized void setExtractionDirectory(String directory) {
035    extractionDirectory = directory;
036  }
037
038  private static String defaultExtractionRoot;
039
040  /**
041   * Gets the default extraction root location (~/.wpilib/nativecache) for use if
042   * setExtractionDirectory is not set.
043   *
044   * @return The default extraction root location.
045   */
046  public static synchronized String getDefaultExtractionRoot() {
047    if (defaultExtractionRoot != null) {
048      return defaultExtractionRoot;
049    }
050    String home = System.getProperty("user.home");
051    defaultExtractionRoot = Paths.get(home, ".wpilib", "nativecache").toString();
052    return defaultExtractionRoot;
053  }
054
055  /**
056   * Returns platform path.
057   *
058   * @return The current platform path.
059   * @throws IllegalStateException Thrown if the operating system is unknown.
060   */
061  public static String getPlatformPath() {
062    String filePath;
063    String arch = System.getProperty("os.arch");
064
065    boolean intel32 = "x86".equals(arch) || "i386".equals(arch);
066    boolean intel64 = "amd64".equals(arch) || "x86_64".equals(arch);
067
068    if (System.getProperty("os.name").startsWith("Windows")) {
069      if (intel32) {
070        filePath = "/windows/x86/";
071      } else {
072        filePath = "/windows/x86-64/";
073      }
074    } else if (System.getProperty("os.name").startsWith("Mac")) {
075      filePath = "/osx/universal/";
076    } else if (System.getProperty("os.name").startsWith("Linux")) {
077      if (intel32) {
078        filePath = "/linux/x86/";
079      } else if (intel64) {
080        filePath = "/linux/x86-64/";
081      } else if (new File("/usr/local/frc/bin/frcRunRobot.sh").exists()) {
082        filePath = "/linux/athena/";
083      } else if ("arm".equals(arch) || "arm32".equals(arch)) {
084        filePath = "/linux/arm32/";
085      } else if ("aarch64".equals(arch) || "arm64".equals(arch)) {
086        filePath = "/linux/arm64/";
087      } else {
088        filePath = "/linux/nativearm/";
089      }
090    } else {
091      throw new IllegalStateException();
092    }
093
094    return filePath;
095  }
096
097  private static String getLoadErrorMessage(String libraryName, UnsatisfiedLinkError ule) {
098    StringBuilder msg = new StringBuilder(512);
099    msg.append(libraryName)
100        .append(" could not be loaded from path\n" + "\tattempted to load for platform ")
101        .append(getPlatformPath())
102        .append("\nLast Load Error: \n")
103        .append(ule.getMessage())
104        .append('\n');
105    if (System.getProperty("os.name").startsWith("Windows")) {
106      msg.append(
107          "A common cause of this error is missing the C++ runtime.\n"
108              + "Download the latest at https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads\n");
109    }
110    return msg.toString();
111  }
112
113  /**
114   * Extract a list of native libraries.
115   *
116   * @param <T> The class where the resources would be located
117   * @param clazz The actual class object
118   * @param resourceName The resource name on the classpath to use for file lookup
119   * @return List of all libraries that were extracted
120   * @throws IOException Thrown if resource not found or file could not be extracted
121   */
122  @SuppressWarnings("unchecked")
123  public static <T> List<String> extractLibraries(Class<T> clazz, String resourceName)
124      throws IOException {
125    TypeReference<HashMap<String, Object>> typeRef = new TypeReference<>() {};
126    ObjectMapper mapper = new ObjectMapper();
127    Map<String, Object> map;
128    try (var stream = clazz.getResourceAsStream(resourceName)) {
129      map = mapper.readValue(stream, typeRef);
130    }
131
132    var platformPath = Paths.get(getPlatformPath());
133    var platform = platformPath.getName(0).toString();
134    var arch = platformPath.getName(1).toString();
135
136    var platformMap = (Map<String, List<String>>) map.get(platform);
137
138    var fileList = platformMap.get(arch);
139
140    var extractionPathString = getExtractionDirectory();
141
142    if (extractionPathString == null) {
143      String hash = (String) map.get("hash");
144
145      var defaultExtractionRoot = getDefaultExtractionRoot();
146      var extractionPath = Paths.get(defaultExtractionRoot, platform, arch, hash);
147      extractionPathString = extractionPath.toString();
148
149      setExtractionDirectory(extractionPathString);
150    }
151
152    List<String> extractedFiles = new ArrayList<>();
153
154    byte[] buffer = new byte[0x10000]; // 64K copy buffer
155
156    for (var file : fileList) {
157      try (var stream = clazz.getResourceAsStream(file)) {
158        Objects.requireNonNull(stream);
159
160        var outputFile = Paths.get(extractionPathString, new File(file).getName());
161        extractedFiles.add(outputFile.toString());
162        if (outputFile.toFile().exists()) {
163          continue;
164        }
165        var parent = outputFile.getParent();
166        if (parent == null) {
167          throw new IOException("Output file has no parent");
168        }
169        parent.toFile().mkdirs();
170
171        try (var os = Files.newOutputStream(outputFile)) {
172          int readBytes;
173          while ((readBytes = stream.read(buffer)) != -1) { // NOPMD
174            os.write(buffer, 0, readBytes);
175          }
176        }
177      }
178    }
179
180    return extractedFiles;
181  }
182
183  /**
184   * Load a single library from a list of extracted files.
185   *
186   * @param libraryName The library name to load
187   * @param extractedFiles The extracted files to search
188   * @throws IOException If library was not found
189   */
190  public static void loadLibrary(String libraryName, List<String> extractedFiles)
191      throws IOException {
192    String currentPath = null;
193    try {
194      for (var extractedFile : extractedFiles) {
195        if (extractedFile.contains(libraryName)) {
196          // Load it
197          currentPath = extractedFile;
198          System.load(extractedFile);
199          return;
200        }
201      }
202      throw new IOException("Could not find library " + libraryName);
203    } catch (UnsatisfiedLinkError ule) {
204      throw new IOException(getLoadErrorMessage(currentPath, ule));
205    }
206  }
207
208  /**
209   * Load a list of native libraries out of a single directory.
210   *
211   * @param <T> The class where the resources would be located
212   * @param clazz The actual class object
213   * @param librariesToLoad List of libraries to load
214   * @throws IOException Throws an IOException if not found
215   */
216  public static <T> void loadLibraries(Class<T> clazz, String... librariesToLoad)
217      throws IOException {
218    // Extract everything
219
220    var extractedFiles = extractLibraries(clazz, "/ResourceInformation.json");
221
222    for (var library : librariesToLoad) {
223      loadLibrary(library, extractedFiles);
224    }
225  }
226}