Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
86 changes: 56 additions & 30 deletions src/main/java/org/photonvision/tools/HashNativeResources.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<String, String> fileHashes) {}

/**
* Platform-specific information containing architectures for a specific OS platform (e.g., linux,
* windows).
*/
public record PlatformInfo(Map<String, ArchInfo> 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<String, PlatformInfo> platforms,
// List of supported versions for these native resources
List<String> versions) {}

private final DirectoryProperty inputDirectory;
private final RegularFileProperty hashFile;
private final RegularFileProperty versionsInput;
Expand Down Expand Up @@ -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<String, Object> platforms = new HashMap<>();

byte[] buffer = new byte[0xFFFF];
int readBytes = 0;
Map<String, PlatformInfo> platforms = new HashMap<>();

for (File file : directory.getAsFileTree()) {
if (!file.isFile()) {
Expand All @@ -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<String, List<String>> platformMap = (Map<String, List<String>>) platforms.get(platform);
if (platformMap == null) {
platformMap = new HashMap<>();
List<String> 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<String, String> fileHashes = new HashMap<>();
fileHashes.put(strPath, hexFileHash);
Map<String, ArchInfo> architectures = new HashMap<>();
architectures.put(arch, new ArchInfo(fileHashes));
platforms.put(platform, new PlatformInfo(architectures));
} else {
List<String> archFiles = platformMap.get(arch);
if (archFiles == null) {
archFiles = new ArrayList<>();
archFiles.add(strPath);
platformMap.put(arch, archFiles);
Map<String, ArchInfo> architectures = platformInfo.architectures();
ArchInfo archInfo = architectures.get(arch);
if (archInfo == null) {
Map<String, String> 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<String> 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());
}
}