001/* 002 * Copyright (c) FIRST and other WPILib contributors. 003 * Open Source Software; you can modify and/or share it under the terms of 004 * the WPILib BSD license below: 005 * 006 * Redistribution and use in source and binary forms, with or without 007 * modification, are permitted provided that the following conditions 008 * are met: 009 * Redistributions of source code must retain the above copyright notice, 010 * this list of conditions and the following disclaimer. 011 * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * Neither the name of FIRST, WPILib, nor the names of other WPILib 015 * contributors may be used to endorse or promote products derived from 016 * this software without specific prior written permission. 017 */ 018 019package org.photonvision.jni; 020 021import com.fasterxml.jackson.databind.ObjectMapper; 022import java.io.BufferedInputStream; 023import java.io.File; 024import java.io.FileInputStream; 025import java.io.IOException; 026import java.nio.file.Files; 027import java.nio.file.Paths; 028import java.nio.file.StandardCopyOption; 029import java.security.DigestInputStream; 030import java.security.MessageDigest; 031import java.security.NoSuchAlgorithmException; 032import java.util.ArrayList; 033import java.util.HexFormat; 034import java.util.List; 035import java.util.Map; 036import java.util.Objects; 037import java.util.concurrent.CopyOnWriteArrayList; 038 039/** Loads dynamic libraries for all platforms. */ 040public final class CombinedRuntimeLoader { 041 private CombinedRuntimeLoader() {} 042 043 private static String extractionDirectory; 044 045 private static final Object extractCompleteLock = new Object(); 046 private static boolean extractAndVerifyComplete = false; 047 private static List<String> filesAlreadyExtracted = new CopyOnWriteArrayList<>(); 048 049 /** 050 * Returns library extraction directory. 051 * 052 * @return Library extraction directory. 053 */ 054 public static synchronized String getExtractionDirectory() { 055 return extractionDirectory; 056 } 057 058 private static synchronized void setExtractionDirectory(String directory) { 059 extractionDirectory = directory; 060 } 061 062 private static String defaultExtractionRoot; 063 064 /** 065 * Gets the default extraction root location (~/.wpilib/nativecache) for use if 066 * setExtractionDirectory is not set. 067 * 068 * @return The default extraction root location. 069 */ 070 public static synchronized String getDefaultExtractionRoot() { 071 if (defaultExtractionRoot != null) { 072 return defaultExtractionRoot; 073 } 074 String home = System.getProperty("user.home"); 075 defaultExtractionRoot = Paths.get(home, ".wpilib", "nativecache").toString(); 076 return defaultExtractionRoot; 077 } 078 079 /** 080 * Returns platform path. 081 * 082 * @return The current platform path. 083 * @throws IllegalStateException Thrown if the operating system is unknown. 084 */ 085 public static String getPlatformPath() { 086 String filePath; 087 String arch = System.getProperty("os.arch"); 088 089 boolean intel32 = "x86".equals(arch) || "i386".equals(arch); 090 boolean intel64 = "amd64".equals(arch) || "x86_64".equals(arch); 091 092 if (System.getProperty("os.name").startsWith("Windows")) { 093 if (intel32) { 094 filePath = "/windows/x86/"; 095 } else { 096 filePath = "/windows/x86-64/"; 097 } 098 } else if (System.getProperty("os.name").startsWith("Mac")) { 099 filePath = "/osx/universal/"; 100 } else if (System.getProperty("os.name").startsWith("Linux")) { 101 if (intel32) { 102 filePath = "/linux/x86/"; 103 } else if (intel64) { 104 filePath = "/linux/x86-64/"; 105 } else if (new File("/usr/local/frc/bin/frcRunRobot.sh").exists()) { 106 filePath = "/linux/athena/"; 107 } else if ("arm".equals(arch) || "arm32".equals(arch)) { 108 filePath = "/linux/arm32/"; 109 } else if ("aarch64".equals(arch) || "arm64".equals(arch)) { 110 filePath = "/linux/arm64/"; 111 } else { 112 filePath = "/linux/nativearm/"; 113 } 114 } else { 115 throw new IllegalStateException(); 116 } 117 118 return filePath; 119 } 120 121 private static String getLoadErrorMessage(String libraryName, UnsatisfiedLinkError ule) { 122 StringBuilder msg = new StringBuilder(512); 123 msg.append(libraryName) 124 .append(" could not be loaded from path\n" + "\tattempted to load for platform ") 125 .append(getPlatformPath()) 126 .append("\nLast Load Error: \n") 127 .append(ule.getMessage()) 128 .append('\n'); 129 if (System.getProperty("os.name").startsWith("Windows")) { 130 msg.append( 131 "A common cause of this error is missing the C++ runtime.\n" 132 + "Download the latest at https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads\n"); 133 } 134 return msg.toString(); 135 } 136 137 /** 138 * Architecture-specific information containing file hashes for a specific CPU architecture (e.g., 139 * x86-64, arm64). 140 */ 141 public record ArchInfo(Map<String, String> fileHashes) {} 142 143 /** 144 * Platform-specific information containing architectures for a specific OS platform (e.g., linux, 145 * windows). 146 */ 147 public record PlatformInfo(Map<String, ArchInfo> architectures) {} 148 149 /** Overall resource information to be serialized */ 150 public record ResourceInformation( 151 // Combined MD5 hash of all native resource files 152 String hash, 153 // Platform-specific native libraries organized by platform then architecture 154 Map<String, PlatformInfo> platforms, 155 // List of supported versions for these native resources 156 List<String> versions) {} 157 158 /** 159 * Extract a list of native libraries. 160 * 161 * @param <T> The class where the resources would be located 162 * @param clazz The actual class object 163 * @param resourceName The resource name on the classpath to use for file lookup 164 * @return List of all libraries that were extracted 165 * @throws IOException Thrown if resource not found or file could not be extracted 166 */ 167 public static <T> List<String> extractLibraries(Class<T> clazz, String resourceName) 168 throws IOException { 169 ObjectMapper mapper = new ObjectMapper(); 170 ResourceInformation resourceInfo; 171 try (var stream = clazz.getResourceAsStream(resourceName)) { 172 resourceInfo = mapper.readValue(stream, ResourceInformation.class); 173 } 174 175 var platformPath = Paths.get(getPlatformPath()); 176 var platform = platformPath.getName(0).toString(); 177 var arch = platformPath.getName(1).toString(); 178 179 var platformInfo = resourceInfo.platforms().get(platform); 180 if (platformInfo == null) { 181 throw new IOException("Platform " + platform + " not found in resource information"); 182 } 183 184 var archInfo = platformInfo.architectures().get(arch); 185 if (archInfo == null) { 186 throw new IOException("Architecture " + arch + " not found for platform " + platform); 187 } 188 189 // Map of <file to extract> to <hash we loaded from the JSON> 190 Map<String, String> filenameToHash = archInfo.fileHashes(); 191 192 var extractionPathString = getExtractionDirectory(); 193 194 if (extractionPathString == null) { 195 // Folder to extract to derived from overall hash 196 String combinedHash = resourceInfo.hash(); 197 198 var defaultExtractionRoot = getDefaultExtractionRoot(); 199 var extractionPath = Paths.get(defaultExtractionRoot, platform, arch, combinedHash); 200 extractionPathString = extractionPath.toString(); 201 202 setExtractionDirectory(extractionPathString); 203 } 204 205 List<String> extractedFiles = new ArrayList<>(); 206 207 for (String file : filenameToHash.keySet()) { 208 try (var stream = clazz.getResourceAsStream(file)) { 209 Objects.requireNonNull(stream); 210 211 var outputFile = Paths.get(extractionPathString, new File(file).getName()); 212 213 String fileHash = filenameToHash.get(file); 214 215 extractedFiles.add(outputFile.toString()); 216 if (outputFile.toFile().exists()) { 217 if (hashEm(outputFile.toFile()).equals(fileHash)) { 218 continue; 219 } else { 220 // Hashes don't match, delete and re-extract 221 System.err.println( 222 outputFile.toAbsolutePath().toString() 223 + " failed validation - deleting and re-extracting"); 224 outputFile.toFile().delete(); 225 } 226 } 227 var parent = outputFile.getParent(); 228 if (parent == null) { 229 throw new IOException("Output file has no parent"); 230 } 231 parent.toFile().mkdirs(); 232 233 try (var os = Files.newOutputStream(outputFile)) { 234 Files.copy(stream, outputFile, StandardCopyOption.REPLACE_EXISTING); 235 } 236 237 if (!hashEm(outputFile.toFile()).equals(fileHash)) { 238 throw new IOException("Hash of extracted file does not match expected hash"); 239 } 240 } 241 } 242 243 return extractedFiles; 244 } 245 246 private static String hashEm(File f) throws IOException { 247 try { 248 MessageDigest fileHash = MessageDigest.getInstance("MD5"); 249 try (var dis = 250 new DigestInputStream(new BufferedInputStream(new FileInputStream(f)), fileHash)) { 251 dis.readAllBytes(); 252 } 253 var ret = HexFormat.of().formatHex(fileHash.digest()); 254 return ret; 255 } catch (NoSuchAlgorithmException e) { 256 throw new IOException("Unable to verify extracted native files", e); 257 } 258 } 259 260 /** 261 * Load a single library from a list of extracted files. 262 * 263 * @param libraryName The library name to load 264 * @param extractedFiles The extracted files to search 265 * @throws IOException If library was not found 266 */ 267 public static void loadLibrary(String libraryName, List<String> extractedFiles) 268 throws IOException { 269 String currentPath = null; 270 try { 271 for (var extractedFile : extractedFiles) { 272 if (extractedFile.contains(libraryName)) { 273 // Load it 274 currentPath = extractedFile; 275 System.load(extractedFile); 276 return; 277 } 278 } 279 throw new IOException("Could not find library " + libraryName); 280 } catch (UnsatisfiedLinkError ule) { 281 throw new IOException(getLoadErrorMessage(currentPath, ule)); 282 } 283 } 284 285 /** 286 * Load a list of native libraries out of a single directory. 287 * 288 * @param <T> The class where the resources would be located 289 * @param clazz The actual class object 290 * @param librariesToLoad List of libraries to load 291 * @throws IOException Throws an IOException if not found 292 */ 293 public static <T> void loadLibraries(Class<T> clazz, String... librariesToLoad) 294 throws IOException { 295 synchronized (extractCompleteLock) { 296 if (extractAndVerifyComplete == false) { 297 // Extract everything 298 filesAlreadyExtracted = extractLibraries(clazz, "/ResourceInformation.json"); 299 extractAndVerifyComplete = true; 300 } 301 302 for (var library : librariesToLoad) { 303 loadLibrary(library, filesAlreadyExtracted); 304 } 305 } 306 } 307}