From fe44c083966ba17af3eb8b97907793ad35ecab75 Mon Sep 17 00:00:00 2001 From: Schalk Cronje Date: Tue, 28 May 2019 15:23:18 +0200 Subject: [PATCH] Support an internal Ivy proxy to Rubygems.org (#364) - Resolve dependencies with transitive dependencies. - Added a repository handler extension for defining rubygems. - Protect ivy.xml file creation against concurrent access from threads of multiple processes. - Support all known GEM version requirement formats --- base-plugin/build.gradle | 1 + .../jrubygradle/internal/GemVersion.groovy | 206 ---------- .../internal/GemVersionResolver.groovy | 5 +- build.gradle | 5 +- core-plugin/build.gradle | 31 ++ .../IvyXmlProxyServerIntegrationSpec.groovy | 138 +++++++ .../jrubygradle/core/ApiException.groovy | 12 + .../jrubygradle/core/GemDependency.java | 7 + .../com/github/jrubygradle/core/GemInfo.java | 104 +++++ .../github/jrubygradle/core/GemVersion.groovy | 355 ++++++++++++++++++ .../core/GemVersionException.groovy | 12 + .../jrubygradle/core/IvyXmlProxyServer.java | 10 + .../jrubygradle/core/JRubyCorePlugin.groovy | 23 ++ .../core/RepositoryHandlerExtension.groovy | 66 ++++ .../jrubygradle/core/RubyGemQueryRestApi.java | 40 ++ .../core/internal/DefaultGemDependency.groovy | 14 + .../core/internal/DefaultGemInfo.groovy | 33 ++ .../internal/DefaultRubyGemRestApi.groovy | 134 +++++++ .../jrubygradle/core/internal/GemToIvy.groovy | 92 +++++ .../jrubygradle/core/internal/IvyUtils.groovy | 34 ++ .../internal/IvyXmlGlobalProxyRegistry.groovy | 57 +++ .../internal/IvyXmlRatpackProxyServer.groovy | 136 +++++++ .../com.github.jruby-gradle.core.properties | 1 + .../core/IvyXmlProxyServerSpec.groovy | 38 ++ .../core/RubyGemQueryRestApiSpec.groovy | 81 ++++ .../core/internal/GemToIvySpec.groovy | 22 ++ .../core}/internal/GemVersionSpec.groovy | 65 ++-- gradle.properties | 1 + gradle/integration-tests.gradle | 3 +- settings.gradle | 2 +- 30 files changed, 1482 insertions(+), 246 deletions(-) delete mode 100644 base-plugin/src/main/groovy/com/github/jrubygradle/internal/GemVersion.groovy create mode 100644 core-plugin/build.gradle create mode 100644 core-plugin/src/integTest/groovy/com/github/jrubygradle/core/IvyXmlProxyServerIntegrationSpec.groovy create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/ApiException.groovy create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/GemDependency.java create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/GemInfo.java create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/GemVersion.groovy create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/GemVersionException.groovy create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/IvyXmlProxyServer.java create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/JRubyCorePlugin.groovy create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/RepositoryHandlerExtension.groovy create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/RubyGemQueryRestApi.java create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/DefaultGemDependency.groovy create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/DefaultGemInfo.groovy create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/DefaultRubyGemRestApi.groovy create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/GemToIvy.groovy create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/IvyUtils.groovy create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/IvyXmlGlobalProxyRegistry.groovy create mode 100644 core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/IvyXmlRatpackProxyServer.groovy create mode 100644 core-plugin/src/main/resources/META-INF/gradle-plugins/com.github.jruby-gradle.core.properties create mode 100644 core-plugin/src/test/groovy/com/github/jrubygradle/core/IvyXmlProxyServerSpec.groovy create mode 100644 core-plugin/src/test/groovy/com/github/jrubygradle/core/RubyGemQueryRestApiSpec.groovy create mode 100644 core-plugin/src/test/groovy/com/github/jrubygradle/core/internal/GemToIvySpec.groovy rename {base-plugin/src/test/groovy/com/github/jrubygradle => core-plugin/src/test/groovy/com/github/jrubygradle/core}/internal/GemVersionSpec.groovy (63%) diff --git a/base-plugin/build.gradle b/base-plugin/build.gradle index f57fa8fd..24d86ed2 100644 --- a/base-plugin/build.gradle +++ b/base-plugin/build.gradle @@ -29,6 +29,7 @@ ext { dependencies { + compile project(":jruby-gradle-core-plugin") compile "org.eclipse.jetty:jetty-server:${jettyVersion}" compile "org.eclipse.jetty:jetty-webapp:${jettyVersion}" runtime('de.saumya.mojo:rubygems:0.2.3@war') { diff --git a/base-plugin/src/main/groovy/com/github/jrubygradle/internal/GemVersion.groovy b/base-plugin/src/main/groovy/com/github/jrubygradle/internal/GemVersion.groovy deleted file mode 100644 index 1354d846..00000000 --- a/base-plugin/src/main/groovy/com/github/jrubygradle/internal/GemVersion.groovy +++ /dev/null @@ -1,206 +0,0 @@ -package com.github.jrubygradle.internal - -import java.util.regex.Pattern - -/** - * With rubygems almost all dependencies will be declared - * via versions ranges and tools like Bundler are very strict on how to - * resolve those versions - i.e. the resolved version needs to obey each given - * contraint. Maven does the same but Gradle and Ivy pick the latest and - * newest version when there are more then one contraint for the same gem - - * which can create problems when using Bundler alongside Gradle. - * - * When converting a GemSpec into a Maven pom.xml the translation of a - * gem version range into a maven version range. typically '~> 1.0' from ruby - * becomes [1.0, 1.99999] on the maven side. so most dependencies from - * gem artifacts will use such version ranges. - * - * to help gradle to be closer to the rubygems world when resolving gem - * artifacts, it needs to calculate intersection between version ranges - * in maven manner. - * - * this class basically represents a maven version range with boundary - * (exclusive vs. inclusive) and its lower and upper bounded version and - * allows to intersect its range with another version range. - * - * it also translate fixed version '1.0' to [1.0, 1.0] or the gradle notation - * 1.2+ to [1.2, 1.99999] or 1.+ to [1.0, 1.99999] following the gemspec-to-pom - * pattern. - * - * @author Christian Meier - */ -class GemVersion { - - private static final MAX_VERSION = '99999' - - private static final LOW_EX = '(' - private static final LOW_IN = '[' - private static final UP_EX = ')' - private static final UP_IN = ']' - private static final Pattern DOT_PLUS = Pattern.compile('\\.\\+') - private static final Pattern PLUS = Pattern.compile('\\+') - private static final Pattern DIGITS_PLUS = Pattern.compile('[0-9]+\\+') - private static final Pattern HEAD = Pattern.compile('^.*,\\s*') - private static final Pattern TAIL = Pattern.compile(',.*$') - private static final Pattern FIRST = Pattern.compile('^[\\[\\(]') - private static final Pattern LAST = Pattern.compile('[\\]\\)]$') - private static final Pattern ZEROS = Pattern.compile('(\\.0)+$') - - private static final VERSION_SPLIT = '[.]' - - final String low - final String high - final prefix = LOW_IN - final postfix = UP_IN - - private GemVersion(String pre, String low, String high, String post) { - this.prefix = pre - this.low = low - this.high = high - this.postfix = post - } - - /** - * converts the given string to a version range with inclusive or - * exclusive boundaries. - * - * @param String version - */ - GemVersion(String version) { - // workaround a bug in gem proxy which can not handle != operators - version = version.replace('=', '') - if (version.contains('+')) { - low = ZEROS.matcher(PLUS.matcher(DOT_PLUS.matcher(version).replaceFirst('.0')).replaceFirst('')).replaceFirst('') - high = DIGITS_PLUS.matcher(DOT_PLUS.matcher(version).replaceFirst('.99999')).replaceFirst(MAX_VERSION) - } - else if (version.contains(LOW_IN) || version.contains(LOW_EX) || - version.contains(UP_IN) || version.contains(UP_EX)) { - prefix = version.charAt(0).toString() - postfix = version.charAt(version.size() - 1).toString() - low = ZEROS.matcher(FIRST.matcher(TAIL.matcher(version).replaceFirst('')).replaceFirst('')).replaceFirst('') - high = LAST.matcher(HEAD.matcher(version).replaceFirst('')).replaceFirst('') - - if (high == '') { - high = MAX_VERSION - } - } - else { - low = version - high = version - } - } - - /** - * since GemVersion is version range with lower bound and upper bound - * this method just calculates the intersection of this version range - * with the given other version range. it also honors whether the boundary - * itself is included or excluded by the respective ranges. - * - * @param String the other version range to be intersected with this version range - * @return GemVersion the intersected version range - */ - GemVersion intersect(String otherVersion) { - GemVersion other = new GemVersion(otherVersion) - String newPrefix - String newLow - switch (compare(low, other.low)) { - case -1: - newLow = other.low - newPrefix = other.prefix - break - case 0: - newPrefix = prefix == LOW_EX || other.prefix == LOW_EX ? LOW_EX : LOW_IN - newLow = low - break - case 1: - newLow = low - newPrefix = prefix - } - String newPostfix - String newHigh - - switch (compare(high, other.high)) { - case 1: - newHigh = other.high - newPostfix = other.postfix - break - case 0: - newPostfix = postfix == UP_EX || other.postfix == UP_EX ? UP_EX : UP_IN - newHigh = high - break - case -1: - newHigh = high - newPostfix = postfix - } - return new GemVersion(newPrefix, newLow, newHigh, newPostfix) - } - - /** - * compares two version strings. first it splits the version - * into parts on their ".". if one version has more parts then - * the other, then the number of parts is used for comparison. - * otherwise we find a part which differs between the versions - * and compare them. this last comparision converts the parts to - * integers if both contains only digits. otherwise a lexical - * string comparision is used. - * - * @param String aObject first version - * @param String bObject second version - * @return int -1 if aObject < bObject, 0 if both are equal and 1 if aObject > bObject - */ - private int compare(String aObject, String bObject) { - String[] aDigits = aObject.split(VERSION_SPLIT) - String[] bDigits = bObject.split(VERSION_SPLIT) - int index = -1 - - for (int i = 0; i < aDigits.length && i < bDigits.length; i++) { - if (aDigits[i] != bDigits[i] ) { - index = i - break - } - } - - if (index == -1) { - // one contains the other - so look at the length - if (aDigits.length < bDigits.length) { - return -1 - } - if (aDigits.length == bDigits.length) { - return 0 - } - return 1 - } - - if (aDigits[index].isInteger() && bDigits[index].isInteger()) { - // compare them as number - aDigits[index] as int <=> bDigits[index] as int - } - else { - // compare them as string - aDigits[index] <=> bDigits[index] - } - } - - /** - * examines the version range on conflict, i.e. lower bound bigger then - * upper bound. - * @return boolean true if lower bound bigger then upper bound - */ - boolean conflict() { - return (compare(low, high) == 1) - } - - /** - * string of the underlying data as maven version range. for prereleased - * versions with ranges like [1.pre, 1.pre] the to range will be replaced - * by the single boundary of the range. - * - * @return String maven version range - */ - String toString() { - if (prefix == LOW_IN && postfix == UP_IN && low == high && low =~ /[a-zA-Z]/) { - return low - } - return "${prefix}${low},${high}${postfix}" - } -} diff --git a/base-plugin/src/main/groovy/com/github/jrubygradle/internal/GemVersionResolver.groovy b/base-plugin/src/main/groovy/com/github/jrubygradle/internal/GemVersionResolver.groovy index 6e112a55..0c4826e8 100644 --- a/base-plugin/src/main/groovy/com/github/jrubygradle/internal/GemVersionResolver.groovy +++ b/base-plugin/src/main/groovy/com/github/jrubygradle/internal/GemVersionResolver.groovy @@ -1,5 +1,6 @@ package com.github.jrubygradle.internal +import com.github.jrubygradle.core.GemVersion import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.artifacts.Configuration @@ -7,6 +8,8 @@ import org.gradle.api.artifacts.DependencyResolveDetails import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging +import static com.github.jrubygradle.core.GemVersion.gemVersionFromGradleRequirement + /** * Resolver to compute gem versions */ @@ -84,7 +87,7 @@ class GemVersionResolver { details.useVersion(next.toString()) } else { - GemVersion next = new GemVersion(details.requested.version) + GemVersion next = gemVersionFromGradleRequirement(details.requested.version) versions[details.requested.name] = next logger.debug("${configuration} nothing collected") logger.debug("${configuration} resolved ${next}") diff --git a/build.gradle b/build.gradle index 83125f82..8cde6799 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ plugins { id 'com.jfrog.bintray' version '1.8.4' apply false id 'org.ajoberstar.github-pages' version '1.2.0' apply false id 'org.ysb33r.cloudci.appveyor.testreporter' version '2.5' apply false + id "io.ratpack.ratpack-java" version "1.6.1" apply false } buildScan { @@ -51,8 +52,8 @@ subprojects { dependencies { compile localGroovy() compile gradleApi() - compile 'org.ysb33r.gradle:grolifant:0.8' - gradleTestRuntime 'org.ysb33r.gradle:grolifant:0.8' + compile 'org.ysb33r.gradle:grolifant:0.12' + gradleTestRuntime 'org.ysb33r.gradle:grolifant:0.12' } codenarc { diff --git a/core-plugin/build.gradle b/core-plugin/build.gradle new file mode 100644 index 00000000..c490aa33 --- /dev/null +++ b/core-plugin/build.gradle @@ -0,0 +1,31 @@ +//plugins { +// id "io.ratpack.ratpack-java" +//} + +//plugins { +// id 'io.spring.dependency-management' version '1.0.6.RELEASE' +//} +// +// +// +//dependencyManagement { +// imports { +// mavenBom 'io.micronaut:micronaut-bom:1.1.2' +// } +//} +dependencies { +// implementation 'io.micronaut:micronaut-http-server-netty' + implementation "io.github.http-builder-ng:http-builder-ng-okhttp:${httpbuilderNgVersion}" + implementation "io.ratpack:ratpack-core:1.6.1" + integrationTestCompile gradleTestKit() + + testCompile(spockVersion) { + exclude module: 'groovy-all' + exclude group: 'org.codehaus.groovy' + } + + integrationTestCompile(spockVersion) { + exclude module: 'groovy-all' + exclude group: 'org.codehaus.groovy' + } +} \ No newline at end of file diff --git a/core-plugin/src/integTest/groovy/com/github/jrubygradle/core/IvyXmlProxyServerIntegrationSpec.groovy b/core-plugin/src/integTest/groovy/com/github/jrubygradle/core/IvyXmlProxyServerIntegrationSpec.groovy new file mode 100644 index 00000000..129d4f48 --- /dev/null +++ b/core-plugin/src/integTest/groovy/com/github/jrubygradle/core/IvyXmlProxyServerIntegrationSpec.groovy @@ -0,0 +1,138 @@ +package com.github.jrubygradle.core + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.Specification + + +class IvyXmlProxyServerIntegrationSpec extends Specification { + + @Rule + TemporaryFolder temporaryFolder + + File projectDir + File buildFile + File testKitDir + + void setup() { + projectDir = new File(temporaryFolder.root, 'test-project') + buildFile = new File(projectDir, 'build.gradle') + testKitDir = new File(temporaryFolder.root, '.gradle') + testKitDir.mkdirs() + projectDir.mkdirs() + } + + void 'Startup a server inside a Gradle project'() { + when: + buildFile.text = ''' + plugins { + id 'com.github.jruby-gradle.core' + } + + repositories { + ruby.gems() + } + + configurations { + something + } + + dependencies { + something 'rubygems:credit_card_validator:1.3.2' + } + ''' + + BuildResult result = GradleRunner.create() + .withProjectDir(projectDir) + .withPluginClasspath() + .withTestKitDir(testKitDir) + .withArguments(['dependencies', '--configuration=something', '-i', '-s']) + .forwardOutput() + .withDebug(true) + .build() + + then: + result.output.contains('rubygems:credit_card_validator:1.3.2') + result.output.contains('rubygems:base_app:[1.0.5,99999.0.0] ->') + } + + void 'Download a collection of GEMs'() { + when: + buildFile.text = ''' + plugins { + id 'com.github.jruby-gradle.core' + } + + repositories { + ruby.gems() + } + + configurations { + something + } + + dependencies { + something 'rubygems:credit_card_validator:1.3.2' + } + + task copyGems(type: Copy) { + from configurations.something + into "${buildDir}/something" + } + ''' + + BuildResult result = GradleRunner.create() + .withProjectDir(projectDir) + .withPluginClasspath() + .withTestKitDir(testKitDir) + .withArguments(['copyGems', '-i', '-s']) + .forwardOutput() + .withDebug(true) + .build() + + then: + new File(projectDir,'build/something/credit_card_validator-1.3.2.gem').exists() + new File(projectDir,'build/something/base_app-1.0.6.gem').exists() + } + + + void 'Download Asciidoctor Reveal.JS GEM and friends'() { + when: + buildFile.text = ''' + plugins { + id 'com.github.jruby-gradle.core' + } + + repositories { + ruby.gems() + } + + configurations { + something + } + + dependencies { + something 'rubygems:asciidoctor-revealjs:2.0.0' + } + + task copyGems(type: Copy) { + from configurations.something + into "${buildDir}/something" + } + ''' + + BuildResult result = GradleRunner.create() + .withProjectDir(projectDir) + .withPluginClasspath() + .withTestKitDir(testKitDir) + .withArguments(['copyGems', '-s']) + .forwardOutput() + .withDebug(true) + .build() + + then: + new File(projectDir,'build/something/asciidoctor-2.0.9.gem').exists() + } +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/ApiException.groovy b/core-plugin/src/main/groovy/com/github/jrubygradle/core/ApiException.groovy new file mode 100644 index 00000000..390029bc --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/ApiException.groovy @@ -0,0 +1,12 @@ +package com.github.jrubygradle.core + +import groovy.transform.CompileStatic +import groovy.transform.InheritConstructors + +/** Throws when there are issues with the RubyGems REST API. + * + */ +@InheritConstructors +@CompileStatic +class ApiException extends Exception { +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/GemDependency.java b/core-plugin/src/main/groovy/com/github/jrubygradle/core/GemDependency.java new file mode 100644 index 00000000..2c9624fd --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/GemDependency.java @@ -0,0 +1,7 @@ +package com.github.jrubygradle.core; + +public interface GemDependency { + String getName(); + + String getRequirements(); +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/GemInfo.java b/core-plugin/src/main/groovy/com/github/jrubygradle/core/GemInfo.java new file mode 100644 index 00000000..1082220f --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/GemInfo.java @@ -0,0 +1,104 @@ +package com.github.jrubygradle.core; + +import java.net.URI; +import java.util.List; + +public interface GemInfo { + + /** GEM name. + * + * @return Name of GEM. Never {@code null}. + */ + String getName(); + + /** GEM version. + * + * @return Version of GEM. + */ + String getVersion(); + + /** GEM platform. + * + * @return Usage platform for GEM. + */ + String getPlatform(); + + /** Required version of Rubygems. + * + * @return Version specification. + */ + String getRubyGemsVersion(); + + /** Required version of Ruby + * + * @return Version specification. + */ + String getRubyVersion(); + + /** GEM authors. + * + * @return List of authors. Can be empty, but not {@code null}. + */ + List getAuthors(); + + /** GEM short description. + * + * @return Summary text. + */ + String getSummary(); + + /** GEM long description. + * + * @return Informative text. + */ + String getDescription(); + + /** GEM hash. + * + * @return SHA + */ + String getSha(); + + /** Project home. + * + * @return URI of project + */ + URI getProjectUri(); + + /** Location to download GEM. + * + * @return URI to GEM. + */ + URI getGemUri(); + + /** Project website. + * + * @return URI to homepage. + */ + URI getHomepageUri(); + + /** Location of documentation. + * + * @return Documentation URI. + */ + URI getDocumentationUri(); + + /** Transitive runtime dependencies. + * + * @return List of dependencies. Can be empty, but never {@code null}. + */ + List getDependencies(); + + /** Transitive development dependencies. + * + * @return List of dependencies. Can be empty, but never {@code null}. + */ + List getDevelopmentDependencies(); + + /** Whether the GEM is still a prerelease version. + * + * @return {@code true} for prerelease + */ + boolean isPrerelease(); + +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/GemVersion.groovy b/core-plugin/src/main/groovy/com/github/jrubygradle/core/GemVersion.groovy new file mode 100644 index 00000000..f940e562 --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/GemVersion.groovy @@ -0,0 +1,355 @@ +package com.github.jrubygradle.core + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import java.util.regex.Pattern + +/** + * With rubygems almost all dependencies will be declared + * via versions ranges and tools like Bundler are very strict on how to + * resolve those versions - i.e. the resolved version needs to obey each given + * constraint. Maven does the same but Gradle and Ivy pick the latest and + * newest version when there are more then one constraint for the same gem - + * which can create problems when using Bundler alongside Gradle. + * + * When converting a GemSpec into a Maven pom.xml the translation of a + * gem version range into a maven version range. typically '~> 1.0' from ruby + * becomes [1.0, 1.99999] on the maven side. so most dependencies from + * gem artifacts will use such version ranges. + * + * to help gradle to be closer to the rubygems world when resolving gem + * artifacts, it needs to calculate intersection between version ranges + * in maven manner. + * + * this class basically represents a maven version range with boundary + * (exclusive vs. inclusive) and its lower and upper bounded version and + * allows to intersect its range with another version range. + * + * it also translate fixed version '1.0' to [1.0, 1.0] or the gradle notation + * 1.2+ to [1.2, 1.99999] or 1.+ to [1.0, 1.99999] following the gemspec-to-pom + * pattern. + * + * @author Christian Meier + */ +@CompileStatic +class GemVersion implements Comparable { + + public static final String MAX_VERSION = '99999' + + private static final String LOW_EX = '(' + private static final String LOW_IN = '[' + private static final String UP_EX = ')' + private static final String UP_IN = ']' + private static final Pattern DOT_PLUS = Pattern.compile('\\.\\+') + private static final Pattern PLUS = Pattern.compile('\\+') + private static final Pattern DIGITS_PLUS = Pattern.compile('[0-9]+\\+') + private static final Pattern HEAD = Pattern.compile('^.*,\\s*') + private static final Pattern TAIL = Pattern.compile(',.*$') + private static final Pattern FIRST = Pattern.compile('^[\\[\\(]') + private static final Pattern LAST = Pattern.compile('[\\]\\)]$') + private static final Pattern ZEROS = Pattern.compile('(\\.0)+$') + + private static final Pattern GREATER_EQUAL = ~/^>=\s*(.+)/ + private static final Pattern GREATER = ~/^>\s*(.+)/ + private static final Pattern EQUAL = ~/^=\s*(.+)/ + private static final Pattern LESS = ~/^<\s*(.+)/ + private static final Pattern LESS_EQUAL = ~/^<=\s*(.+)/ + private static final Pattern TWIDDLE_WAKKA = ~/^~>\s*(.+)/ + + private static final String VERSION_SPLIT = '[.]' + + final String low + final String high + final String prefix = LOW_IN + final String postfix = UP_IN + + /** Create a Gem version instance from a Gradle version requirement. + * + * @param singleRequirement Gradle version string. + * @return GemVersion instance. + */ + static GemVersion gemVersionFromGradleRequirement(String singleRequirement) { + new GemVersion(singleRequirement) + } + + /** Create a Gem version instance from a single GEM version requirement. + * + * @param singleRequirement Single GEM requirement string. + * @return GemVersion instance. + */ + @SuppressWarnings('DuplicateStringLiteral') + static GemVersion gemVersionFromGemRequirement(String singleRequirement) { + if (singleRequirement.matches(GREATER_EQUAL)) { + new GemVersion( + true, + getVersionFromRequirement(singleRequirement, GREATER_EQUAL), + "${MAX_VERSION.toString()}.0.0", + true + ) + } else if (singleRequirement.matches(GREATER)) { + new GemVersion( + false, + getVersionFromRequirement(singleRequirement, GREATER), + "${MAX_VERSION.toString()}.0.0", + true + ) + } else if (singleRequirement.matches(EQUAL)) { + String exact = getVersionFromRequirement(singleRequirement, EQUAL) + new GemVersion( + true, + exact, + exact, + true + ) + } else if (singleRequirement.matches(LESS)) { + new GemVersion( + true, + '0.0.0', + getVersionFromRequirement(singleRequirement, LESS), + false + ) + } else if (singleRequirement.matches(LESS_EQUAL)) { + new GemVersion( + true, + '0.0.0', + getVersionFromRequirement(singleRequirement, LESS_EQUAL), + true + ) + } else if (singleRequirement.matches(TWIDDLE_WAKKA)) { + String base = getVersionFromRequirement(singleRequirement, TWIDDLE_WAKKA) + int adds = 3 - base.split(VERSION_SPLIT).size() + if (adds < 0) { + adds = 0 + } + new GemVersion( + true, + "${base}${'.0' * adds} ", + "${base}${('.' + MAX_VERSION) * adds}", + true + ) + } else { + throw new GemVersionException("Do not not how to process ${singleRequirement} as a version string") + } +} + +boolean isHighInclusive() { + prefix == LOW_IN +} + +boolean isLowInclusive() { + postfix == UP_IN +} + +boolean isOpenHigh() { + high == MAX_VERSION +} + +/** + * since GemVersion is version range with lower bound and upper bound + * this method just calculates the intersection of this version range + * with the given other version range. it also honors whether the boundary + * itself is included or excluded by the respective ranges. + * + * @param The other version range to be intersected with this version range + * @return GemVersion the intersected version range + */ +GemVersion intersect(String otherVersion) { + intersect(gemVersionFromGradleRequirement(otherVersion)) +} + +/** + * since GemVersion is version range with lower bound and upper bound + * this method just calculates the intersection of this version range + * with the given other version range. it also honors whether the boundary + * itself is included or excluded by the respective ranges. + * + * @param The other version range to be intersected with this version range + * @return GemVersion the intersected version range + */ +GemVersion intersect(GemVersion other) { + String newPrefix + String newLow + switch (compare(low, other.low)) { + case -1: + newLow = other.low + newPrefix = other.prefix + break + case 0: + newPrefix = prefix == LOW_EX || other.prefix == LOW_EX ? LOW_EX : LOW_IN + newLow = low + break + case 1: + newLow = low + newPrefix = prefix + } + + String newPostfix + String newHigh + + switch (compare(high, other.high)) { + case 1: + newHigh = other.high + newPostfix = other.postfix + break + case 0: + newPostfix = postfix == UP_EX || other.postfix == UP_EX ? UP_EX : UP_IN + newHigh = high + break + case -1: + newHigh = high + newPostfix = postfix + } + return new GemVersion(newPrefix, newLow, newHigh, newPostfix) +} + +GemVersion union(GemVersion other) { + List pair = [this, other] + GemVersion min = pair.min() + GemVersion max = pair.max() + + new GemVersion(min.prefix, min.low, max.high, max.postfix) +} + +@Override +int compareTo(GemVersion o) { + int loCompare = compare(low, o.low) + if (loCompare) { + return loCompare + } + + if (prefix != o.prefix) { + return prefix == LOW_IN ? -1 : 1 + } + + int hiCompare = compare(high, o.high) + + if (hiCompare) { + return hiCompare + } + + if (postfix != o.postfix) { + return postfix == UP_IN ? 1 : -1 + } + + 0 +} + +/** + * examines the version range on conflict, i.e. lower bound bigger then + * upper bound. + * @return boolean true if lower bound bigger then upper bound + */ +boolean conflict() { + return (compare(low, high) == 1) +} + +/** + * string of the underlying data as maven version range. for prereleased + * versions with ranges like [1.pre, 1.pre] the to range will be replaced + * by the single boundary of the range. + * + * @return String maven version range + */ +String toString() { + if (prefix == LOW_IN && postfix == UP_IN && low == high && low =~ /[a-zA-Z]/) { + return low + } + return "${prefix}${low},${high}${postfix}" +} + +@CompileDynamic +@SuppressWarnings('NoDef') +private static String getVersionFromRequirement(String gemRevision, Pattern matchPattern) { + def matcher = gemRevision =~ matchPattern + matcher[0][1] +} + +private GemVersion(Boolean lowInclusive, String low, String high, Boolean highInclusive) { + this.prefix = lowInclusive ? LOW_IN : LOW_EX + this.low = low + this.high = high + this.postfix = highInclusive ? UP_IN : UP_EX +} + +private GemVersion(String pre, String low, String high, String post) { + this.prefix = pre + this.low = low + this.high = high + this.postfix = post +} + +/** + * converts the given string to a version range with inclusive or + * exclusive boundaries. + * + * @param String gradleVersionPattern + */ +private GemVersion(final String gradleVersionPattern) { +// // workaround a bug in gem proxy which can not handle != operators +// version = version.replace('=', '') + if (gradleVersionPattern.contains('+')) { + low = ZEROS.matcher(PLUS.matcher(DOT_PLUS.matcher(gradleVersionPattern).replaceFirst('.0')).replaceFirst('')).replaceFirst('') + high = DIGITS_PLUS.matcher(DOT_PLUS.matcher(gradleVersionPattern).replaceFirst('.99999')).replaceFirst(MAX_VERSION) + } else if (gradleVersionPattern.contains(LOW_IN) || gradleVersionPattern.contains(LOW_EX) || + gradleVersionPattern.contains(UP_IN) || gradleVersionPattern.contains(UP_EX)) { + prefix = gradleVersionPattern.charAt(0).toString() + postfix = gradleVersionPattern.charAt(gradleVersionPattern.size() - 1).toString() + low = ZEROS.matcher(FIRST.matcher(TAIL.matcher(gradleVersionPattern).replaceFirst('')).replaceFirst('')).replaceFirst('') + high = LAST.matcher(HEAD.matcher(gradleVersionPattern).replaceFirst('')).replaceFirst('') + + if (high == '') { + high = MAX_VERSION + } + } else { + low = gradleVersionPattern + high = gradleVersionPattern + } +} + +/** + * compares two version strings. first it splits the version + * into parts on their ".". if one version has more parts then + * the other, then the number of parts is used for comparison. + * otherwise we find a part which differs between the versions + * and compare them. this last comparision converts the parts to + * integers if both contains only digits. otherwise a lexical + * string comparision is used. + * + * @param String aObject first version + * @param String bObject second version + * @return int -1 if aObject < bObject, 0 if both are equal and 1 if aObject > bObject + */ +private int compare(String aObject, String bObject) { + String[] aDigits = aObject.split(VERSION_SPLIT) + String[] bDigits = bObject.split(VERSION_SPLIT) + int index = -1 + + for (int i = 0; i < aDigits.length && i < bDigits.length; i++) { + if (aDigits[i] != bDigits[i]) { + index = i + break + } + } + + if (index == -1) { + // one contains the other - so look at the length + if (aDigits.length < bDigits.length) { + return -1 + } + if (aDigits.length == bDigits.length) { + return 0 + } + return 1 + } + + if (aDigits[index].isInteger() && bDigits[index].isInteger()) { + // compare them as number + aDigits[index] as int <=> bDigits[index] as int + } else { + // compare them as string + aDigits[index] <=> bDigits[index] + } +} + +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/GemVersionException.groovy b/core-plugin/src/main/groovy/com/github/jrubygradle/core/GemVersionException.groovy new file mode 100644 index 00000000..e09c5d67 --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/GemVersionException.groovy @@ -0,0 +1,12 @@ +package com.github.jrubygradle.core + +import groovy.transform.CompileStatic +import groovy.transform.InheritConstructors + +/** Thrown when GEM version strings cannot be correctly parsed. + * + */ +@InheritConstructors +@CompileStatic +class GemVersionException extends ApiException { +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/IvyXmlProxyServer.java b/core-plugin/src/main/groovy/com/github/jrubygradle/core/IvyXmlProxyServer.java new file mode 100644 index 00000000..d93538c4 --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/IvyXmlProxyServer.java @@ -0,0 +1,10 @@ +package com.github.jrubygradle.core; + +import java.net.URI; +import java.nio.file.Path; + +public interface IvyXmlProxyServer extends Runnable { + URI getBindAddress(); + Path ivyFile(String group, String name, String revision); + void setRefreshDependencies(boolean refresh); +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/JRubyCorePlugin.groovy b/core-plugin/src/main/groovy/com/github/jrubygradle/core/JRubyCorePlugin.groovy new file mode 100644 index 00000000..e6b028df --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/JRubyCorePlugin.groovy @@ -0,0 +1,23 @@ +package com.github.jrubygradle.core + +import groovy.transform.CompileStatic +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.ExtensionAware + +/** Provides only a repository handler extensiosn for looking up rubygem + * metadata. + * + * @since 2.0 + */ +@CompileStatic +class JRubyCorePlugin implements Plugin { + @Override + void apply(Project project) { + ((ExtensionAware) project.repositories).extensions.create( + RepositoryHandlerExtension.NAME, + RepositoryHandlerExtension, + project + ) + } +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/RepositoryHandlerExtension.groovy b/core-plugin/src/main/groovy/com/github/jrubygradle/core/RepositoryHandlerExtension.groovy new file mode 100644 index 00000000..2c3b42ca --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/RepositoryHandlerExtension.groovy @@ -0,0 +1,66 @@ +package com.github.jrubygradle.core + +import com.github.jrubygradle.core.internal.IvyXmlGlobalProxyRegistry +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.gradle.api.Project +import org.gradle.api.artifacts.repositories.ArtifactRepository +import org.gradle.api.artifacts.repositories.IvyArtifactRepository +import org.gradle.util.GradleVersion + +/** Extension which can be added to {@code project.repositories}. + * + * @since 2.0 + */ +@CompileStatic +class RepositoryHandlerExtension { + public static final String NAME = 'ruby' + public static final String DEFAULT_GROUP_NAME = 'rubygems' + + RepositoryHandlerExtension(final Project project) { + this.project = project + this.ivyProxies = new IvyXmlGlobalProxyRegistry((project)) + } + + ArtifactRepository gems() { + bindRepositoryToProxyServer('https://rubygems.org'.toURI(), DEFAULT_GROUP_NAME) + } + + ArtifactRepository gems(Object uri) { + bindRepositoryToProxyServer(project.uri(uri), DEFAULT_GROUP_NAME) + } + + ArtifactRepository gems(String group, Object uri) { + bindRepositoryToProxyServer(project.uri(uri), group) + } + + private ArtifactRepository bindRepositoryToProxyServer( + URI serverUri, + String group + ) { + IvyXmlProxyServer proxy = ivyProxies.registerProxy(serverUri, group) + restrictToGems(createIvyRepo(serverUri, proxy.bindAddress), group) + } + + @CompileDynamic + private IvyArtifactRepository createIvyRepo(URI server, URI bindAddress) { + this.project.repositories.ivy { + artifactPattern "${server}/downloads/[artifact]-[revision].gem" + ivyPattern "${bindAddress}/[organisation]/[module]/[revision]/ivy.xml" + } + } + + @CompileDynamic + private IvyArtifactRepository restrictToGems(IvyArtifactRepository repo, String group) { + if (HAS_CONTENT_FEATURE) { + repo.content { + it.includeGroup group + } + } + repo + } + + private final Project project + private final IvyXmlGlobalProxyRegistry ivyProxies + private static final boolean HAS_CONTENT_FEATURE = GradleVersion.current() >= GradleVersion.version('5.1') +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/RubyGemQueryRestApi.java b/core-plugin/src/main/groovy/com/github/jrubygradle/core/RubyGemQueryRestApi.java new file mode 100644 index 00000000..e6c6bfe9 --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/RubyGemQueryRestApi.java @@ -0,0 +1,40 @@ +package com.github.jrubygradle.core; + +import org.ysb33r.grolifant.api.Version; + +import java.util.List; + +/** Interface for querying a service that confirorms to the RubyGem API. + * + * @since 2.0. + * + * @see https://guides.rubygems.org/rubygems-org-api + */ +public interface RubyGemQueryRestApi { + /** + * Return all published versions for a specific GEM + * + * @param gemName Name of GEM. + * @return List of versions. Can be empty if the GEM does not have any versions. Never {@code null}. + * @throws {@link ApiException} if a networking or parser error occurs. + */ + List allVersions(String gemName) throws ApiException; + + /** + * Return latest published version of GEM. + * + * @param gemName Name of GEM. + * @return Version of GEM + * @throws {@link ApiException} if GEM does not exist. + */ + String latestVersion(String gemName) throws ApiException; + + /** Returns the basic metadata for a GEM. + * + * @param gemName Name of GEM. + * @param version Version of GEM. + * @return Metadata for GEM + * @throws {@link ApiException} if GEM + version does not exist. + */ + GemInfo metadata(String gemName, String version) throws ApiException; +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/DefaultGemDependency.groovy b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/DefaultGemDependency.groovy new file mode 100644 index 00000000..678a16b9 --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/DefaultGemDependency.groovy @@ -0,0 +1,14 @@ +package com.github.jrubygradle.core.internal + +import com.github.jrubygradle.core.GemDependency +import groovy.transform.CompileStatic + +/** Defining a GEM dependency. + * + * @since 2.0 + */ +@CompileStatic +class DefaultGemDependency implements GemDependency { + String name + String requirements +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/DefaultGemInfo.groovy b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/DefaultGemInfo.groovy new file mode 100644 index 00000000..fa5b6cde --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/DefaultGemInfo.groovy @@ -0,0 +1,33 @@ +package com.github.jrubygradle.core.internal + +import com.github.jrubygradle.core.GemDependency +import com.github.jrubygradle.core.GemInfo +import groovy.transform.CompileStatic + +/** An implementation of GEM metadata. + * + * @since 2.0 + */ +@CompileStatic +class DefaultGemInfo implements GemInfo { + String name + String version + String platform + String summary + String description + String sha + String rubyVersion + String rubyGemsVersion + + boolean prerelease + + URI projectUri + URI gemUri + URI homepageUri + URI documentationUri + + List authors = [] + + List dependencies = [] + List developmentDependencies = [] +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/DefaultRubyGemRestApi.groovy b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/DefaultRubyGemRestApi.groovy new file mode 100644 index 00000000..0d7af8d2 --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/DefaultRubyGemRestApi.groovy @@ -0,0 +1,134 @@ +package com.github.jrubygradle.core.internal + +import com.github.jrubygradle.core.ApiException +import com.github.jrubygradle.core.GemInfo +import com.github.jrubygradle.core.RubyGemQueryRestApi +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovyx.net.http.HttpBuilder +import groovyx.net.http.HttpException +import okhttp3.OkHttpClient + +import static groovyx.net.http.ContentTypes.JSON +import static groovyx.net.http.NativeHandlers.Parsers.json +import static groovyx.net.http.OkHttpBuilder.configure + +/** + * @since 2.0 + */ +@CompileStatic +class DefaultRubyGemRestApi implements RubyGemQueryRestApi { + + DefaultRubyGemRestApi(final String serverUri) { + this.httpBuilder = getHttpBuilder(serverUri.toURI()) + } + + DefaultRubyGemRestApi(final URI serverUri) { + this.httpBuilder = getHttpBuilder(serverUri) + } + + @Override + @SuppressWarnings('CatchThrowable') + List allVersions(String gemName) { + try { + extractVersions(getData(V1, "versions/${gemName}")) + } catch (Throwable e) { + throw new ApiException("Count not retrieve list of versions for ${gemName}", e) + } + } + + @Override + @SuppressWarnings('CatchThrowable') + String latestVersion(String gemName) { + String version + try { + version = extractVersion(getData(V1, "versions/${gemName}/latest")) + } catch (Throwable e) { + throw new ApiException("Failed to retrieve latest version of ${gemName}", e) + } + if (version == 'unknown') { + throw new ApiException("Cound not retrieve latest version of ${gemName}. Maybe it does not exist") + } + version + } + + @Override + @SuppressWarnings('CatchThrowable') + GemInfo metadata(String gemName, String gemVersion) { + try { + extractMetadata(getData(V2, "rubygems/${gemName}/versions/${gemVersion}")) + } catch (HttpException e) { + throw new ApiException(":${gemName}:${gemVersion} not found", e) + } catch (Throwable e) { + throw new ApiException("Could not obtain information for :${gemName}:${gemVersion}.", e) + } + } + + private Object getData(final String useApiVersion, String relativePath) { + httpBuilder.get { + request.uri.path = "/${useApiVersion}/${relativePath}.json" + response.parser(JSON[0]) { config, resp -> + json(config, resp) + } + } + } + + private static HttpBuilder getHttpBuilder(URI uri) { + configure { + request.uri = uri + client.clientCustomizer { OkHttpClient.Builder builder -> + builder.followRedirects(true) + builder.followSslRedirects(true) + } + } + } + + @CompileDynamic + private String extractVersion(Object jsonParser) { + jsonParser.version + } + + @CompileDynamic + private List extractVersions(Object jsonParser) { + jsonParser*.number + } + + @CompileDynamic + private GemInfo extractMetadata(Object jsonParser) { + DefaultGemInfo metadata = new DefaultGemInfo( + name: jsonParser.name, + version: jsonParser.version, + platform: jsonParser.platform, + description: jsonParser.description, + summary: jsonParser.summary, + sha: jsonParser.sha, + rubyVersion: jsonParser.ruby_version, + rubyGemsVersion: jsonParser.rubygems_version, + projectUri: jsonParser.project_uri?.toURI(), + gemUri: jsonParser.gem_uri?.toURI(), + homepageUri: jsonParser.homepage_uri?.toURI(), + documentationUri: jsonParser.documentation_uri?.toURI(), + authors: ((String) jsonParser.authors).split(', ').toList() ?: [], + prerelease: jsonParser.prerelease + // licenses arrayList + ) + + if (jsonParser.dependencies?.runtime) { + metadata.dependencies.addAll(jsonParser.dependencies.runtime.collect { + new DefaultGemDependency(name: it.name, requirements: it.requirements) + }) + } + + if (jsonParser.dependencies?.development) { + metadata.developmentDependencies.addAll(jsonParser.dependencies.development.collect { + new DefaultGemDependency(name: it.name, requirements: it.requirements) + }) + } + + metadata + } + + private final HttpBuilder httpBuilder + static private final String V1 = 'api/v1' + static private final String V2 = 'api/v2' +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/GemToIvy.groovy b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/GemToIvy.groovy new file mode 100644 index 00000000..12347800 --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/GemToIvy.groovy @@ -0,0 +1,92 @@ +package com.github.jrubygradle.core.internal + +import com.github.jrubygradle.core.GemInfo +import com.github.jrubygradle.core.GemVersion +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.xml.MarkupBuilder + +import static com.github.jrubygradle.core.GemVersion.gemVersionFromGemRequirement + +/** Converts from Gem metadata to Ivy metadata. + * + * @since 2.0 + */ +@CompileStatic +class GemToIvy { + + GemToIvy(URI serverUri) { + this.serverUri = serverUri.toString() + } + + GemToIvy(URI serverUri, String group) { + this.serverUri = serverUri.toString() + this.org = group + } + + @CompileDynamic + @SuppressWarnings('NoDef') + Writer writeTo(Writer writer, GemInfo gem) { + def xml = new MarkupBuilder(writer) + + xml.'ivy-module'( + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:noNamespaceSchemaLocation': 'http://ant.apache.org/ivy/schemas/ivy.xsd', + version: '2.0' + ) { + info(organisation: this.org, module: gem.name, revision: gem.version /*, publication: */) { + if (gem.description || gem.homepageUri) { + if (gem.homepageUri) { + description(homepage: gem.homepageUri) { + gem.description ?: '' + } + } else { + description { + gem.description ?: '' + } + } + } + /* 1..n */ + } + + publications { + artifact(type: 'gem') + } + + if (gem.dependencies) { + dependencies { + gem.dependencies.each { dep -> + dependency(org: this.org, name: dep.name, rev: translateGemRevisionRequirements(dep.requirements)) + } + } + } + } + + writer + } + + String write(GemInfo gem) { + StringWriter writer = new StringWriter() + writeTo(writer, gem) + writer.toString() + } + + private String translateGemRevisionRequirements(String requirements) { + List reqs = requirements.split(/,\s*/).collect { String it -> + gemVersionFromGemRequirement(it) + } + + if (reqs.size() > 1) { + ivyFormatFromRange(reqs.min().union(reqs.max())) + } else { + ivyFormatFromRange(reqs[0]) + } + } + + private String ivyFormatFromRange(GemVersion range) { + "${range.lowInclusive ? '[' : ']'}${range.low},${range.high}${range.highInclusive ? ']' : '['}" + } + + private final String serverUri + private final String org = 'rubygems' +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/IvyUtils.groovy b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/IvyUtils.groovy new file mode 100644 index 00000000..3ca657c6 --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/IvyUtils.groovy @@ -0,0 +1,34 @@ +package com.github.jrubygradle.core.internal + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.xml.MarkupBuilder + +/** Utilities for dealing with Ivy formats. + * + */ +@CompileStatic +class IvyUtils { + + /** Converts a list of revisions to an HTML directory listing. + * + * @param revisions List of GEM revisions. + * @return HTML-based directory listing which can be use to serve up + * something in the way that Gradle would expect it to be. + */ + @CompileDynamic + static String revisionsAsHtmlDirectoryListing(List revisions) { + StringWriter out = new StringWriter() + new MarkupBuilder(out).html { + head() + body { + revisions.each { rev -> + pre { + a(href: "${rev}/", rel: 'nofollow') + } + } + } + } + out.toString() + } +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/IvyXmlGlobalProxyRegistry.groovy b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/IvyXmlGlobalProxyRegistry.groovy new file mode 100644 index 00000000..b2ef5a4c --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/IvyXmlGlobalProxyRegistry.groovy @@ -0,0 +1,57 @@ +package com.github.jrubygradle.core.internal + +import com.github.jrubygradle.core.IvyXmlProxyServer +import groovy.transform.CompileStatic +import org.gradle.api.Project + +import java.security.MessageDigest +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap + +/** Allow only one version of the proxy to run + * + */ +@CompileStatic +class IvyXmlGlobalProxyRegistry { + + IvyXmlGlobalProxyRegistry(Project project) { + rootCacheDir = new File(project.gradle.gradleUserHomeDir, 'rubygems-ivyxml-cache') + refresh = project.gradle.startParameter.refreshDependencies + } + + IvyXmlProxyServer registerProxy(URI remoteURI, String group) { + IvyXmlProxyServer proxy = getOrCreateServer(remoteURI, group, new File(rootCacheDir, uriHash(remoteURI))) + proxy.refreshDependencies = refresh + proxy + } + + private String uriHash(URI remoteURI) { + MessageDigest.getInstance('SHA-1').digest(remoteURI.toString().bytes).encodeHex().toString() + } + + @SuppressWarnings('ClosureAsLastMethodParameter') + static private IvyXmlProxyServer getOrCreateServer( + URI uri, + String group, + File cacheDir + ) { + SERVER_MAP.computeIfAbsent(uri, { + IvyXmlProxyServer server = createProxyServer(uri, group, cacheDir) + server.run() + server + }) + } + + static private IvyXmlProxyServer createProxyServer( + URI uri, + String group, + File cacheDir + ) { + new IvyXmlRatpackProxyServer(cacheDir, uri, group) + } + + static private final ConcurrentMap SERVER_MAP = new ConcurrentHashMap<>() + + private final boolean refresh + private final File rootCacheDir +} diff --git a/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/IvyXmlRatpackProxyServer.groovy b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/IvyXmlRatpackProxyServer.groovy new file mode 100644 index 00000000..75ec3155 --- /dev/null +++ b/core-plugin/src/main/groovy/com/github/jrubygradle/core/internal/IvyXmlRatpackProxyServer.groovy @@ -0,0 +1,136 @@ +package com.github.jrubygradle.core.internal + +import com.github.jrubygradle.core.ApiException +import com.github.jrubygradle.core.GemInfo +import com.github.jrubygradle.core.GemVersion +import com.github.jrubygradle.core.IvyXmlProxyServer +import com.github.jrubygradle.core.RubyGemQueryRestApi +import groovy.transform.CompileStatic +import groovy.transform.Synchronized +import groovy.util.logging.Slf4j +import org.ysb33r.grolifant.api.ExclusiveFileAccess +import ratpack.handling.RequestLogger +import ratpack.server.RatpackServer + +import ratpack.server.ServerConfig + +import java.nio.file.Files +import java.nio.file.Path + +import static com.github.jrubygradle.core.GemVersion.gemVersionFromGradleRequirement +import static com.github.jrubygradle.core.internal.IvyUtils.revisionsAsHtmlDirectoryListing +import static java.nio.file.Files.move +import static java.nio.file.StandardCopyOption.ATOMIC_MOVE +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING + +/** Uses Ratpack to run a small proxy server inside Gradle to proxy Rubygems.org + * as if it is local Ivy server with remote artifacts. + * + * @since 2.0 + */ +@CompileStatic +@Slf4j +class IvyXmlRatpackProxyServer implements IvyXmlProxyServer { + IvyXmlRatpackProxyServer(File cache, URI serverUri, String group) { + localCachePath = cache + gemToIvy = new GemToIvy(serverUri) + api = new DefaultRubyGemRestApi(serverUri) + this.group = group + } + + void debug(String text) { + log.debug(text) + } + + void debug(String text, Object context) { + log.debug(text, context) + } + + @Override + void setRefreshDependencies(boolean refresh) { + refreshDependencies = refresh ? 1 : 0 + } + + @Override + URI getBindAddress() { + "http://localhost:${server.bindPort}".toURI() + } + + @Override + @SuppressWarnings('DuplicateStringLiteral') + void run() { + server = RatpackServer.start { + it.serverConfig( + ServerConfig.embedded() + .publicAddress('http://localhost'.toURI()) + .port(0) + .baseDir(localCachePath) + ).handlers { chain -> + chain.all(RequestLogger.ncsa()) + chain.get("${this.group}/:module/:revision/ivy.xml") { ctx -> + String name = ctx.allPathTokens['module'] + String revision = getGemQueryRevisionFromIvy(name, ctx.allPathTokens['revision']) + Path ivyXml = ivyFile(group, name, revision) + debug "Requested ${group}:${name}:${ctx.allPathTokens['revision']} translated to GEM with version ${revision}" + if (Files.notExists(ivyXml) || refreshDependencies) { + try { + createIvyXml(ivyXml, name, revision) + ctx.response.contentType('text/xml').sendFile(ivyXml) + } catch (ApiException e) { + debug(e.message, e) + ctx.clientError(404) + } + } else { + ctx.response.contentType('text/xml').sendFile(ivyXml) + } + + debug "Cached file is ${ivyXml.toAbsolutePath()}" + debug "Cached file contains ${ivyXml.text}" + }.get(':group/:module') { ctx -> + String grp = ctx.allPathTokens['group'] + String name = ctx.allPathTokens['module'] + debug "Request to find all versions for ${grp}:${name}" + List versions = api.allVersions(name) + debug "Got versions ${versions.join(', ')}" + ctx.response.contentType('text/html').send(revisionsAsHtmlDirectoryListing(versions)) + }.get { ctx -> + ctx.clientError(403) + } + } + } + debug "Ivy.xml proxy server starting on ${bindAddress}" + server + } + + @SuppressWarnings('UnusedMethodParameter') + Path ivyFile(String group, String name, String revision) { + new File(localCachePath, "${name}/${revision}/ivy.xml").toPath() + } + + @Synchronized + @SuppressWarnings('BuilderMethodWithSideEffects') + private void createIvyXml(Path ivyXml, String name, String revision) { + ExclusiveFileAccess efa = new ExclusiveFileAccess(120000, 20) + efa.access(ivyXml.toFile()) { + GemInfo gemInfo = api.metadata(name, revision) + ivyXml.parent.toFile().mkdirs() + Path tmp = ivyXml.resolveSibling("${ivyXml.toFile().name}.tmp") + tmp.withWriter { writer -> + gemToIvy.writeTo(writer, gemInfo) + } + move(tmp, ivyXml, ATOMIC_MOVE, REPLACE_EXISTING) + } + } + + private String getGemQueryRevisionFromIvy(String gemName, String revisionPattern) { + GemVersion version = gemVersionFromGradleRequirement(revisionPattern) + version.openHigh ? api.latestVersion(gemName) : version.high + } + + private volatile int refreshDependencies = 0 + private RatpackServer server + private final File localCachePath + private final GemToIvy gemToIvy + private final RubyGemQueryRestApi api + private final String group +} diff --git a/core-plugin/src/main/resources/META-INF/gradle-plugins/com.github.jruby-gradle.core.properties b/core-plugin/src/main/resources/META-INF/gradle-plugins/com.github.jruby-gradle.core.properties new file mode 100644 index 00000000..6b8bf655 --- /dev/null +++ b/core-plugin/src/main/resources/META-INF/gradle-plugins/com.github.jruby-gradle.core.properties @@ -0,0 +1 @@ +implementation-class=com.github.jrubygradle.core.JRubyCorePlugin diff --git a/core-plugin/src/test/groovy/com/github/jrubygradle/core/IvyXmlProxyServerSpec.groovy b/core-plugin/src/test/groovy/com/github/jrubygradle/core/IvyXmlProxyServerSpec.groovy new file mode 100644 index 00000000..6f64c744 --- /dev/null +++ b/core-plugin/src/test/groovy/com/github/jrubygradle/core/IvyXmlProxyServerSpec.groovy @@ -0,0 +1,38 @@ +package com.github.jrubygradle.core + +import com.github.jrubygradle.core.internal.IvyXmlRatpackProxyServer +import groovyx.net.http.HttpBuilder +import groovyx.net.http.OkHttpBuilder +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.Specification + +class IvyXmlProxyServerSpec extends Specification { + + public static final String CREDIT_CARD = RubyGemQueryRestApiSpec.CREDIT_CARD + public static final String TEST_IVY_PATH = "${CREDIT_CARD}/1.3.2/ivy.xml" + + IvyXmlProxyServer server + HttpBuilder httpBuilder + + @Rule + TemporaryFolder projectRoot + + void setup() { + server = new IvyXmlRatpackProxyServer(projectRoot.root, 'https://rubygems.org'.toURI(), 'rubygems') + server.run() + httpBuilder = OkHttpBuilder.configure { + request.uri = server.bindAddress + } + } + + void 'Build an Ivy Xml file from a query to Rubygems'() { + when: 'I query the local proxy server' + httpBuilder.get { + request.uri.path = "/rubygems/${TEST_IVY_PATH}" + } + + then: 'The Ivy file should be generated and cached locally' + new File(projectRoot.root,TEST_IVY_PATH) + } +} \ No newline at end of file diff --git a/core-plugin/src/test/groovy/com/github/jrubygradle/core/RubyGemQueryRestApiSpec.groovy b/core-plugin/src/test/groovy/com/github/jrubygradle/core/RubyGemQueryRestApiSpec.groovy new file mode 100644 index 00000000..0d131733 --- /dev/null +++ b/core-plugin/src/test/groovy/com/github/jrubygradle/core/RubyGemQueryRestApiSpec.groovy @@ -0,0 +1,81 @@ +package com.github.jrubygradle.core + +import com.github.jrubygradle.core.internal.DefaultRubyGemRestApi +import org.ysb33r.grolifant.api.Version +import spock.lang.Specification + +class RubyGemQueryRestApiSpec extends Specification { + + public static final String NON_EXISTANT_GEM = 'fasdfasdfasdasdasdfads' + public static final String CREDIT_CARD = 'credit_card_validator' + + RubyGemQueryRestApi rubygems + + void setup() { + rubygems = api + } + + void 'List of versions for non-existant GEM throws exception'() { + when: + rubygems.allVersions(NON_EXISTANT_GEM) + + then: + thrown(ApiException) + } + + void 'Latest version of non-existant GEM throws exception'() { + when: + rubygems.latestVersion(NON_EXISTANT_GEM) + + then: + thrown(ApiException) + } + + void 'GEM info on non-existant GEM throws exception'() { + when: + rubygems.metadata(NON_EXISTANT_GEM, '1.2.3') + + then: + thrown(ApiException) + } + + void 'Extract versions for an existing GEM'() { + when: + def versions = rubygems.allVersions(CREDIT_CARD) + + then: + versions.size() > 1 + + when: + String creditCardValidatorVersion = versions.max(new Comparator() { + @Override + int compare(String o1, String o2) { + Version.of(o1) <=> Version.of(o2) + } + }) + def ccv = rubygems.latestVersion(CREDIT_CARD) + + then: + ccv == creditCardValidatorVersion + } + + void 'Extract metadata for an existing GEM'() { + when: + String testVersion = '1.3.2' + def metadata = rubygems.metadata(CREDIT_CARD, testVersion) + + then: + verifyAll { + metadata.name == CREDIT_CARD + metadata.version == testVersion + metadata.authors.size() == 9 + metadata.summary != null + metadata.sha != null + metadata.platform == 'ruby' + } + } + + private RubyGemQueryRestApi getApi() { + new DefaultRubyGemRestApi('https://rubygems.org') + } +} \ No newline at end of file diff --git a/core-plugin/src/test/groovy/com/github/jrubygradle/core/internal/GemToIvySpec.groovy b/core-plugin/src/test/groovy/com/github/jrubygradle/core/internal/GemToIvySpec.groovy new file mode 100644 index 00000000..44007308 --- /dev/null +++ b/core-plugin/src/test/groovy/com/github/jrubygradle/core/internal/GemToIvySpec.groovy @@ -0,0 +1,22 @@ +package com.github.jrubygradle.core.internal + + +import spock.lang.Specification + +class GemToIvySpec extends Specification { + + void 'Write Ivy Xml'() { + given: + def gem = new DefaultGemInfo( + name: 'foo_module', + version: '1.2.3' + ) + def gemToIvy = new GemToIvy('https://foo'.toURI()) + + when: + def result = gemToIvy.write(gem) + + then: + result.contains("info organisation='rubygems' module='foo_module' revision='1.2.3'") + } +} \ No newline at end of file diff --git a/base-plugin/src/test/groovy/com/github/jrubygradle/internal/GemVersionSpec.groovy b/core-plugin/src/test/groovy/com/github/jrubygradle/core/internal/GemVersionSpec.groovy similarity index 63% rename from base-plugin/src/test/groovy/com/github/jrubygradle/internal/GemVersionSpec.groovy rename to core-plugin/src/test/groovy/com/github/jrubygradle/core/internal/GemVersionSpec.groovy index c4c94656..d8150cdf 100644 --- a/base-plugin/src/test/groovy/com/github/jrubygradle/internal/GemVersionSpec.groovy +++ b/core-plugin/src/test/groovy/com/github/jrubygradle/core/internal/GemVersionSpec.groovy @@ -1,11 +1,14 @@ -package com.github.jrubygradle.internal +package com.github.jrubygradle.core.internal +import com.github.jrubygradle.core.GemVersion import spock.lang.Specification +import static com.github.jrubygradle.core.GemVersion.gemVersionFromGradleRequirement + class GemVersionSpec extends Specification { void "parses single version"() { given: - GemVersion subject = new GemVersion('1.2.3') + GemVersion subject = gemVersionFromGradleRequirement('1.2.3') expect: subject.toString() == '[1.2.3,1.2.3]' @@ -13,7 +16,7 @@ class GemVersionSpec extends Specification { void "parses single prerelease version"() { given: - GemVersion subject = new GemVersion('1.2.pre') + GemVersion subject = gemVersionFromGradleRequirement('1.2.pre') expect: subject.toString() == '1.2.pre' @@ -21,7 +24,7 @@ class GemVersionSpec extends Specification { void "parses gradle semantic version first sample"() { given: - GemVersion subject = new GemVersion('1.2.3+') + GemVersion subject = gemVersionFromGradleRequirement('1.2.3+') expect: subject.toString() == '[1.2.3,1.2.99999]' @@ -29,7 +32,7 @@ class GemVersionSpec extends Specification { void "parses gradle semantic version second sample"() { given: - GemVersion subject = new GemVersion('1.2.+') + GemVersion subject = gemVersionFromGradleRequirement('1.2.+') expect: subject.toString() == '[1.2,1.2.99999]' @@ -37,7 +40,7 @@ class GemVersionSpec extends Specification { void "parses maven open version range"() { given: - GemVersion subject = new GemVersion('[1.2.0,)') + GemVersion subject = gemVersionFromGradleRequirement('[1.2.0,)') expect: subject.toString() == '[1.2,99999)' @@ -45,7 +48,7 @@ class GemVersionSpec extends Specification { void "parses maven version range first sample"() { given: - GemVersion subject = new GemVersion('(1.2.0.0, 1.2.4)') + GemVersion subject = gemVersionFromGradleRequirement('(1.2.0.0, 1.2.4)') expect: subject.toString() == '(1.2,1.2.4)' @@ -53,7 +56,7 @@ class GemVersionSpec extends Specification { void "parses maven version range second sample"() { given: - GemVersion subject = new GemVersion('(1.2.0, 1.2.4]') + GemVersion subject = gemVersionFromGradleRequirement('(1.2.0, 1.2.4]') expect: subject.toString() == '(1.2,1.2.4]' @@ -61,7 +64,7 @@ class GemVersionSpec extends Specification { void "parses maven version range third sample"() { given: - GemVersion subject = new GemVersion('[1.2.0, 1.2.4)') + GemVersion subject = gemVersionFromGradleRequirement('[1.2.0, 1.2.4)') expect: subject.toString() == '[1.2,1.2.4)' @@ -69,7 +72,7 @@ class GemVersionSpec extends Specification { void "parses maven version range forth sample"() { given: - GemVersion subject = new GemVersion('[1.2.1, 1.2.4]') + GemVersion subject = gemVersionFromGradleRequirement('[1.2.1, 1.2.4]') expect: subject.toString() == '[1.2.1,1.2.4]' @@ -77,7 +80,7 @@ class GemVersionSpec extends Specification { void "parses maven version range trailing zeros"() { given: - GemVersion subject = new GemVersion('[1.2.1.0.0.0, 1.2.4]') + GemVersion subject = gemVersionFromGradleRequirement('[1.2.1.0.0.0, 1.2.4]') expect: subject.toString() == '[1.2.1,1.2.4]' @@ -85,7 +88,7 @@ class GemVersionSpec extends Specification { void "parses maven version range trailing zeros as prereleased version"() { given: - GemVersion subject = new GemVersion('[1.2.1.0.pre.0, 1.2.4]') + GemVersion subject = gemVersionFromGradleRequirement('[1.2.1.0.pre.0, 1.2.4]') expect: subject.toString() == '[1.2.1.0.pre,1.2.4]' @@ -93,7 +96,7 @@ class GemVersionSpec extends Specification { void "intersects two versions first sample"() { given: - GemVersion subject = new GemVersion('[1.2.1, 1.2.4]') + GemVersion subject = gemVersionFromGradleRequirement('[1.2.1, 1.2.4]') expect: subject.intersect('(1.2.1, 1.2.4)').toString() == '(1.2.1,1.2.4)' @@ -101,7 +104,7 @@ class GemVersionSpec extends Specification { void "intersects two versions second sample"() { given: - GemVersion subject = new GemVersion('[1.2.0, 1.2.4]') + GemVersion subject = gemVersionFromGradleRequirement('[1.2.0, 1.2.4]') expect: subject.intersect('(1.2.1, 1.2.3)').toString() == '(1.2.1,1.2.3)' @@ -109,7 +112,7 @@ class GemVersionSpec extends Specification { void "intersects two versions third sample"() { given: - GemVersion subject = new GemVersion('[1.2.0, 1.2.4]') + GemVersion subject = gemVersionFromGradleRequirement('[1.2.0, 1.2.4]') expect: subject.intersect('[1.2.1, 1.2.3]').toString() == '[1.2.1,1.2.3]' @@ -117,7 +120,7 @@ class GemVersionSpec extends Specification { void "intersects two versions first sample reversed"() { given: - GemVersion subject = new GemVersion('(1.2.0, 1.2.4)') + GemVersion subject = gemVersionFromGradleRequirement('(1.2.0, 1.2.4)') expect: subject.intersect('[1.2.0, 1.2.4]').toString() == '(1.2,1.2.4)' @@ -125,7 +128,7 @@ class GemVersionSpec extends Specification { void "intersects two versions second sample reversed"() { given: - GemVersion subject = new GemVersion('(1.2.1, 1.2.3)') + GemVersion subject = gemVersionFromGradleRequirement('(1.2.1, 1.2.3)') expect: subject.intersect('[1.2.0, 1.2.4]').toString() == '(1.2.1,1.2.3)' @@ -133,7 +136,7 @@ class GemVersionSpec extends Specification { void "intersects two versions third sample reversed"() { given: - GemVersion subject = new GemVersion('[1.2.1, 1.2.3]') + GemVersion subject = gemVersionFromGradleRequirement('[1.2.1, 1.2.3]') expect: subject.intersect('[1.2.0, 1.2.4]').toString() == '[1.2.1,1.2.3]' @@ -141,7 +144,7 @@ class GemVersionSpec extends Specification { void "intersects two versions with non lexical ordering"() { given: - GemVersion subject = new GemVersion('[1.2.10, 1.2.14]') + GemVersion subject = gemVersionFromGradleRequirement('[1.2.10, 1.2.14]') expect: subject.intersect('[1.2.2, 1.10.14]').toString() == '[1.2.10,1.2.14]' @@ -149,7 +152,7 @@ class GemVersionSpec extends Specification { void "intersects two versions with different length"() { given: - GemVersion subject = new GemVersion('[1.2, 1.2.14]') + GemVersion subject = gemVersionFromGradleRequirement('[1.2, 1.2.14]') expect: subject.intersect('[1.2.2, 1.3]').toString() == '[1.2.2,1.2.14]' @@ -157,7 +160,7 @@ class GemVersionSpec extends Specification { void "intersects two versions special one"() { given: - GemVersion subject = new GemVersion('[0.9.0,0.9.99999]') + GemVersion subject = gemVersionFromGradleRequirement('[0.9.0,0.9.99999]') expect: subject.intersect('[0,)').toString() == '[0.9,0.9.99999]' @@ -165,7 +168,7 @@ class GemVersionSpec extends Specification { void "intersects two versions with special full range"() { given: - GemVersion subject = new GemVersion('[0,)') + GemVersion subject = gemVersionFromGradleRequirement('[0,)') expect: subject.intersect('[0.9.0,0.9.99999]').toString() == '[0.9,0.9.99999]' @@ -173,15 +176,15 @@ class GemVersionSpec extends Specification { void "intersects two versions with workaround due to upstream bug"() { given: - GemVersion subject = new GemVersion('(=2.5.1.1,99999)') + GemVersion subject = gemVersionFromGradleRequirement('(2.5.1.1,99999)') expect: - subject.intersect('(=2.5.1.1,)').toString() == '(2.5.1.1,99999)' + subject.intersect('(2.5.1.1,)').toString() == '(2.5.1.1,99999)' } void "intersects with conflict"() { given: - GemVersion subject = new GemVersion('[1.2.1, 1.2.3]') + GemVersion subject = gemVersionFromGradleRequirement('[1.2.1, 1.2.3]') expect: subject.intersect('[1.2.4, 1.2.4]').conflict() == true @@ -189,23 +192,15 @@ class GemVersionSpec extends Specification { void "finds no conflicts in non-integer version ranges"() { given: - GemVersion subject = new GemVersion('[1.2.bar, 1.2.foo]') + GemVersion subject = gemVersionFromGradleRequirement('[1.2.bar, 1.2.foo]') expect: !subject.conflict() } - void "finds conflicts in non-integer version ranges"() { - given: - GemVersion subject = new GemVersion('[1.2.foo, 1.2.bar]') - - expect: - subject.conflict() - } - void "does not throw an exception for a '+' version"() { when: - new GemVersion('+').conflict() + gemVersionFromGradleRequirement('+').conflict() then: notThrown(Exception) diff --git a/gradle.properties b/gradle.properties index 5319722b..87121730 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,5 @@ sourceCompatibility=1.8 jrubyVersion=9.2.7.0 jettyVersion=9.2.12.v20150709 bcprovVersion=1.46 +httpbuilderNgVersion=1.0.3 torqueboxProxy=http://rubygems-proxy.torquebox.org/releases diff --git a/gradle/integration-tests.gradle b/gradle/integration-tests.gradle index 3e80d74c..4d181c9e 100644 --- a/gradle/integration-tests.gradle +++ b/gradle/integration-tests.gradle @@ -38,6 +38,7 @@ task copyIntegrationTestGems (type:Copy) { task integrationTest(type: Test, dependsOn: jar) { testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath - dependsOn copyIntegrationTestGems, test + dependsOn copyIntegrationTestGems + mustRunAfter test } check.dependsOn integrationTest diff --git a/settings.gradle b/settings.gradle index 0b29bc2a..2d7340a3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ -['base','war','jar'].each { mod -> +['base','war','jar', 'core'].each { mod -> def fName = "jruby-gradle${(mod == 'base') ? '' : ('-' + mod)}-plugin" include fName