From 8ac47e39418ee5d46e1057f7edd569c5d9038a98 Mon Sep 17 00:00:00 2001 From: Christopher Trom Date: Wed, 19 Aug 2026 15:59:04 -0500 Subject: [PATCH] Add BlowdryerSetup.classpathJar(String) to find a jar already on the classpath Finds a jar declared in the plugins block of settings.gradle (or otherwise already on the classpath) by matching its filename, and uses it as the resource source via the existing localJar(File) mechanism. Closes #21. --- CHANGELOG.md | 2 + README.md | 15 +++++- .../diffplug/blowdryer/BlowdryerSetup.java | 49 ++++++++++++++++++- .../blowdryer/BlowdryerPluginTest.java | 49 ++++++++++++++++++- .../com/diffplug/blowdryer/GradleHarness.java | 38 +++++++++++++- 5 files changed, 149 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d91f686..c096dd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `BlowdryerSetup.classpathJar(String)` finds a jar already on the classpath (e.g. one declared in the `plugins` block of `settings.gradle`) by matching its filename, and uses it as the resource source. ([#21](https://github.com/diffplug/blowdryer/issues/21)) ## [1.7.1] - 2023-12-08 ### Fixed diff --git a/README.md b/README.md index ddc6c91..81ddb4f 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,20 @@ blowdryerSetup { } ``` -To pull this jar from a maven repository, see [#21](https://github.com/diffplug/blowdryer/issues/21). +If you'd rather pull the jar from a maven repository, declare it in the `plugins` block of `settings.gradle` and let `classpathJar` find it on the classpath by (a substring of) its filename: + +```gradle +// settings.gradle +plugins { + id 'com.diffplug.blowdryerSetup' version '1.7.1' + id 'acme.blowdryer-scripts' version '3.0.0' +} +blowdryerSetup { + classpathJar('acme.blowdryer-scripts') +} +``` + +If your `acme.blowdryer-scripts` jar bundles its own `Plugin` which calls `blowdryerSetup { classpathJar(...) }` internally, then applying that one plugin is all a consumer has to do - they don't need a `blowdryerSetup` block of their own at all. ## Plugin versions diff --git a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java index 27cc7e7..e631fab 100644 --- a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java +++ b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2023 DiffPlug + * Copyright (C) 2019-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,16 @@ import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; +import java.util.List; import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; @@ -363,6 +368,48 @@ public void localJar(File jarFile) { Blowdryer.setResourcePlugin(resource -> rootUrl + resource); } + /** + * Finds a jar already on the classpath (e.g. one declared in the `plugins` block of + * `settings.gradle`) whose filename contains {@code name}, and uses it to extract a file + * resource, same as {@link #localJar(File)}. + * @param name Substring to match against the filename of a jar on the classpath. + */ + public void classpathJar(String name) { + Objects.requireNonNull(name, "name must not be null."); + localJar(findClasspathJar(name)); + } + + private static File findClasspathJar(String name) { + List candidates = new ArrayList<>(); + ClassLoader loader = BlowdryerSetup.class.getClassLoader(); + while (loader != null) { + if (loader instanceof URLClassLoader) { + for (URL url : ((URLClassLoader) loader).getURLs()) { + if (fileName(url).contains(name)) { + candidates.add(url); + } + } + } + loader = loader.getParent(); + } + if (candidates.isEmpty()) { + throw new IllegalArgumentException("No jar on the classpath has a filename containing '" + name + "'."); + } else if (candidates.size() > 1) { + throw new IllegalArgumentException("Multiple jars on the classpath have a filename containing '" + name + "': " + candidates); + } + try { + return new File(candidates.get(0).toURI()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Could not convert " + candidates.get(0) + " to a file", e); + } + } + + private static String fileName(URL url) { + String path = url.getPath(); + int lastSlash = path.lastIndexOf('/'); + return lastSlash == -1 ? path : path.substring(lastSlash + 1); + } + @NotNull private String getFullResourcePath(String resource) { return (repoSubfolder.isEmpty() ? "" : repoSubfolder + "/") + resource; diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java index d85b61a..1376e1a 100644 --- a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java +++ b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2023 DiffPlug + * Copyright (C) 2019-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,9 @@ */ package com.diffplug.blowdryer; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.Arrays; import java.util.Locale; import java.util.stream.Collectors; @@ -71,6 +73,21 @@ private void settingsLocalJar(String dependency) throws IOException { "blowdryerSetup { localJar(file('" + dependency + "')) }"); } + private void settingsClasspathJar(String name) throws IOException { + write(SETTINGS_GRADLE, + "plugins { id 'com.diffplug.blowdryerSetup' }", + "blowdryerSetup { classpathJar('" + name + "') }"); + } + + private File testJarFile() { + String jarFile = BlowdryerPluginTest.class.getResource("test.jar").getFile(); + if (System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win")) { + Assertions.assertThat(jarFile).startsWith("/"); + jarFile = jarFile.substring(1); + } + return new File(jarFile); + } + @Test public void githubTag() throws IOException { settingsGithub("test/2/a"); @@ -364,6 +381,36 @@ public void localJarFileDownloadDoesNotExist() throws IOException { gradleRunner().buildAndFail(); } + @Test + public void classpathJarFound() throws IOException { + settingsClasspathJar("test.jar"); + + write(BUILD_GRADLE, + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('sample').exists()"); + + gradleRunnerWithExtraClasspath(testJarFile()).build(); + } + + @Test + public void classpathJarNoMatch() throws IOException { + settingsClasspathJar("does-not-exist"); + + Assertions.assertThat(gradleRunnerWithExtraClasspath(testJarFile()).buildAndFail().getOutput().replace("\r\n", "\n")) + .contains("No jar on the classpath has a filename containing 'does-not-exist'"); + } + + @Test + public void classpathJarMultipleMatches() throws IOException { + byte[] testJarBytes = Files.readAllBytes(testJarFile().toPath()); + File testJarA = write("libs/test-a.jar", testJarBytes); + File testJarB = write("libs/test-b.jar", testJarBytes); + settingsClasspathJar("test"); + + Assertions.assertThat(gradleRunnerWithExtraClasspath(testJarA, testJarB).buildAndFail().getOutput().replace("\r\n", "\n")) + .contains("Multiple jars on the classpath have a filename containing 'test'"); + } + @Test public void tooOldError() throws IOException { Assume.assumeTrue(!System.getProperty("java.vm.specification.version").equals("17")); diff --git a/src/test/java/com/diffplug/blowdryer/GradleHarness.java b/src/test/java/com/diffplug/blowdryer/GradleHarness.java index 3d90e16..b5c27c3 100644 --- a/src/test/java/com/diffplug/blowdryer/GradleHarness.java +++ b/src/test/java/com/diffplug/blowdryer/GradleHarness.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2023 DiffPlug + * Copyright (C) 2018-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,13 @@ */ package com.diffplug.blowdryer; +import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; import org.gradle.testkit.runner.GradleRunner; public class GradleHarness extends ResourceHarness { @@ -30,6 +36,36 @@ protected GradleRunner gradleRunner() throws IOException { return runner; } + /** A gradleRunner() whose plugin classpath also includes the given jars. */ + protected GradleRunner gradleRunnerWithExtraClasspath(File... extraJars) throws IOException { + List classpath = new ArrayList<>(pluginClasspath()); + classpath.addAll(Arrays.asList(extraJars)); + GradleRunner runner = GradleRunner.create() + .withProjectDir(rootFolder()) + .withPluginClasspath(classpath); + if (jreVersion() < 16) { + runner.withGradleVersion(BlowdryerSetupPlugin.MINIMUM_GRADLE); + } + return runner; + } + + /** Reads the same plugin-under-test classpath that {@code withPluginClasspath()} uses. */ + private static List pluginClasspath() throws IOException { + try (InputStream input = GradleHarness.class.getClassLoader().getResourceAsStream("plugin-under-test-metadata.properties")) { + if (input == null) { + throw new IllegalStateException("Could not find plugin-under-test-metadata.properties on the test classpath."); + } + Properties props = new Properties(); + props.load(input); + String classpath = props.getProperty("implementation-classpath"); + List files = new ArrayList<>(); + for (String path : classpath.split(File.pathSeparator)) { + files.add(new File(path)); + } + return files; + } + } + private static int jreVersion() { return Integer.parseInt(System.getProperty("java.vm.specification.version")); }