diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7381c4b..a53d6cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,17 @@ name: CI on: [push, pull_request] jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + java-version: 17 + distribution: 'temurin' + - name: Check for changes + run: ./gradlew spotlessCheck + build: strategy: matrix: diff --git a/src/main/java/org/photonvision/tools/HashNativeResources.java b/src/main/java/org/photonvision/tools/HashNativeResources.java index 54facb4..110f0e9 100644 --- a/src/main/java/org/photonvision/tools/HashNativeResources.java +++ b/src/main/java/org/photonvision/tools/HashNativeResources.java @@ -1,20 +1,22 @@ package org.photonvision.tools; import com.google.gson.GsonBuilder; +import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; +import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; import java.util.HashMap; +import java.util.HexFormat; import java.util.List; import java.util.Map; import javax.inject.Inject; -import org.codehaus.groovy.runtime.EncodingGroovyMethods; import org.gradle.api.DefaultTask; import org.gradle.api.file.Directory; import org.gradle.api.file.DirectoryProperty; @@ -26,6 +28,28 @@ import org.gradle.api.tasks.TaskAction; public class HashNativeResources extends DefaultTask { + + /** + * Architecture-specific information containing file hashes for a specific CPU architecture (e.g., + * x86-64, arm64). + */ + public record ArchInfo(Map fileHashes) {} + + /** + * Platform-specific information containing architectures for a specific OS platform (e.g., linux, + * windows). + */ + public record PlatformInfo(Map architectures) {} + + /** Overall resource information to be serialized */ + public record ResourceInformation( + // Combined MD5 hash of all native resource files + String hash, + // Platform-specific native libraries organized by platform then architecture + Map platforms, + // List of supported versions for these native resources + List versions) {} + private final DirectoryProperty inputDirectory; private final RegularFileProperty hashFile; private final RegularFileProperty versionsInput; @@ -55,16 +79,13 @@ public HashNativeResources() { @TaskAction public void execute() throws NoSuchAlgorithmException, IOException { - MessageDigest hash = MessageDigest.getInstance("MD5"); + MessageDigest combinedHash = MessageDigest.getInstance("MD5"); Directory directory = inputDirectory.get(); Path inputPath = directory.getAsFile().toPath(); - Map platforms = new HashMap<>(); - - byte[] buffer = new byte[0xFFFF]; - int readBytes = 0; + Map platforms = new HashMap<>(); for (File file : directory.getAsFileTree()) { if (!file.isFile()) { @@ -73,44 +94,49 @@ public void execute() throws NoSuchAlgorithmException, IOException { Path path = inputPath.relativize(file.toPath()); - try (FileInputStream is = new FileInputStream(file)) { - while ((readBytes = is.read(buffer)) != -1) { - hash.update(buffer, 0, readBytes); - } + // Compute individual file hash + MessageDigest fileHash = MessageDigest.getInstance("MD5"); + try (var dis = + new DigestInputStream( + new DigestInputStream(new BufferedInputStream(new FileInputStream(file)), fileHash), + combinedHash)) { + dis.transferTo(OutputStream.nullOutputStream()); } String platform = path.getName(0).toString(); String arch = path.getName(1).toString(); String strPath = "/" + path.toString().replace("\\", "/"); - - @SuppressWarnings("unchecked") // This will always be the correct type - Map> platformMap = (Map>) platforms.get(platform); - if (platformMap == null) { - platformMap = new HashMap<>(); - List archFiles = new ArrayList<>(); - archFiles.add(strPath); - platformMap.put(arch, archFiles); - platforms.put(platform, platformMap); + String hexFileHash = HexFormat.of().formatHex(fileHash.digest()); + + PlatformInfo platformInfo = platforms.get(platform); + if (platformInfo == null) { + Map fileHashes = new HashMap<>(); + fileHashes.put(strPath, hexFileHash); + Map architectures = new HashMap<>(); + architectures.put(arch, new ArchInfo(fileHashes)); + platforms.put(platform, new PlatformInfo(architectures)); } else { - List archFiles = platformMap.get(arch); - if (archFiles == null) { - archFiles = new ArrayList<>(); - archFiles.add(strPath); - platformMap.put(arch, archFiles); + Map architectures = platformInfo.architectures(); + ArchInfo archInfo = architectures.get(arch); + if (archInfo == null) { + Map fileHashes = new HashMap<>(); + fileHashes.put(strPath, hexFileHash); + architectures.put(arch, new ArchInfo(fileHashes)); } else { - archFiles.add(strPath); + archInfo.fileHashes().put(strPath, hexFileHash); } } } - var versions = Files.readAllLines(versionsInput.get().getAsFile().toPath()); + String hash = HexFormat.of().formatHex(combinedHash.digest()); + + List versions = Files.readAllLines(versionsInput.get().getAsFile().toPath()); + ResourceInformation output = new ResourceInformation(hash, platforms, versions); - platforms.put("hash", EncodingGroovyMethods.encodeHex(hash.digest()).toString()); - platforms.put("versions", versions); GsonBuilder builder = new GsonBuilder(); builder.setPrettyPrinting(); - var json = builder.create().toJson(platforms); + var json = builder.create().toJson(output); Files.writeString(hashFile.get().getAsFile().toPath(), json, Charset.defaultCharset()); } }