diff --git a/.github/workflows/test-native-compression.yml b/.github/workflows/test-native-compression.yml new file mode 100644 index 0000000000..cbf6fa6167 --- /dev/null +++ b/.github/workflows/test-native-compression.yml @@ -0,0 +1,44 @@ +on: + workflow_dispatch: + pull_request: + paths: + - 'native-compression/libaec-jna/**' + - 'native-compression/libaec-native/**' + - 'native-compression/build.gradle' + +jobs: + tests: + strategy: + matrix: + os: [ + ubuntu-24.04, + ubuntu-24.04-arm, + windows-2022, + macos-14, + macos-13 + ] + name: netCDF-Java Native Compression Tests + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '11' + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Run libaec JNA tests + run: ./gradlew clean :native-compression:libaec-jna:test + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: NativeCompression_JUnit_Results_${{ github.sha }}_-${{ matrix.os }} + path: native-compression/libaec-jna/build/reports/tests diff --git a/.gitignore b/.gitignore index dc0f8d01b7..6ed39aaaae 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ thredds.tgz # dependency check NVD files project-files/owasp-dependency-check/nvd + +# native binary distribution files +project-files/native diff --git a/native-compression/build.gradle b/native-compression/build.gradle new file mode 100644 index 0000000000..dc2450ee83 --- /dev/null +++ b/native-compression/build.gradle @@ -0,0 +1,2 @@ +description = 'Native binding for compression' +ext.title = 'native compression binding' diff --git a/native-compression/libaec-jna/build.gradle b/native-compression/libaec-jna/build.gradle new file mode 100644 index 0000000000..4d28f8dc43 --- /dev/null +++ b/native-compression/libaec-jna/build.gradle @@ -0,0 +1,20 @@ +apply from: "$rootDir/gradle/any/dependencies.gradle" +apply from: "$rootDir/gradle/any/java-library.gradle" + +description = 'Java bindings for decoding libaec compression using JNA' +ext.title = 'libaec compression decoder using JNA' + +dependencies { + api enforcedPlatform(project(':netcdf-java-platform')) + testImplementation enforcedPlatform(project(':netcdf-java-testing-platform')) + + api 'net.java.dev.jna:jna' + implementation 'org.slf4j:slf4j-api' + + testImplementation project(':cdm-test-utils') + + testImplementation 'com.google.truth:truth' + + testRuntimeOnly project(':native-compression:libaec-native') + testRuntimeOnly 'ch.qos.logback:logback-classic' +} diff --git a/native-compression/libaec-jna/src/main/java/edu/ucar/unidata/compression/jna/libaec/LibAec.java b/native-compression/libaec-jna/src/main/java/edu/ucar/unidata/compression/jna/libaec/LibAec.java new file mode 100644 index 0000000000..09a1196d52 --- /dev/null +++ b/native-compression/libaec-jna/src/main/java/edu/ucar/unidata/compression/jna/libaec/LibAec.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2025 University Corporation for Atmospheric Research/Unidata + * See LICENSE for license information. + */ + +package edu.ucar.unidata.compression.jna.libaec; + +import com.sun.jna.Memory; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; +import com.sun.jna.ptr.PointerByReference; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +/** + * JNA access to libaec. Not a full implementation, just the functions + * actually used for decoding (and testing). This is a transliteration + * of the libaec library file include/libaec.h. + * + * @author sarms + * @since 5.7.1 + */ + +public final class LibAec { + + private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LibAec.class); + private static final String libName = "aec"; + + static { + try { + File library = Native.extractFromResourcePath(libName); + Native.register(library.getAbsolutePath()); + log.debug("Using libaec library from libaec-native.jar"); + } catch (IOException e) { + try { + Native.register(libName); + log.debug("Using libaec library from system"); + } catch (UnsatisfiedLinkError ule) { + String message = + "libaec C library not present. To read this data, include the libaec-native jar in your classpath " + + "(edu.ucar:libaec-native) or install libaec on your system."; + log.error(message); + throw new RuntimeException(message, ule); + } + } + } + + public static class AecStream extends Structure { + public static AecStream create(int bitsPerSample, int blockSize, int rsi, int flags) { + AecStream aecStream = new AecStream(); + aecStream.bits_per_sample = bitsPerSample; + aecStream.block_size = blockSize; + aecStream.rsi = rsi; + aecStream.flags = flags; + + return aecStream; + } + + public void setInputMemory(Memory inputMemory) { + this.next_in = inputMemory; + this.avail_in = new SizeT(inputMemory.size()); + } + + public void setOutputMemory(Memory outputMemory) { + this.next_out = outputMemory; + this.avail_out = new SizeT(outputMemory.size()); + } + + public Pointer next_in; + // number of bytes available at next_in + public SizeT avail_in; + + // total number of input bytes read so far + public SizeT total_in; + + public Pointer next_out; + + // remaining free space at next_out + public SizeT avail_out; + + // total number of bytes output so far + public SizeT total_out; + + // resolution in bits per sample (n = 1, ..., 32) + public int bits_per_sample; + + // block size in samples + public int block_size; + + // Reference sample interval, the number of blocks + // between consecutive reference samples (up to 4096) + public int rsi; + + public int flags; + + public volatile PointerByReference state; + + @Override + protected List getFieldOrder() { + return Arrays.asList("next_in", "avail_in", "total_in", "next_out", "avail_out", "total_out", "bits_per_sample", + "block_size", "rsi", "flags", "state"); + } + } + + // Sample data description flags + + // Samples are signed. Telling libaec this results in a slightly + // better compression ratio. Default is unsigned. + static final int AEC_DATA_SIGNED = 1; + + // 24 bit samples are coded in 3 bytes + static final int AEC_DATA_3BYTE = 2; + + // Samples are stored with their most significant bit first. This has + // nothing to do with the endianness of the host. Default is LSB. + static final int AEC_DATA_MSB = 4; + + // Set if preprocessor should be used + static final int AEC_DATA_PREPROCESS = 8; + + // Use restricted set of code options + static final int AEC_RESTRICTED = 16; + + + // Pad RSI to byte boundary. Only used for decoding some CCSDS sample + // data. Do not use this to produce new data as it violates the + // standard. + static final int AEC_PAD_RSI = 32; + + // Do not enforce standard regarding legal block sizes. + static final int AEC_NOT_ENFORCE = 64; + + // Return codes of library functions + + public static final int AEC_OK = 0; + public static final int AEC_CONF_ERROR = (-1); + public static final int AEC_STREAM_ERROR = (-2); + public static final int AEC_DATA_ERROR = (-3); + public static final int AEC_MEM_ERROR = (-4); + public static final int AEC_RSI_OFFSETS_ERROR = (-5); + + // Options for flushing + + // Do not enforce output flushing. More input may be provided with + // later calls. So far only relevant for encoding. + public static final int AEC_NO_FLUSH = 0; + + // Flush output and end encoding. The last call to aec_encode() must + // set AEC_FLUSH to drain all output. + // It is not possible to continue encoding of the same stream after it + // has been flushed. For one, the last block may be padded zeros after + // preprocessing. Secondly, the last encoded byte may be padded with + // fill bits. + public static final int AEC_FLUSH = 1; + + // Declare native methods corresponding to the functions in libaec.h + // package private - for round trip testing only + static native int aec_buffer_encode(AecStream strm); + + public static native int aec_buffer_decode(AecStream strm); +} diff --git a/native-compression/libaec-jna/src/main/java/edu/ucar/unidata/compression/jna/libaec/SizeT.java b/native-compression/libaec-jna/src/main/java/edu/ucar/unidata/compression/jna/libaec/SizeT.java new file mode 100644 index 0000000000..c21e503ab6 --- /dev/null +++ b/native-compression/libaec-jna/src/main/java/edu/ucar/unidata/compression/jna/libaec/SizeT.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata + * See LICENSE for license information. + */ + +package edu.ucar.unidata.compression.jna.libaec; + +import com.sun.jna.IntegerType; +import com.sun.jna.Native; + +/** + * Map a native size_t with JNA. + * + * @see + */ +public class SizeT extends IntegerType { + public SizeT() { + this(0); + } + + public SizeT(long value) { + super(Native.SIZE_T_SIZE, value, true); + } + + public String toString() { + return String.format("%d", super.longValue()); + } +} diff --git a/native-compression/libaec-jna/src/main/java/edu/ucar/unidata/compression/jna/libaec/aec.h b/native-compression/libaec-jna/src/main/java/edu/ucar/unidata/compression/jna/libaec/aec.h new file mode 100644 index 0000000000..c235ff1f53 --- /dev/null +++ b/native-compression/libaec-jna/src/main/java/edu/ucar/unidata/compression/jna/libaec/aec.h @@ -0,0 +1,183 @@ +/** + * @file libaec.h + * + * @section LICENSE + * Copyright 2024 Mathis Rosenhauer, Moritz Hanke, Joerg Behrens, Luis Kornblueh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @section DESCRIPTION + * + * Adaptive Entropy Coding library + * + */ + +#ifndef LIBAEC_H +#define LIBAEC_H 1 + +#define AEC_VERSION_MAJOR 1 +#define AEC_VERSION_MINOR 1 +#define AEC_VERSION_PATCH 3 +#define AEC_VERSION_STR "1.1.3" + +#include + +#ifdef __cplusplus +extern "C"{ +#endif + +#if defined LIBAEC_BUILD && HAVE_VISIBILITY +# define LIBAEC_DLL_EXPORTED __attribute__((__visibility__("default"))) +#elif (defined _WIN32 && !defined __CYGWIN__) && defined LIBAEC_SHARED +# if defined LIBAEC_BUILD +# define LIBAEC_DLL_EXPORTED __declspec(dllexport) +# else +# define LIBAEC_DLL_EXPORTED __declspec(dllimport) +# endif +#else +# define LIBAEC_DLL_EXPORTED +#endif + +struct internal_state; + +struct aec_stream { + const unsigned char *next_in; + + /* number of bytes available at next_in */ + size_t avail_in; + + /* total number of input bytes read so far */ + size_t total_in; + + unsigned char *next_out; + + /* remaining free space at next_out */ + size_t avail_out; + + /* total number of bytes output so far */ + size_t total_out; + + /* resolution in bits per sample (n = 1, ..., 32) */ + unsigned int bits_per_sample; + + /* block size in samples */ + unsigned int block_size; + + /* Reference sample interval, the number of blocks + * between consecutive reference samples (up to 4096). */ + unsigned int rsi; + + unsigned int flags; + + struct internal_state *state; +}; + +/*********************************/ +/* Sample data description flags */ +/*********************************/ + +/* Samples are signed. Telling libaec this results in a slightly + * better compression ratio. Default is unsigned. */ +#define AEC_DATA_SIGNED 1 + +/* 24 bit samples are coded in 3 bytes */ +#define AEC_DATA_3BYTE 2 + +/* Samples are stored with their most significant bit first. This has + * nothing to do with the endianness of the host. Default is LSB. */ +#define AEC_DATA_MSB 4 + +/* Set if preprocessor should be used */ +#define AEC_DATA_PREPROCESS 8 + +/* Use restricted set of code options */ +#define AEC_RESTRICTED 16 + +/* Pad RSI to byte boundary. Only used for decoding some CCSDS sample + * data. Do not use this to produce new data as it violates the + * standard. */ +#define AEC_PAD_RSI 32 + +/* Do not enforce standard regarding legal block sizes. */ +#define AEC_NOT_ENFORCE 64 + +/*************************************/ +/* Return codes of library functions */ +/*************************************/ +#define AEC_OK 0 +#define AEC_CONF_ERROR (-1) +#define AEC_STREAM_ERROR (-2) +#define AEC_DATA_ERROR (-3) +#define AEC_MEM_ERROR (-4) +#define AEC_RSI_OFFSETS_ERROR (-5) + +/************************/ +/* Options for flushing */ +/************************/ + +/* Do not enforce output flushing. More input may be provided with + * later calls. So far only relevant for encoding. */ +#define AEC_NO_FLUSH 0 + +/* Flush output and end encoding. The last call to aec_encode() must + * set AEC_FLUSH to drain all output. + * + * It is not possible to continue encoding of the same stream after it + * has been flushed. For one, the last block may be padded zeros after + * preprocessing. Secondly, the last encoded byte may be padded with + * fill bits. */ +#define AEC_FLUSH 1 + +/*********************************************/ +/* Streaming encoding and decoding functions */ +/*********************************************/ +LIBAEC_DLL_EXPORTED int aec_encode_init(struct aec_stream *strm); +LIBAEC_DLL_EXPORTED int aec_encode_enable_offsets(struct aec_stream *strm); +LIBAEC_DLL_EXPORTED int aec_encode_count_offsets(struct aec_stream *strm, size_t *rsi_offsets_count); +LIBAEC_DLL_EXPORTED int aec_encode_get_offsets(struct aec_stream *strm, size_t *rsi_offsets, size_t rsi_offsets_count); +LIBAEC_DLL_EXPORTED int aec_buffer_seek(struct aec_stream *strm, size_t offset); +LIBAEC_DLL_EXPORTED int aec_encode(struct aec_stream *strm, int flush); +LIBAEC_DLL_EXPORTED int aec_encode_end(struct aec_stream *strm); + +LIBAEC_DLL_EXPORTED int aec_decode_init(struct aec_stream *strm); +LIBAEC_DLL_EXPORTED int aec_decode_enable_offsets(struct aec_stream *strm); +LIBAEC_DLL_EXPORTED int aec_decode_count_offsets(struct aec_stream *strm, size_t *rsi_offsets_count); +LIBAEC_DLL_EXPORTED int aec_decode_get_offsets(struct aec_stream *strm, size_t *rsi_offsets, size_t rsi_offsets_count); +LIBAEC_DLL_EXPORTED int aec_decode(struct aec_stream *strm, int flush); +LIBAEC_DLL_EXPORTED int aec_decode_range(struct aec_stream *strm, const size_t *rsi_offsets, size_t rsi_offsets_count, size_t pos, size_t size); +LIBAEC_DLL_EXPORTED int aec_decode_end(struct aec_stream *strm); + +/***************************************************************/ +/* Utility functions for encoding or decoding a memory buffer. */ +/***************************************************************/ +LIBAEC_DLL_EXPORTED int aec_buffer_encode(struct aec_stream *strm); +LIBAEC_DLL_EXPORTED int aec_buffer_decode(struct aec_stream *strm); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBAEC_H */ diff --git a/native-compression/libaec-jna/src/test/java/edu/ucar/unidata/compression/jna/libaec/TestLibAec.java b/native-compression/libaec-jna/src/test/java/edu/ucar/unidata/compression/jna/libaec/TestLibAec.java new file mode 100644 index 0000000000..6824c6c7b6 --- /dev/null +++ b/native-compression/libaec-jna/src/test/java/edu/ucar/unidata/compression/jna/libaec/TestLibAec.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2025 University Corporation for Atmospheric Research/Unidata + * See LICENSE for license information. + */ + +package edu.ucar.unidata.compression.jna.libaec; + +import static com.google.common.truth.Truth.assertThat; +import static edu.ucar.unidata.compression.jna.libaec.LibAec.AEC_DATA_PREPROCESS; +import static edu.ucar.unidata.compression.jna.libaec.LibAec.AEC_DATA_SIGNED; +import static edu.ucar.unidata.compression.jna.libaec.LibAec.AEC_OK; + +import com.sun.jna.Memory; +import edu.ucar.unidata.compression.jna.libaec.LibAec.AecStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.junit.Test; + +public class TestLibAec { + + private static final int[] origData = new int[] {10, 18, 31, 42, 50}; + + // compute upper bound of compressed size in bytes + // see https://gitlab.dkrz.de/k202009/libaec#output + // In rare cases, like for random data, total_out can be larger than + // the size of the input data total_in. The following should hold true + // even for pathological cases. + // total_out <= total_in * 67 / 64 + 256 + private static final int maxTotalOutBytes = origData.length * Integer.BYTES * 67 / 64 + 256; + + // unsigned byte values of encoded version of origData + private static final int[] expectedEncoded = {24, 0, 0, 0, 80, 64, 130, 31, 254, 80, 0, 0, 0}; + + // encoding parameters + private static final int bitsPerSample = 32; + private static final int blockSize = 16; + private static final int rsi = 128; + private static final int flags = AEC_DATA_SIGNED | AEC_DATA_PREPROCESS; + + @Test + public void aecBufferEncode() { + // convert input data into byte array + ByteBuffer bb = ByteBuffer.allocate(origData.length * Integer.BYTES); + bb.order(ByteOrder.nativeOrder()); + bb.asIntBuffer().put(origData); + byte[] inputData = bb.array(); + + // initialize native memory to hold input and output data + try (Memory inputMemory = new Memory(inputData.length * Byte.BYTES); + Memory outputMemory = new Memory(maxTotalOutBytes)) { + + // create stream decoder + AecStream aecStreamDecode = AecStream.create(bitsPerSample, blockSize, rsi, flags); + + // load input data into native memory + inputMemory.write(0, inputData, 0, inputData.length); + aecStreamDecode.setInputMemory(inputMemory); + + aecStreamDecode.setOutputMemory(outputMemory); + + // encode + int ok = LibAec.aec_buffer_encode(aecStreamDecode); + assertThat(ok).isEqualTo(AEC_OK); + + // check expected number of bytes encoded + assertThat(aecStreamDecode.total_out.intValue()).isEqualTo(expectedEncoded.length * Byte.BYTES); + + // read encoded data from native memory + byte[] encodedData = new byte[aecStreamDecode.total_out.intValue()]; + outputMemory.read(0, encodedData, 0, encodedData.length); + + // compare encoded data to expect values (note: encoded values are unsigned bytes) + for (int i = 0; i < encodedData.length; i++) { + assertThat(Byte.toUnsignedInt(encodedData[i])).isEqualTo(expectedEncoded[i]); + } + } + } + + @Test + public void aecBufferDecode() { + try (Memory inputMemory = new Memory(expectedEncoded.length * Byte.BYTES); + Memory outputMemory = new Memory(origData.length * Integer.BYTES)) { + // set encoding parameters + AecStream aecStreamEncode = AecStream.create(bitsPerSample, blockSize, rsi, flags); + + // load expected encoded data into native memory + byte[] expectedEncodedBytes = new byte[expectedEncoded.length]; + for (int i = 0; i < expectedEncoded.length; i++) { + expectedEncodedBytes[i] = (byte) expectedEncoded[i]; + } + inputMemory.write(0, expectedEncodedBytes, 0, expectedEncodedBytes.length); + + aecStreamEncode.setInputMemory(inputMemory); + aecStreamEncode.setOutputMemory(outputMemory); + + // decode + int ok = LibAec.aec_buffer_decode(aecStreamEncode); + assertThat(ok).isEqualTo(AEC_OK); + + // check expected number of bytes decoded + assertThat(aecStreamEncode.total_out.intValue()).isEqualTo(origData.length * Integer.BYTES); + + // read encoded data from native memory + int[] decodedData = new int[aecStreamEncode.total_out.intValue() / Integer.BYTES]; + outputMemory.read(0, decodedData, 0, decodedData.length); + + // compare decoded data to original values + for (int i = 0; i < decodedData.length; i++) { + assertThat(decodedData[i]).isEqualTo(origData[i]); + } + } + } + + @Test + public void roundTrip() { + // byte array to hold the encoded data + byte[] encodedData; + + // convert input data into byte array + ByteBuffer bb = ByteBuffer.allocate(origData.length * Integer.BYTES); + bb.order(ByteOrder.nativeOrder()); + bb.asIntBuffer().put(origData); + byte[] inputData = bb.array(); + + // initialize native memory to hold input and output data + try (Memory inputMemory = new Memory(inputData.length * Byte.BYTES); + Memory outputMemory = new Memory(maxTotalOutBytes)) { + + // set encoding parameters + AecStream aecStreamDecode = AecStream.create(bitsPerSample, blockSize, rsi, flags); + + // load input data into native memory + inputMemory.write(0, inputData, 0, inputData.length); + aecStreamDecode.setInputMemory(inputMemory); + + aecStreamDecode.setOutputMemory(outputMemory); + + // encode + int ok = LibAec.aec_buffer_encode(aecStreamDecode); + assertThat(ok).isEqualTo(AEC_OK); + + // read encoded data from native memory + encodedData = new byte[aecStreamDecode.total_out.intValue()]; + outputMemory.read(0, encodedData, 0, encodedData.length); + } + + assertThat(encodedData).isNotNull(); + + try (Memory inputMemory = new Memory(encodedData.length * Byte.BYTES); + Memory outputMemory = new Memory(origData.length * Integer.BYTES)) { + + // set encoding parameters + AecStream aecStreamEncode = AecStream.create(bitsPerSample, blockSize, rsi, flags); + + // load encoded data into memory + inputMemory.write(0, encodedData, 0, encodedData.length); + + aecStreamEncode.setInputMemory(inputMemory); + aecStreamEncode.setOutputMemory(outputMemory); + + // decode + int ok = LibAec.aec_buffer_decode(aecStreamEncode); + assertThat(ok).isEqualTo(AEC_OK); + + // check expected number of bytes decoded + assertThat(aecStreamEncode.total_out.intValue()).isEqualTo(origData.length * Integer.BYTES); + + // read encoded data from native memory + int[] decodedData = new int[aecStreamEncode.total_out.intValue() / Integer.BYTES]; + outputMemory.read(0, decodedData, 0, decodedData.length); + + // compare decoded data to original values + for (int i = 0; i < decodedData.length; i++) { + assertThat(decodedData[i]).isEqualTo(origData[i]); + } + } + } +} diff --git a/native-compression/libaec-native/build.gradle b/native-compression/libaec-native/build.gradle new file mode 100644 index 0000000000..0dbe2a82b0 --- /dev/null +++ b/native-compression/libaec-native/build.gradle @@ -0,0 +1,52 @@ +import java.security.DigestInputStream +import java.security.MessageDigest + +apply from: "$rootDir/gradle/any/java-library.gradle" + +description = 'Jar distribution of native libraries for libaec compression.' +ext.title = 'Native libraries for libaec.' + +// zip file produced by GitHub workflow +def libaecNative = 'libaec-native-1.1.3-1ef165d829f330d72d199d3c4655c327d339604d.zip' + +// sha256 checksum from GitHub workflow output +def expectedChecksum = 'f8273bf7eef9aef6bf4766301d978a2dea7cefcad72a2d7c2d655299d5847a4b' + +def resourceZip = new File("${rootDir}/project-files/native/libaec/${libaecNative}") + +def fetchNativeResources = tasks.register("fetchNativeResources") { + outputs.file resourceZip + doLast { + if (!resourceZip.exists()) { + def actualChecksum = '' + def resourceUrl = "https://downloads.unidata.ucar.edu/netcdf-java/native/libaec/${libaecNative}" + new URL(resourceUrl).withInputStream { + ips -> + DigestInputStream dips = new DigestInputStream(ips, MessageDigest.getInstance('SHA-256')) + resourceZip.withOutputStream { ops -> + ops << dips + } + actualChecksum = dips.getMessageDigest().digest().encodeHex().toString() + } + if (actualChecksum != expectedChecksum) { + resourceZip.delete() + String msg = + String.format('Error: checksum on libaec.zip does not match expected ' + + 'value.\n Expected: %s\n Actual: %s\n', expectedChecksum, actualChecksum) + throw new RuntimeException(msg); + } + } + } +} + +def processNativeResources = tasks.register("processNativeResources", Copy) { + inputs.file resourceZip + from zipTree(resourceZip) + destinationDir(file("$buildDir/resources/main")) + eachFile { details -> + details.path = details.path.replaceFirst("resources/", "") + } + dependsOn(fetchNativeResources) +} + +processResources.dependsOn(processNativeResources) diff --git a/settings.gradle b/settings.gradle index cb743f2a77..17ed36aaa3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,8 @@ include 'docs' include 'grib' include 'httpservices' include 'legacy' +include 'native-compression:libaec-jna' +include 'native-compression:libaec-native' include 'netcdf4' include 'netcdf-java-bom' include 'netcdf-java-platform'