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}