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")); }