diff --git a/build.gradle b/build.gradle index c83bf27a..567c66ae 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'org-openjfx-javafxplugin' id 'org.beryx.jlink' version '3.1.1' id 'org.gradlex.extra-java-module-info' version '1.9' + id 'com.sparrowwallet.filterjar' } def sparrowVersion = '2.1.4' @@ -457,4 +458,12 @@ extraJavaModuleInfo { module('com.jcraft:jzlib', 'com.jcraft.jzlib') { exports('com.jcraft.jzlib') } +} + +String torOs = os.macOsX ? "macos" : (os.windows ? "mingw" : "linux-libc") +filterInfo { + filter('io.matthewnelson.kmp-tor', 'resource-lib-tor-gpl-jvm') { + include("io/matthewnelson/kmp/tor/resource/lib/tor/native/${torOs}/${releaseArch}") + exclude('io/matthewnelson/kmp/tor/resource/lib/tor/native/') + } } \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 8cf7a830..f34a90b9 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -20,5 +20,9 @@ gradlePlugin { id = "org-openjfx-javafxplugin" implementationClass = "org.openjfx.gradle.JavaFXPlugin" } + register("com.sparrowwallet.filterjar") { + id = "com.sparrowwallet.filterjar" + implementationClass = "com.sparrowwallet.filterjar.FilterJarPlugin" + } } } diff --git a/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarExtension.java b/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarExtension.java new file mode 100644 index 00000000..a8f756ff --- /dev/null +++ b/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarExtension.java @@ -0,0 +1,69 @@ +package com.sparrowwallet.filterjar; + +import org.gradle.api.Action; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.attributes.Attribute; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.tasks.SourceSet; + +import javax.inject.Inject; + +public abstract class FilterJarExtension { + static Attribute FILTERED_ATTRIBUTE = Attribute.of("filtered", Boolean.class); + + public abstract MapProperty getFilterConfigs(); + + @Inject + protected abstract ObjectFactory getObjects(); + + @Inject + protected abstract ConfigurationContainer getConfigurations(); + + public void filter(String group, String artifact, Action action) { + String name = group + ":" + artifact; + JarFilterConfigImpl config = new JarFilterConfigImpl(name, getObjects()); + config.setGroup(group); + config.setArtifact(artifact); + action.execute(config); + getFilterConfigs().put(name, config); + } + + /** + * Activate the plugin's functionality for dependencies of all scopes of the given source set + * (runtimeClasspath, compileClasspath, annotationProcessor). + * Note that the plugin activates the functionality for all source sets by default. + * Therefore, this method only has an effect for source sets for which a {@link #deactivate(Configuration)} + * has been performed. + * + * @param sourceSet the Source Set to activate (e.g. sourceSets.test) + */ + public void activate(SourceSet sourceSet) { + Configuration runtimeClasspath = getConfigurations().getByName(sourceSet.getRuntimeClasspathConfigurationName()); + Configuration compileClasspath = getConfigurations().getByName(sourceSet.getCompileClasspathConfigurationName()); + Configuration annotationProcessor = getConfigurations().getByName(sourceSet.getAnnotationProcessorConfigurationName()); + + activate(runtimeClasspath); + activate(compileClasspath); + activate(annotationProcessor); + } + + /** + * Activate the plugin's functionality for a single resolvable Configuration. + * + * @param resolvable a resolvable Configuration (e.g. configurations["customClasspath"]) + */ + public void activate(Configuration resolvable) { + resolvable.getAttributes().attribute(FILTERED_ATTRIBUTE, true); + } + + /** + * Deactivate the plugin's functionality for a single resolvable Configuration. + * + * @param resolvable a resolvable Configuration (e.g. configurations.annotationProcessor) + */ + public void deactivate(Configuration resolvable) { + resolvable.getAttributes().attribute(FILTERED_ATTRIBUTE, false); + } +} diff --git a/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarParameters.java b/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarParameters.java new file mode 100644 index 00000000..23565391 --- /dev/null +++ b/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarParameters.java @@ -0,0 +1,10 @@ +package com.sparrowwallet.filterjar; + +import org.gradle.api.artifacts.transform.TransformParameters; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.tasks.Input; + +public interface FilterJarParameters extends TransformParameters { + @Input + MapProperty getFilterConfigs(); +} diff --git a/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarPlugin.java b/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarPlugin.java new file mode 100644 index 00000000..fd284bc0 --- /dev/null +++ b/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarPlugin.java @@ -0,0 +1,33 @@ +package com.sparrowwallet.filterjar; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.tasks.SourceSetContainer; + +import static com.sparrowwallet.filterjar.FilterJarExtension.FILTERED_ATTRIBUTE; + +public class FilterJarPlugin implements Plugin { + @Override + public void apply(Project project) { + // Register the extension + FilterJarExtension extension = project.getExtensions().create("filterInfo", FilterJarExtension.class); + + project.getPlugins().withType(JavaPlugin.class).configureEach(_ -> { + // By default, activate plugin for all source sets + project.getExtensions().getByType(SourceSetContainer.class).all(extension::activate); + + // All jars have a filtered=false attribute by default + project.getDependencies().getArtifactTypes().maybeCreate("jar").getAttributes().attribute(FILTERED_ATTRIBUTE, false); + + // Register the transform + project.getDependencies().registerTransform(FilterJarTransform.class, transform -> { + transform.getFrom().attribute(FILTERED_ATTRIBUTE, false); + transform.getTo().attribute(FILTERED_ATTRIBUTE, true); + transform.parameters(params -> { + params.getFilterConfigs().putAll(extension.getFilterConfigs()); + }); + }); + }); + } +} diff --git a/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarTransform.java b/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarTransform.java new file mode 100644 index 00000000..b8f9659d --- /dev/null +++ b/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarTransform.java @@ -0,0 +1,79 @@ +package com.sparrowwallet.filterjar; + + +import org.gradle.api.artifacts.transform.InputArtifact; +import org.gradle.api.artifacts.transform.TransformAction; +import org.gradle.api.artifacts.transform.TransformOutputs; +import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; + +import java.io.File; +import java.util.Map; +import java.util.Set; +import java.util.HashSet; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.nio.file.Files; + +public abstract class FilterJarTransform implements TransformAction { + @InputArtifact + @PathSensitive(PathSensitivity.NAME_ONLY) + public abstract Provider getInputArtifact(); + + @Override + public void transform(TransformOutputs outputs) { + File originalJar = getInputArtifact().get().getAsFile(); + String jarName = originalJar.getName(); + + // Get filter configurations from parameters + Map filterConfigs = getParameters().getFilterConfigs().get(); + + //Inclusions are prioritised ahead of exclusions + Set inclusions = new HashSet<>(); + Set exclusions = new HashSet<>(); + + // Check if this JAR matches any configured filters (simplified matching based on artifact name) + filterConfigs.forEach((key, config) -> { + if(jarName.contains(config.getArtifact())) { + inclusions.addAll(config.getInclusions()); + exclusions.addAll(config.getExclusions()); + } + }); + + try { + if(!exclusions.isEmpty()) { + filterJar(originalJar, getFilterJar(outputs, originalJar), inclusions, exclusions); + } else { + outputs.file(originalJar); + } + } catch(Exception e) { + throw new RuntimeException("Failed to transform jar: " + jarName, e); + } + } + + private void filterJar(File inputFile, File outputFile, Set inclusions, Set exclusions) throws Exception { + try(JarFile jarFile = new JarFile(inputFile); JarOutputStream jarOut = new JarOutputStream(Files.newOutputStream(outputFile.toPath()))) { + jarFile.entries().asIterator().forEachRemaining(entry -> { + String entryName = entry.getName(); + boolean shouldInclude = inclusions.stream().anyMatch(entryName::startsWith); + boolean shouldExclude = exclusions.stream().anyMatch(entryName::startsWith); + if(shouldInclude || !shouldExclude) { + try { + jarOut.putNextEntry(new JarEntry(entryName)); + jarFile.getInputStream(entry).transferTo(jarOut); + jarOut.closeEntry(); + } catch(Exception e) { + throw new RuntimeException("Error processing entry: " + entryName, e); + } + } + }); + } + } + + private File getFilterJar(TransformOutputs outputs, File originalJar) { + return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-filtered.jar"); + } +} diff --git a/buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfig.java b/buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfig.java new file mode 100644 index 00000000..5bd792c7 --- /dev/null +++ b/buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfig.java @@ -0,0 +1,19 @@ +package com.sparrowwallet.filterjar; + +import org.gradle.api.tasks.Input; + +import java.util.List; + +public interface JarFilterConfig { + @Input + String getGroup(); + + @Input + String getArtifact(); + + @Input + List getInclusions(); + + @Input + List getExclusions(); +} diff --git a/buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfigImpl.java b/buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfigImpl.java new file mode 100644 index 00000000..912ccd4d --- /dev/null +++ b/buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfigImpl.java @@ -0,0 +1,64 @@ +package com.sparrowwallet.filterjar; + +import org.gradle.api.Named; +import org.gradle.api.model.ObjectFactory; +import javax.inject.Inject; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class JarFilterConfigImpl implements Named, JarFilterConfig, Serializable { + private final String name; + private String group; + private String artifact; + private final List inclusions; + private final List exclusions; + + @Inject + public JarFilterConfigImpl(String name, ObjectFactory objectFactory) { + this.name = name; + this.inclusions = new ArrayList<>(); + this.exclusions = new ArrayList<>(); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + @Override + public String getArtifact() { + return artifact; + } + + public void setArtifact(String artifact) { + this.artifact = artifact; + } + + @Override + public List getInclusions() { + return inclusions; + } + + public void include(String path) { + inclusions.add(path); + } + + @Override + public List getExclusions() { + return exclusions; + } + + public void exclude(String path) { + exclusions.add(path); + } +}