├── .github └── workflows │ └── nebula.yml ├── .gitignore ├── .java-version ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle ├── gradle.lockfile ├── gradle.properties ├── gradle ├── gradle-daemon-jvm.properties ├── idea-codestyle.xml ├── idea-copyright.xml ├── idea-inspections.xml ├── idea.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── integTest ├── groovy │ └── nebula │ │ └── plugin │ │ └── resolutionrules │ │ ├── AbstractAlignAndMigrateSpec.groovy │ │ ├── AbstractAlignRulesSpec.groovy │ │ ├── AbstractIntegrationTestKitSpec.groovy │ │ ├── AbstractRulesWithSpringBootPluginSpec.groovy │ │ ├── AlignAndLockWithDowngradedTransitiveDependenciesSpec.groovy │ │ ├── AlignAndMigrateViaReplacementSpec.groovy │ │ ├── AlignAndMigrateViaSubstitutionSpec.groovy │ │ ├── AlignAndSubstituteRulesSpec.groovy │ │ ├── AlignAndSubstituteRulesWithSpringBoot1xPluginSpec.groovy │ │ ├── AlignAndSubstituteRulesWithSpringBoot2xPluginAndManagedDepsSpec.groovy │ │ ├── AlignAndSubstituteRulesWithSpringBoot2xPluginWithoutManagedDepsSpec.groovy │ │ ├── AlignRulesBasicSpec.groovy │ │ ├── AlignRulesBasicWithCoreSpec.groovy │ │ ├── AlignRulesDirectDependenciesSpec.groovy │ │ ├── AlignRulesForceSpec.groovy │ │ ├── AlignRulesForceStrictlyWithSubstitutionSpec.groovy │ │ ├── AlignRulesMultiprojectSpec.groovy │ │ ├── AlignRulesPluginInteractionSpec.groovy │ │ ├── AlignRulesTransitiveDependenciesSpec.groovy │ │ ├── AlignRulesVersionMatchSpec.groovy │ │ ├── AlignRulesVersionSuffixesSpec.groovy │ │ ├── IgnoredConfigurationsWithRulesSpec.groovy │ │ ├── ResolutionRulesPluginSpec.groovy │ │ ├── SubstituteRulesSpec.groovy │ │ └── SubstituteRulesWithRangesSpec.groovy └── resources │ └── logback.xml ├── main └── kotlin │ └── nebula │ └── plugin │ └── resolutionrules │ ├── alignRule.kt │ ├── configurations.kt │ ├── extensions.kt │ ├── json.kt │ ├── plugin.kt │ └── rules.kt └── test └── groovy └── nebula └── plugin └── resolutionrules ├── AlignRuleMatcherTest.groovy ├── NebulaResolutionRulesExtensionTest.groovy └── RulesTest.groovy /.github/workflows/nebula.yml: -------------------------------------------------------------------------------- 1 | name: Nebula Build 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | tags: 7 | - v*.*.* 8 | - v*.*.*-rc.* 9 | pull_request: 10 | 11 | jobs: 12 | validation: 13 | name: "Gradle Wrapper Validation" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: gradle/wrapper-validation-action@v1 18 | buildmultijdk: 19 | if: (!startsWith(github.ref, 'refs/tags/v')) 20 | needs: validation 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | # test against latest update of some major Java version(s), as well as specific LTS version(s) 25 | java: [8, 17, 21] 26 | name: Gradle Build without Publish 27 | steps: 28 | - uses: actions/checkout@v1 29 | - name: Setup git user 30 | run: | 31 | git config --global user.name "Nebula Plugin Maintainers" 32 | git config --global user.email "nebula-plugins-oss@netflix.com" 33 | - name: Set up JDKs 34 | uses: actions/setup-java@v4 35 | with: 36 | distribution: 'zulu' 37 | java-version: | 38 | 8 39 | ${{ matrix.java }} 40 | java-package: jdk 41 | - uses: actions/cache@v4 42 | id: gradle-cache 43 | with: 44 | path: ~/.gradle/caches 45 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle/dependency-locks/*.lockfile') }} 46 | restore-keys: | 47 | - ${{ runner.os }}-gradle- 48 | - uses: actions/cache@v4 49 | id: gradle-wrapper-cache 50 | with: 51 | path: ~/.gradle/wrapper 52 | key: ${{ runner.os }}-gradlewrapper-${{ hashFiles('gradle/wrapper/*') }} 53 | restore-keys: | 54 | - ${{ runner.os }}-gradlewrapper- 55 | - name: Gradle build 56 | run: ./gradlew --info --stacktrace build 57 | env: 58 | JDK_VERSION_FOR_TESTS: ${{ matrix.java }} 59 | validatepluginpublication: 60 | if: startsWith(github.ref, 'refs/tags/v') 61 | needs: validation 62 | runs-on: ubuntu-latest 63 | name: Gradle Plugin Publication Validation 64 | env: 65 | NETFLIX_OSS_SONATYPE_USERNAME: ${{ secrets.ORG_SONATYPE_USERNAME }} 66 | NETFLIX_OSS_SONATYPE_PASSWORD: ${{ secrets.ORG_SONATYPE_PASSWORD }} 67 | steps: 68 | - uses: actions/checkout@v1 69 | - name: Setup git user 70 | run: | 71 | git config --global user.name "Nebula Plugin Maintainers" 72 | git config --global user.email "nebula-plugins-oss@netflix.com" 73 | - name: Set up JDKs 74 | uses: actions/setup-java@v4 75 | with: 76 | distribution: 'zulu' 77 | java-version: | 78 | 8 79 | 21 80 | java-package: jdk 81 | - uses: actions/cache@v4 82 | id: gradle-cache 83 | with: 84 | path: ~/.gradle/caches 85 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle/dependency-locks/*.lockfile') }} 86 | restore-keys: | 87 | - ${{ runner.os }}-gradle- 88 | - uses: actions/cache@v4 89 | id: gradle-wrapper-cache 90 | with: 91 | path: ~/.gradle/wrapper 92 | key: ${{ runner.os }}-gradlewrapper-${{ hashFiles('gradle/wrapper/*') }} 93 | restore-keys: | 94 | - ${{ runner.os }}-gradlewrapper- 95 | - name: Verify plugin publication 96 | if: | 97 | startsWith(github.ref, 'refs/tags/v') && 98 | (!contains(github.ref, '-rc.')) 99 | run: ./gradlew --stacktrace -Dgradle.publish.key=${{ secrets.gradlePublishKey }} -Dgradle.publish.secret=${{ secrets.gradlePublishSecret }} -Prelease.useLastTag=true final publishPlugin --validate-only -x check -x signPluginMavenPublication 100 | publish: 101 | if: startsWith(github.ref, 'refs/tags/v') 102 | needs: validatepluginpublication 103 | runs-on: ubuntu-latest 104 | name: Gradle Build and Publish 105 | env: 106 | NETFLIX_OSS_SONATYPE_USERNAME: ${{ secrets.ORG_SONATYPE_USERNAME }} 107 | NETFLIX_OSS_SONATYPE_PASSWORD: ${{ secrets.ORG_SONATYPE_PASSWORD }} 108 | NETFLIX_OSS_SIGNING_KEY: ${{ secrets.ORG_SIGNING_KEY }} 109 | NETFLIX_OSS_SIGNING_PASSWORD: ${{ secrets.ORG_SIGNING_PASSWORD }} 110 | NETFLIX_OSS_REPO_USERNAME: ${{ secrets.ORG_NETFLIXOSS_USERNAME }} 111 | NETFLIX_OSS_REPO_PASSWORD: ${{ secrets.ORG_NETFLIXOSS_PASSWORD }} 112 | steps: 113 | - uses: actions/checkout@v1 114 | - name: Setup git user 115 | run: | 116 | git config --global user.name "Nebula Plugin Maintainers" 117 | git config --global user.email "nebula-plugins-oss@netflix.com" 118 | - name: Set up JDKs 119 | uses: actions/setup-java@v4 120 | with: 121 | distribution: 'zulu' 122 | java-version: | 123 | 8 124 | 21 125 | java-package: jdk 126 | - uses: actions/cache@v4 127 | id: gradle-cache 128 | with: 129 | path: ~/.gradle/caches 130 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle/dependency-locks/*.lockfile') }} 131 | restore-keys: | 132 | - ${{ runner.os }}-gradle- 133 | - uses: actions/cache@v4 134 | id: gradle-wrapper-cache 135 | with: 136 | path: ~/.gradle/wrapper 137 | key: ${{ runner.os }}-gradlewrapper-${{ hashFiles('gradle/wrapper/*') }} 138 | restore-keys: | 139 | - ${{ runner.os }}-gradlewrapper- 140 | - name: Publish candidate 141 | if: | 142 | startsWith(github.ref, 'refs/tags/v') && 143 | contains(github.ref, '-rc.') 144 | run: ./gradlew --info --stacktrace -Prelease.useLastTag=true candidate 145 | - name: Publish release 146 | if: | 147 | startsWith(github.ref, 'refs/tags/v') && 148 | (!contains(github.ref, '-rc.')) 149 | run: ./gradlew --info --stacktrace -Dgradle.publish.key=${{ secrets.gradlePublishKey }} -Dgradle.publish.secret=${{ secrets.gradlePublishSecret }} -Prelease.useLastTag=true final 150 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .DS_Store 3 | generated/ 4 | out/ 5 | .gradle 6 | .idea 7 | *.ipr 8 | *.iml 9 | *.iws 10 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 1.8 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2.5.2 / 2017/03/16 2 | ================== 3 | - Fix a memory leak in the gradle daemon caused by putting an object in a static context 4 | 5 | 2.5.1 / 2017/03/15 6 | ================== 7 | - Minor performance optimization to rule matching 8 | 9 | 2.5.0 / 2017/03/15 10 | ================== 11 | - Use `nebula.dependency-base` to add `dependencyInsightEnhanced` task to grant more insight into resolution rule choices 12 | 13 | 2.4.4 / 2017/03/10 14 | ================== 15 | - Use reflection when getting the event register to avoid an upcoming internal API change from a class -> interface causing `IncompatibleClassChangeError` 16 | 17 | 2.4.3 / 2017/03/08 18 | ================== 19 | - Improve performance of alignment by short-circuiting when all dependencies in a configuration have already been aligned either naturally, or via other plugins such as dependency lock 20 | 21 | 2.4.2 / 2017/02/28 22 | ================== 23 | - Fixed an issue that prevented rules from being applied to configurations created after the project was evaluated, causing global dependency locks not to be affected by rules. 24 | 25 | 2.4.1 / 2017/02/26 26 | ================== 27 | - Prevent beforeResolve rules from applying unless afterEvaluate rules have already been applied. Prevents 'already resolved' warnings for alignment configurations 28 | 29 | 2.4.0 / 2017/02/26 30 | ================== 31 | - Support version selectors (dynamic, range, latest.*) for reject rules 32 | 33 | 2.3.3 / 2017/02/24 34 | ================== 35 | - Fix multi-pass alignment where any unexpected resolved version would cause the second pass to be ineffective for all dependencies 36 | - Fix duplicate path separators in root project configuration names 37 | 38 | 2.3.2 / 2017/01/24 39 | ================== 40 | - Improve version selection for multi-pass alignment to ensure that it doesn't affect dependencies that were selected for reasons other than conflict resolution 41 | 42 | 2.3.1 / 2017/01/20 43 | ================== 44 | - No longer applies any rules to configurations with no dependencies, rather than just optimizing align rules 45 | 46 | 2.3.0 / 2017/01/19 47 | ================== 48 | - Improve performance by avoiding alignment where possible: 49 | - Skips configurations with no dependencies 50 | - Skips non-transitive configurations 51 | - Stops alignment after the baseline resolve, if there are no aligned dependencies 52 | 53 | 1.5.1 / 2016/05/12 54 | ================== 55 | - Protect against spring-boot plugin getting us into a stackoverflow situation 56 | 57 | 1.5.0 / 2016/05/12 58 | ================== 59 | - Align rules no longer replace changes made by other rule type (uses useVersion instead of useTarget). 60 | 61 | 1.4.0 / 2016/05/11 62 | ================== 63 | - Make it so we are not eagerly resolving the different configurations. Will only resolve when gradle resolves the configuration. 64 | 65 | 1.3.0 / 2016/04/28 66 | ================== 67 | - BUGFIX: Align rules attempt to align project dependencies, causing them to be resolved as remote artifacts 68 | - Rules files may now be optional, so optionated rules aren't applied without users opting in 69 | - Align rules support regular expressions in the group, includes and excludes 70 | - Empty rules types can be excluded from rules files (required for backwards compatibility with old rules files, but also makes working with them nicer) 71 | 72 | 1.2.2 / 2016/04/25 73 | ================== 74 | - BUGFIX: Handle circularish dependencies B depends on A for compile, A depends on B for testCompile 75 | 76 | 1.2.1 / 2016/04/19 77 | ================== 78 | - BUGFIX: Make sure resolutionRules configuration can be locked by nebula.dependency-lock 79 | - BUGFIX: Allow other changes to configurations.all and associated resolutionStrategy 80 | 81 | 1.2.0 / 2016/04/11 82 | ================== 83 | - Allow opt out of rules for shared company wide rules that apply to your project, e.g. there is a common align rule for a:foo and a:bar and you produce them 84 | - Performance improvement if there are multiple align rules 85 | - BUGFIX for unresolvable dependencies fixed by a resolution rule 86 | 87 | 1.1.5 / 2016/03/31 88 | ================== 89 | - Fix interaction bug with nebula.dependency-recommender (omitted versions causing issues) 90 | - Fix interaction bug with spring-boot plugin (omitted versions causing issues) 91 | - Fix handling of dependency graphs with cycles in them 92 | 93 | 1.1.4 / 2016/03/22 94 | ================== 95 | - Remove dependency on jackson libraries 96 | 97 | 1.1.3 / 2016/03/21 98 | ================== 99 | - Publish nebula.resolution-rules-producer to gradle plugin portal 100 | 101 | 1.1.2 / 2016/03/21 102 | ================== 103 | - Attempt to add nebula.resolution-rules-producer to gradle plugin portal 104 | 105 | 1.1.1 / 2016/03/21 106 | ================== 107 | - Fix publishing to bintray 108 | 109 | 1.1.0 / 2016/03/21 110 | ================== 111 | - Add in align rule 112 | 113 | 1.0.1 / 2015/10/28 114 | ================== 115 | - Re-publish due to initial JCenter sync failure 116 | 117 | 1.0.0 / 2015/10/28 118 | ================== 119 | - Initial Release 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gradle Resolution Rules Plugin 2 | 3 | ![Support Status](https://img.shields.io/badge/nebula-active-green.svg) 4 | [![Gradle Plugin Portal](https://img.shields.io/maven-metadata/v/https/plugins.gradle.org/m2/com.netflix.nebula/gradle-resolution-rules-plugin/maven-metadata.xml.svg?label=gradlePluginPortal)](https://plugins.gradle.org/plugin/com.netflix.nebula.resolution-rules) 5 | [![Maven Central](https://img.shields.io/maven-central/v/com.netflix.nebula/gradle-resolution-rules-plugin)](https://maven-badges.herokuapp.com/maven-central/com.netflix.nebula/gradle-resolution-rules-plugin) 6 | ![Build](https://github.com/nebula-plugins/gradle-resolution-rules-plugin/actions/workflows/nebula.yml/badge.svg) 7 | [![Apache 2.0](https://img.shields.io/github/license/nebula-plugins/gradle-resolution-rules-plugin.svg)](http://www.apache.org/licenses/LICENSE-2.0) 8 | 9 | 10 | Gradle resolution strategies and module metadata provide an effective way to solve the most common dependency issues, however sharing these rules between projects is cumbersome, and requires custom plugins or `apply from` calls. This plugin provides general purpose rule types, allowing rules to be published, versioned, shared between projects, and optionally [dependency locked](https://github.com/nebula-plugins/gradle-dependency-lock-plugin). 11 | 12 | These rule types solve the most common cause of dependency issues in projects, including: 13 | 14 | - Duplicate classes caused by changes to group or artifact ids, without renaming packages 15 | - Duplicate classes caused by bundle dependencies, which do not conflict resolve against the normal dependencies for that library 16 | - Lack of version alignment between libraries, where version alignment is needed for compatibility 17 | - Ensuring a minimum version of a library 18 | 19 | # Quick Start 20 | 21 | Refer to the [Gradle Plugin Portal](https://plugins.gradle.org/plugin/nebula.resolution-rules) for instructions on how to apply the plugin. 22 | 23 | ## Open Source Rules 24 | 25 | We produce a rules for dependencies found in Maven Central and other public repositories, to use those rules in your project add the following to your root project: 26 | 27 | ```groovy 28 | allprojects { 29 | apply plugin: 'com.netflix.nebula.resolution-rules' 30 | } 31 | 32 | dependencies { 33 | resolutionRules 'com.netflix.nebula:gradle-resolution-rules:latest.release' 34 | } 35 | ``` 36 | 37 | See the [gradle-resolution-rules](https://github.com/nebula-plugins/gradle-resolution-rules) project for details of the rules, and instructions on how to enable optional rule sets. 38 | 39 | # Documentation 40 | 41 | The project wiki contains the [full documentation](https://github.com/nebula-plugins/gradle-resolution-rules-plugin/wiki) for the plugin. 42 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | plugins { 19 | id 'com.netflix.nebula.plugin-plugin' version '21.2.2' 20 | id "org.jetbrains.kotlin.jvm" version "2.1.0" 21 | id 'java-gradle-plugin' 22 | } 23 | 24 | dependencies { 25 | implementation platform("com.fasterxml.jackson:jackson-bom:2.9.10.+") 26 | 27 | implementation 'org.jetbrains.kotlin:kotlin-reflect' 28 | implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7' // Not a direct dependency but ensures alignment when we upgrade Kotlin 29 | implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' // Not a direct dependency but ensures alignment when we upgrade Kotlin 30 | implementation 'com.netflix.nebula:nebula-gradle-interop:latest.release' 31 | implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' 32 | implementation 'com.netflix.nebula:nebula-dependency-recommender:latest.release' 33 | 34 | testImplementation gradleTestKit() 35 | testRuntimeOnly 'com.netflix.nebula:gradle-dependency-lock-plugin:latest.release' 36 | } 37 | 38 | apply from: 'gradle/idea.gradle' 39 | 40 | description 'Gradle resolution rules plugin' 41 | 42 | contacts { 43 | 'nebula-plugins-oss@netflix.com' { 44 | moniker 'Nebula Plugins Maintainers' 45 | github 'nebula-plugins' 46 | } 47 | } 48 | 49 | 50 | 51 | tasks.integrationTest { 52 | maxParallelForks = (int) (Runtime.getRuntime().availableProcessors() / 2 + 1) 53 | } 54 | 55 | gradlePlugin { 56 | plugins { 57 | resolutionRules { 58 | id = 'com.netflix.nebula.resolution-rules' 59 | implementationClass = 'nebula.plugin.resolutionrules.ResolutionRulesPlugin' 60 | displayName = 'Gradle Resolution Rules plugin' 61 | description = project.description 62 | tags.set(['nebula', 'resolve', 'resolution', 'rules']) 63 | } 64 | } 65 | } 66 | 67 | java { 68 | toolchain { 69 | languageVersion = JavaLanguageVersion.of(8) 70 | } 71 | } 72 | 73 | idea { 74 | project { 75 | jdkName = '1.8' 76 | languageLevel = '1.8' 77 | } 78 | } 79 | 80 | 81 | tasks.withType(GenerateModuleMetadata).configureEach { 82 | suppressedValidationErrors.add('enforced-platform') 83 | } 84 | 85 | javaCrossCompile { 86 | disableKotlinSupport = true 87 | } 88 | -------------------------------------------------------------------------------- /gradle.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | cglib:cglib-nodep:3.2.2=integTestRuntimeClasspath,testRuntimeClasspath 5 | com.fasterxml.jackson.core:jackson-annotations:2.9.10=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 6 | com.fasterxml.jackson.core:jackson-core:2.9.10=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 7 | com.fasterxml.jackson.core:jackson-databind:2.9.10.8=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 8 | com.fasterxml.jackson.module:jackson-module-kotlin:2.9.10=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 9 | com.fasterxml.jackson:jackson-bom:2.9.10.20210106=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 10 | com.google.guava:guava:20.0=integTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 11 | com.netflix.nebula:gradle-dependency-lock-plugin:15.1.0=integTestRuntimeClasspath,testRuntimeClasspath 12 | com.netflix.nebula:nebula-dependencies-comparison:0.2.1=integTestRuntimeClasspath,testRuntimeClasspath 13 | com.netflix.nebula:nebula-dependency-recommender:12.5.1=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 14 | com.netflix.nebula:nebula-gradle-interop:2.3.0=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 15 | com.netflix.nebula:nebula-test:10.6.2=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 16 | com.squareup.moshi:moshi:1.12.0=integTestRuntimeClasspath,testRuntimeClasspath 17 | com.squareup.okio:okio:2.10.0=integTestRuntimeClasspath,testRuntimeClasspath 18 | javax.inject:javax.inject:1=integTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 19 | joda-time:joda-time:2.10=integTestRuntimeClasspath,testRuntimeClasspath 20 | junit:junit:4.13.2=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 21 | org.apache.commons:commons-lang3:3.12.0=integTestRuntimeClasspath,testRuntimeClasspath 22 | org.apache.commons:commons-lang3:3.8.1=runtimeClasspath 23 | org.apache.maven:maven-artifact:3.8.3=integTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 24 | org.apache.maven:maven-builder-support:3.8.3=integTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 25 | org.apache.maven:maven-model-builder:3.8.3=integTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 26 | org.apache.maven:maven-model:3.8.3=integTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 27 | org.apiguardian:apiguardian-api:1.1.2=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 28 | org.codehaus.groovy:groovy:3.0.12=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 29 | org.codehaus.plexus:plexus-interpolation:1.26=integTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 30 | org.codehaus.plexus:plexus-utils:3.3.0=integTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 31 | org.eclipse.sisu:org.eclipse.sisu.inject:0.3.5=integTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 32 | org.hamcrest:hamcrest-core:1.3=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 33 | org.hamcrest:hamcrest:2.2=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 34 | org.jetbrains.kotlin:kotlin-reflect:2.1.0=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 35 | org.jetbrains.kotlin:kotlin-stdlib-common:1.4.20=integTestRuntimeClasspath,testRuntimeClasspath 36 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 37 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 38 | org.jetbrains.kotlin:kotlin-stdlib:2.1.0=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 39 | org.jetbrains:annotations:13.0=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 40 | org.junit.platform:junit-platform-commons:1.9.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 41 | org.junit.platform:junit-platform-engine:1.9.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 42 | org.objenesis:objenesis:2.4=integTestRuntimeClasspath,testRuntimeClasspath 43 | org.opentest4j:opentest4j:1.2.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 44 | org.spockframework:spock-core:2.3-groovy-3.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 45 | org.spockframework:spock-junit4:2.3-groovy-3.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 46 | empty=annotationProcessor,compile,integTestAnnotationProcessor,integTestCompile,integTestRuntime,runtime,testAnnotationProcessor,testCompile,testRuntime 47 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | systemProp.nebula.features.coreLockingSupport=true 2 | -------------------------------------------------------------------------------- /gradle/gradle-daemon-jvm.properties: -------------------------------------------------------------------------------- 1 | toolchainVersion=21 -------------------------------------------------------------------------------- /gradle/idea-codestyle.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 251 | 253 | -------------------------------------------------------------------------------- /gradle/idea-copyright.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /gradle/idea-inspections.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 32 | 33 | 37 | -------------------------------------------------------------------------------- /gradle/idea.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | idea { 19 | project { 20 | def javaTarget = '1.7' 21 | jdkName = javaTarget 22 | languageLevel = javaTarget 23 | 24 | wildcards += 'logback.groovy' 25 | 26 | ipr { 27 | withXml { provider -> 28 | def node = provider.asNode() 29 | 30 | // Code styles 31 | def codestyle = new XmlParser().parse(file('gradle/idea-codestyle.xml')) 32 | node.append(codestyle) 33 | 34 | // Inspections 35 | def inspections = new XmlParser().parse(file('gradle/idea-inspections.xml')) 36 | node.append(inspections) 37 | 38 | // Copyright 39 | def copyrightManager = node.component.find { it.'@name' == 'CopyrightManager' } 40 | node.remove(copyrightManager) 41 | def copyright = new XmlParser().parse(file('gradle/idea-copyright.xml')) 42 | node.append(copyright) 43 | 44 | // VCS mappings 45 | def vcsDirectoryMappings = node.component.find { it.'@name' == 'VcsDirectoryMappings' } 46 | def mappings = vcsDirectoryMappings.iterator() 47 | while (mappings.hasNext()) { 48 | mappings.next() 49 | mappings.remove() 50 | } 51 | 52 | def gitRoot = file('.git') 53 | if (gitRoot.exists()) { 54 | vcsDirectoryMappings.appendNode('mapping', ['directory': gitRoot.parentFile, 'vcs': 'Git']) 55 | } 56 | 57 | // Annotation processing 58 | node.component.find { it.@name == 'CompilerConfiguration' }['annotationProcessing'][0].replaceNode { 59 | annotationProcessing { 60 | profile(default: true, name: 'Default', useClasspath: 'true', enabled: true) { 61 | outputRelativeToContentRoot(value: true) 62 | processorPath(useClasspath: true) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nebula-plugins/gradle-resolution-rules-plugin/ef67ba983c4364d9a20192506eb124875416f93c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.gradle.develocity' version '3.19' 3 | } 4 | 5 | develocity { 6 | buildScan { 7 | termsOfUseUrl = "https://gradle.com/help/legal-terms-of-use" 8 | termsOfUseAgree = 'yes' 9 | } 10 | } 11 | 12 | rootProject.name = 'gradle-resolution-rules-plugin' 13 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AbstractAlignAndMigrateSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | import nebula.test.dependencies.DependencyGraphBuilder 4 | import nebula.test.dependencies.GradleDependencyGenerator 5 | import nebula.test.dependencies.ModuleBuilder 6 | 7 | /* 8 | Used to verify behavior when a dependency brings in its replacement as its only direct dependency 9 | Example: 10 | foo:bar:1.2.3 is the latest release of foo:bar that brings in its new coordinates at 11 | better-foo:better-bar:2.0.0 12 | We want to make sure that alignment still takes place for all dependencies in better-foo:better-bar 13 | */ 14 | class AbstractAlignAndMigrateSpec extends AbstractAlignRulesSpec { 15 | String alignedVersion = '1.0.3' 16 | File mavenrepo 17 | 18 | def setup() { 19 | createTestDependencies() 20 | buildFile << """ 21 | repositories { 22 | maven { url = '${projectDir.toPath().relativize(mavenrepo.toPath()).toFile()}' } 23 | } 24 | dependencies { 25 | implementation 'test.nebula:a:1.0.0' 26 | implementation 'test.nebula:b:1.0.3' 27 | implementation 'other:e:4.0.0' 28 | } 29 | """.stripIndent() 30 | } 31 | 32 | private void createTestDependencies() { 33 | def graph = new DependencyGraphBuilder() 34 | .addModule('test.nebula:a:1.0.0') 35 | .addModule('test.nebula:a:1.0.1') 36 | .addModule('test.nebula:a:1.0.2') 37 | .addModule('test.nebula:a:1.0.3') 38 | 39 | .addModule('test.nebula:b:1.0.0') 40 | .addModule('test.nebula:b:1.0.1') 41 | .addModule('test.nebula:b:1.0.2') 42 | .addModule('test.nebula:b:1.0.3') 43 | 44 | .addModule('test.nebula:c:1.0.0') 45 | .addModule('test.nebula:c:1.0.1') 46 | .addModule('test.nebula:c:1.0.2') 47 | .addModule('test.nebula:c:1.0.3') 48 | 49 | .addModule(new ModuleBuilder('other:e:4.0.0').addDependency('test.nebula:c:1.0.1').build()) // this is the most interesting dependency under test 50 | 51 | .build() 52 | mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 53 | } 54 | 55 | Collection dependencyInsightTasks() { 56 | return ['dependencyInsight', '--dependency', 'test.nebula'] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AbstractAlignRulesSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | 4 | abstract class AbstractAlignRulesSpec extends AbstractIntegrationTestKitSpec { 5 | def rulesJsonFile 6 | 7 | def setup() { 8 | rulesJsonFile = new File(projectDir, "${moduleName}.json") 9 | buildFile << """\ 10 | plugins { 11 | id 'com.netflix.nebula.resolution-rules' 12 | id 'java' 13 | } 14 | dependencies { 15 | resolutionRules files('$rulesJsonFile') 16 | } 17 | """.stripIndent() 18 | } 19 | 20 | protected def createAlignAndReplaceRules(Map modulesAndReplacements) { 21 | String reason = "★ custom replacement reason" 22 | rulesJsonFile << """ 23 | { 24 | "replace": [ 25 | """.stripIndent() 26 | 27 | List replacements = new ArrayList<>() 28 | modulesAndReplacements.each { module, with -> 29 | replacements.add(""" { 30 | "module" : "$module", 31 | "with" : "$with", 32 | "reason" : "$reason", 33 | "author" : "Test user ", 34 | "date" : "2020-02-27T10:31:14.321Z" 35 | }""") 36 | 37 | } 38 | rulesJsonFile << replacements.join(',') 39 | 40 | rulesJsonFile << """ 41 | ], 42 | "align": [ 43 | { 44 | "group": "(test.nebula|test.nebula.ext)", 45 | "reason": "Align test.nebula dependencies", 46 | "author": "Example Person ", 47 | "date": "2020-02-27T10:31:14.321Z" 48 | } 49 | ] 50 | } 51 | """.stripIndent() 52 | } 53 | 54 | protected def createAlignAndSubstituteRules(Map modulesAndSubstitutions) { 55 | String reason = "★ custom substitution reason" 56 | rulesJsonFile << """ 57 | { 58 | "substitute": [ 59 | """.stripIndent() 60 | 61 | List substitutions = new ArrayList<>() 62 | modulesAndSubstitutions.each { module, with -> 63 | substitutions.add(""" { 64 | "module" : "$module", 65 | "with" : "$with", 66 | "reason" : "$reason", 67 | "author" : "Test user ", 68 | "date" : "2020-02-27T10:31:14.321Z" 69 | }""") 70 | } 71 | 72 | rulesJsonFile << substitutions.join(',') 73 | 74 | rulesJsonFile << """ 75 | ], 76 | "align": [ 77 | { 78 | "group": "(test.nebula|test.nebula.ext)", 79 | "reason": "Align test.nebula dependencies", 80 | "author": "Example Person ", 81 | "date": "2020-02-27T10:31:14.321Z" 82 | } 83 | ] 84 | } 85 | """.stripIndent() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AbstractIntegrationTestKitSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | import nebula.test.IntegrationTestKitSpec 4 | 5 | abstract class AbstractIntegrationTestKitSpec extends IntegrationTestKitSpec { 6 | def setup() { 7 | // Enable configuration cache :) 8 | new File(projectDir, 'gradle.properties') << '''org.gradle.configuration-cache=true'''.stripIndent() 9 | } 10 | 11 | 12 | void disableConfigurationCache() { 13 | def propertiesFile = new File(projectDir, 'gradle.properties') 14 | if(propertiesFile.exists()) { 15 | propertiesFile.delete() 16 | } 17 | propertiesFile.createNewFile() 18 | propertiesFile << '''org.gradle.configuration-cache=false'''.stripIndent() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AbstractRulesWithSpringBootPluginSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | 4 | class AbstractRulesWithSpringBootPluginSpec extends AbstractIntegrationTestKitSpec { 5 | File rulesJsonFile 6 | 7 | def setup() { 8 | rulesJsonFile = new File(projectDir, "rules.json") 9 | keepFiles = true 10 | } 11 | 12 | static void dependencyInsightContains(String resultOutput, String groupAndName, String resultingVersion) { 13 | def content = "$groupAndName:.*$resultingVersion\n" 14 | assert resultOutput.findAll(content).size() >= 1 15 | } 16 | 17 | File addSpringDependenciesWhenUsingManagedDependencies(String requestedVersion) { 18 | buildFile << """ 19 | dependencies { 20 | implementation "org.springframework:spring-core$requestedVersion" 21 | implementation "org.springframework.boot:spring-boot-starter" 22 | implementation "org.springframework.boot:spring-boot-starter-web" 23 | } 24 | """.stripIndent() 25 | } 26 | 27 | static def tasks(String groupForInsight = 'org.springframework:') { 28 | return [ 29 | 'dependencyInsight', 30 | '--dependency', 31 | groupForInsight, '-s' 32 | ] 33 | } 34 | 35 | void setupForDirectDependencyScenario(String extSpringBootVersion, String forcedVersion, String additionalPlugin = '', String additionalExtProperty = '') { 36 | setupBaseSpringBootBasedBuildFileWith(extSpringBootVersion, additionalPlugin, additionalExtProperty) 37 | 38 | if (forcedVersion != '' && forcedVersion != null) { 39 | buildFile << """ 40 | configurations.all { 41 | resolutionStrategy { 42 | force "org.springframework:spring-aop:$forcedVersion" 43 | force "org.springframework:spring-beans:$forcedVersion" 44 | force "org.springframework:spring-context:$forcedVersion" 45 | force "org.springframework:spring-core:$forcedVersion" 46 | force "org.springframework:spring-expression:$forcedVersion" 47 | force "org.springframework:spring-web:$forcedVersion" 48 | force "org.springframework:spring-webmvc:$forcedVersion" 49 | } 50 | } 51 | """.stripIndent() 52 | } 53 | 54 | rulesJsonFile << alignSpringRule() 55 | } 56 | 57 | void setupForTransitiveDependencyScenario(String extSpringBootVersion, String forcedVersion, String additionalPlugin = '', String additionalExtProperty = '') { 58 | setupBaseSpringBootBasedBuildFileWith(extSpringBootVersion, additionalPlugin, additionalExtProperty) 59 | 60 | if (forcedVersion != '' && forcedVersion != null) { 61 | buildFile << """ 62 | configurations.all { 63 | resolutionStrategy { 64 | force "org.slf4j:slf4j-simple:$forcedVersion" 65 | force "org.slf4j:slf4j-api:$forcedVersion" 66 | } 67 | } 68 | """.stripIndent() 69 | } 70 | 71 | rulesJsonFile << alignSlf4jRule() 72 | } 73 | 74 | private File setupBaseSpringBootBasedBuildFileWith(String extSpringBootVersion, String additionalPlugin = '', String additionalExtProperty = '') { 75 | buildFile << """ 76 | buildscript { 77 | dependencies { 78 | classpath("org.springframework.boot:spring-boot-gradle-plugin:$extSpringBootVersion") 79 | classpath "io.spring.gradle:dependency-management-plugin:1.1.0" 80 | } 81 | repositories { 82 | maven { 83 | url = "https://plugins.gradle.org/m2/" 84 | } 85 | } 86 | } 87 | plugins { 88 | id 'java' 89 | id 'com.netflix.nebula.resolution-rules' 90 | } 91 | apply plugin: 'org.springframework.boot'$additionalPlugin 92 | repositories { 93 | mavenCentral() 94 | } 95 | dependencies { 96 | resolutionRules files('$rulesJsonFile') 97 | } 98 | ext {$additionalExtProperty 99 | } 100 | """.stripIndent() 101 | 102 | } 103 | 104 | private static String alignSpringRule() { 105 | """ 106 | { 107 | "align": [ 108 | { 109 | "group": "org\\\\.springframework", 110 | "includes": ["spring-(tx|aop|instrument|context-support|beans|jms|test|core|oxm|web|context|expression|aspects|websocket|framework-bom|webmvc|webmvc-portlet|jdbc|orm|instrument-tomcat|messaging)"], 111 | "excludes": [], 112 | "match": "[2-9]\\\\.[0-9]+\\\\.[0-9]+.RELEASE", 113 | "reason": "Align Spring", 114 | "author": "User ", 115 | "date": "2016-05-16" 116 | } 117 | ] 118 | } 119 | """.stripIndent() 120 | } 121 | 122 | private static String alignSlf4jRule() { 123 | """ 124 | { 125 | "align": [ 126 | { 127 | "name": "align slf4j", 128 | "group": "org.slf4j", 129 | "reason": "Align slf4j", 130 | "author": "User ", 131 | "date": "2016-05-16" 132 | } 133 | ] 134 | } 135 | """.stripIndent() 136 | } 137 | 138 | void writeOutputToProjectDir(String output) { 139 | def file = new File(projectDir, "result.txt") 140 | file.createNewFile() 141 | file << output 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AlignAndMigrateViaReplacementSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | 4 | import spock.lang.Unroll 5 | 6 | class AlignAndMigrateViaReplacementSpec extends AbstractAlignAndMigrateSpec { 7 | @Unroll 8 | def 'align and migrate via replacement'() { 9 | given: 10 | createAlignAndReplaceRules(['other:e': 'test.nebula:c']) 11 | 12 | when: 13 | def tasks = ['dependencyInsight', '--dependency', 'test.nebula'] 14 | def results = runTasks(*tasks) 15 | then: 16 | results.output.contains("test.nebula:a:1.0.0 -> $alignedVersion") 17 | results.output.contains("test.nebula:b:$alignedVersion") 18 | results.output.contains("other:e:4.0.0 -> test.nebula:c:$alignedVersion") 19 | results.output.contains("belongs to platform aligned-platform") 20 | 21 | when: 22 | def dependenciesTasks = ['dependencies', '--configuration', 'compileClasspath'] 23 | def resultsForDependencies = runTasks(*dependenciesTasks) 24 | 25 | then: 26 | resultsForDependencies.output.contains("other:e:4.0.0 -> test.nebula:c:1.0.3") 27 | } 28 | 29 | @Unroll 30 | def 'align and migrate via replacement with brought in dependency as direct as well'() { 31 | given: 32 | createAlignAndReplaceRules(['other:e': 'test.nebula:c']) 33 | buildFile << """ 34 | dependencies { 35 | implementation 'test.nebula:c:1.0.1' 36 | } 37 | """ 38 | 39 | when: 40 | def tasks = ['dependencyInsight', '--dependency', 'test.nebula'] 41 | def results = runTasks(*tasks) 42 | then: 43 | results.output.contains("test.nebula:a:1.0.0 -> $alignedVersion") 44 | results.output.contains("test.nebula:b:$alignedVersion") 45 | results.output.contains("other:e:4.0.0 -> test.nebula:c:$alignedVersion") 46 | results.output.contains("belongs to platform aligned-platform") 47 | 48 | 49 | when: 50 | def dependenciesTasks = ['dependencies', '--configuration', 'compileClasspath'] 51 | def resultsForDependencies = runTasks(*dependenciesTasks) 52 | 53 | then: 54 | resultsForDependencies.output.contains("other:e:4.0.0 -> test.nebula:c:$alignedVersion") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AlignAndMigrateViaSubstitutionSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | 4 | import spock.lang.Unroll 5 | 6 | class AlignAndMigrateViaSubstitutionSpec extends AbstractAlignAndMigrateSpec { 7 | @Unroll 8 | def 'substitution and alignment'() { 9 | given: 10 | createAlignAndSubstituteRules(['other:e:4.0.0': 'test.nebula:c:1.0.1']) 11 | 12 | when: 13 | def results = runTasks(*dependencyInsightTasks()) 14 | 15 | then: 16 | results.output.contains("test.nebula:a:1.0.0 -> $alignedVersion") 17 | results.output.contains("test.nebula:b:$alignedVersion") 18 | results.output.contains("other:e:4.0.0 -> test.nebula:c:$alignedVersion") 19 | results.output.contains("substituted other:e:4.0.0 with test.nebula:c:1.0.1 because '★ custom substitution reason'") 20 | results.output.contains("belongs to platform aligned-platform") 21 | 22 | 23 | when: 24 | def dependenciesTasks = ['dependencies', '--configuration', 'compileClasspath'] 25 | def resultsForDependencies = runTasks(*dependenciesTasks) 26 | 27 | then: 28 | resultsForDependencies.output.contains("other:e:4.0.0 -> test.nebula:c:$alignedVersion") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AlignAndSubstituteRulesWithSpringBoot2xPluginWithoutManagedDepsSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | 4 | import spock.lang.Unroll 5 | 6 | class AlignAndSubstituteRulesWithSpringBoot2xPluginWithoutManagedDepsSpec extends AbstractRulesWithSpringBootPluginSpec { 7 | File rulesJsonFile 8 | 9 | def setup() { 10 | rulesJsonFile = new File(projectDir, "rules.json") 11 | keepFiles = true 12 | System.setProperty('ignoreDeprecations', 'true') 13 | } 14 | 15 | @Unroll 16 | def 'direct dep | with lower requested version'() { 17 | given: 18 | // in Spring Boot 2.x plugin, the `io.spring.dependency-management` plugin is added for dependency management. We are not including it here. 19 | setupForDirectDependencyScenario(extSpringBootVersion, forcedVersion, '', 20 | "\n\tspringVersion = \"$extSpringVersion\"") 21 | buildFile << """ 22 | dependencies { 23 | implementation "org.springframework:spring-core$requestedVersion" 24 | implementation "org.springframework.boot:spring-boot-starter:$extSpringBootVersion" 25 | implementation "org.springframework.boot:spring-boot-starter-web:$extSpringBootVersion" 26 | } 27 | """.stripIndent() 28 | 29 | when: 30 | def result = runTasks(*tasks()) 31 | def output = result.output 32 | 33 | then: 34 | writeOutputToProjectDir(output) 35 | dependencyInsightContains(output, 'org.springframework:spring-aop', managedSpringVersion) 36 | dependencyInsightContains(output, 'org.springframework:spring-beans', managedSpringVersion) 37 | dependencyInsightContains(output, 'org.springframework:spring-expression', managedSpringVersion) 38 | dependencyInsightContains(output, 'org.springframework:spring-core', managedSpringVersion) 39 | 40 | where: 41 | extSpringVersion = '4.2.4.RELEASE' 42 | extSpringBootVersion = '2.7.0' 43 | managedSpringVersion = '5.3.20' // from https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/5.3.20/spring-boot-dependencies-5.3.20.pom 44 | 45 | requestedVersion = ':\${springVersion}' 46 | forcedVersion = '' 47 | } 48 | 49 | @Unroll 50 | def 'direct dep | with higher requested version'() { 51 | given: 52 | // in Spring Boot 2.x plugin, the `io.spring.dependency-management` plugin is added for dependency management. We are not including it here. 53 | setupForDirectDependencyScenario(extSpringBootVersion, forcedVersion, '', 54 | "\n\tspringVersion = \"$extSpringVersion\"") 55 | buildFile << """ 56 | dependencies { 57 | implementation "org.springframework:spring-core$requestedVersion" 58 | implementation "org.springframework.boot:spring-boot-starter:$extSpringBootVersion" 59 | implementation "org.springframework.boot:spring-boot-starter-web:$extSpringBootVersion" 60 | } 61 | """.stripIndent() 62 | 63 | when: 64 | def result = runTasks(*tasks()) 65 | def output = result.output 66 | 67 | then: 68 | writeOutputToProjectDir(output) 69 | dependencyInsightContains(output, 'org.springframework:spring-aop', extSpringVersion) 70 | dependencyInsightContains(output, 'org.springframework:spring-beans', extSpringVersion) 71 | dependencyInsightContains(output, 'org.springframework:spring-expression', extSpringVersion) 72 | dependencyInsightContains(output, 'org.springframework:spring-core', extSpringVersion) 73 | 74 | where: 75 | extSpringVersion = '5.3.24' 76 | extSpringBootVersion = '2.7.0' 77 | managedSpringVersion = '5.3.20' // from https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/5.3.20/spring-boot-dependencies-5.3.20.pom 78 | 79 | requestedVersion = ':\${springVersion}' 80 | forcedVersion = '' } 81 | 82 | @Unroll 83 | def 'direct dep | with requested version and forced'() { 84 | given: 85 | // in Spring Boot 2.x plugin, the `io.spring.dependency-management` plugin is added for dependency management. We are not including it here. 86 | setupForDirectDependencyScenario(extSpringBootVersion, forcedVersion, '', 87 | "\n\tspringVersion = \"$extSpringVersion\"") 88 | buildFile << """ 89 | dependencies { 90 | implementation "org.springframework:spring-core$requestedVersion" 91 | implementation "org.springframework.boot:spring-boot-starter:$extSpringBootVersion" 92 | implementation "org.springframework.boot:spring-boot-starter-web:$extSpringBootVersion" 93 | } 94 | """.stripIndent() 95 | 96 | when: 97 | def result = runTasks(*tasks()) 98 | def output = result.output 99 | 100 | then: 101 | writeOutputToProjectDir(output) 102 | dependencyInsightContains(output, 'org.springframework:spring-aop', forcedVersion) 103 | dependencyInsightContains(output, 'org.springframework:spring-beans', forcedVersion) 104 | dependencyInsightContains(output, 'org.springframework:spring-expression', forcedVersion) 105 | dependencyInsightContains(output, 'org.springframework:spring-core', forcedVersion) 106 | 107 | where: 108 | extSpringVersion = '4.2.4.RELEASE' 109 | extSpringBootVersion = '2.7.0' 110 | 111 | requestedVersion = ':\${springVersion}' 112 | forcedVersion = '4.2.4.RELEASE' 113 | } 114 | 115 | @Unroll 116 | def 'transitive dep | with requested version'() { 117 | given: 118 | // in Spring Boot 2.x plugin, the `io.spring.dependency-management` plugin is added for dependency management. We are not including it here. 119 | setupForTransitiveDependencyScenario(extSpringBootVersion, forcedVersion, '', 120 | "\n\tslf4jVersion = \"$extSlf4jVersion\"") 121 | buildFile << """ 122 | dependencies { 123 | implementation "org.slf4j:slf4j-simple$requestedVersion" 124 | } 125 | """.stripIndent() 126 | 127 | when: 128 | def result = runTasks(*tasks('org.slf4j')) 129 | def output = result.output 130 | 131 | then: 132 | writeOutputToProjectDir(output) 133 | dependencyInsightContains(output, 'org.slf4j:slf4j-simple', extSlf4jVersion) 134 | dependencyInsightContains(output, 'org.slf4j:slf4j-api', extSlf4jVersion) 135 | 136 | where: 137 | extSpringVersion = '4.2.4.RELEASE' 138 | extSpringBootVersion = '2.7.0' 139 | extSlf4jVersion = '1.6.0' 140 | 141 | requestedVersion = ':\$slf4jVersion' 142 | forcedVersion = '' 143 | } 144 | 145 | @Unroll 146 | def 'transitive dep | without requested version and forced'() { 147 | given: 148 | // in Spring Boot 2.x plugin, the `io.spring.dependency-management` plugin is added for dependency management. We are not including it here. 149 | setupForTransitiveDependencyScenario(extSpringBootVersion, forcedVersion, '', 150 | "\n\tslf4jVersion = \"$extSlf4jVersion\"") 151 | buildFile << """ 152 | dependencies { 153 | implementation "org.slf4j:slf4j-simple$requestedVersion" 154 | } 155 | """.stripIndent() 156 | 157 | when: 158 | def result = runTasks(*tasks('org.slf4j')) 159 | def output = result.output 160 | 161 | then: 162 | writeOutputToProjectDir(output) 163 | dependencyInsightContains(output, 'org.slf4j:slf4j-simple', forcedVersion) 164 | dependencyInsightContains(output, 'org.slf4j:slf4j-api', forcedVersion) 165 | 166 | where: 167 | extSpringVersion = '4.2.4.RELEASE' 168 | extSpringBootVersion = '2.7.0' 169 | extSlf4jVersion = '1.6.0' 170 | 171 | requestedVersion = '' 172 | forcedVersion = '1.7.10' 173 | } 174 | 175 | @Unroll 176 | def 'transitive dep | with lower requested version and forced to different version'() { 177 | given: 178 | // in Spring Boot 2.x plugin, the `io.spring.dependency-management` plugin is added for dependency management. We are not including it here. 179 | setupForTransitiveDependencyScenario(extSpringBootVersion, forcedVersion, '', 180 | "\n\tslf4jVersion = \"$extSlf4jVersion\"") 181 | buildFile << """ 182 | dependencies { 183 | implementation "org.slf4j:slf4j-simple$requestedVersion" 184 | } 185 | """.stripIndent() 186 | 187 | when: 188 | def result = runTasks(*tasks('org.slf4j')) 189 | def output = result.output 190 | 191 | then: 192 | writeOutputToProjectDir(output) 193 | dependencyInsightContains(output, 'org.slf4j:slf4j-simple', forcedVersion) 194 | dependencyInsightContains(output, 'org.slf4j:slf4j-api', forcedVersion) 195 | 196 | where: 197 | extSpringVersion = '4.2.4.RELEASE' 198 | extSpringBootVersion = '2.7.0' 199 | extSlf4jVersion = '1.6.0' 200 | 201 | requestedVersion = ':\$slf4jVersion' 202 | forcedVersion = '1.7.10' 203 | } 204 | 205 | @Unroll 206 | def 'transitive dep | with higher requested version and forced to different version'() { 207 | given: 208 | // in Spring Boot 2.x plugin, the `io.spring.dependency-management` plugin is added for dependency management. We are not including it here. 209 | setupForTransitiveDependencyScenario(extSpringBootVersion, forcedVersion, '', 210 | "\n\tslf4jVersion = \"$extSlf4jVersion\"") 211 | buildFile << """ 212 | dependencies { 213 | implementation "org.slf4j:slf4j-simple$requestedVersion" 214 | } 215 | """.stripIndent() 216 | 217 | when: 218 | def result = runTasks(*tasks('org.slf4j')) 219 | def output = result.output 220 | 221 | then: 222 | writeOutputToProjectDir(output) 223 | dependencyInsightContains(output, 'org.slf4j:slf4j-simple', forcedVersion) 224 | dependencyInsightContains(output, 'org.slf4j:slf4j-api', forcedVersion) 225 | 226 | where: 227 | extSpringVersion = '4.2.4.RELEASE' 228 | extSpringBootVersion = '2.7.0' 229 | extSlf4jVersion = '1.8.0-beta4' 230 | 231 | requestedVersion = ':\$slf4jVersion' 232 | forcedVersion = '1.7.10' 233 | } 234 | 235 | @Unroll 236 | def 'transitive dep | with requested version and forced to same version'() { 237 | given: 238 | // in Spring Boot 2.x plugin, the `io.spring.dependency-management` plugin is added for dependency management. We are not including it here. 239 | setupForTransitiveDependencyScenario(extSpringBootVersion, forcedVersion, '', 240 | "\n\tslf4jVersion = \"$extSlf4jVersion\"") 241 | buildFile << """ 242 | dependencies { 243 | implementation "org.slf4j:slf4j-simple$requestedVersion" 244 | } 245 | """.stripIndent() 246 | 247 | when: 248 | def result = runTasks(*tasks('org.slf4j')) 249 | def output = result.output 250 | 251 | then: 252 | writeOutputToProjectDir(output) 253 | dependencyInsightContains(output, 'org.slf4j:slf4j-simple', forcedVersion) 254 | dependencyInsightContains(output, 'org.slf4j:slf4j-api', forcedVersion) 255 | 256 | where: 257 | extSpringVersion = '4.2.4.RELEASE' 258 | extSpringBootVersion = '2.7.0' 259 | extSlf4jVersion = '1.6.0' 260 | 261 | requestedVersion = ':\$slf4jVersion' 262 | forcedVersion = extSlf4jVersion 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AlignRulesBasicWithCoreSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | import nebula.test.dependencies.DependencyGraphBuilder 4 | import nebula.test.dependencies.GradleDependencyGenerator 5 | import nebula.test.dependencies.ModuleBuilder 6 | import org.gradle.api.logging.LogLevel 7 | import org.gradle.util.GradleVersion 8 | import spock.lang.Unroll 9 | 10 | class AlignRulesBasicWithCoreSpec extends AbstractIntegrationTestKitSpec { 11 | private def rulesJsonFile 12 | 13 | def setup() { 14 | keepFiles = true 15 | if (GradleVersion.current().baseVersion < GradleVersion.version("6.0")) { 16 | settingsFile << '''\ 17 | enableFeaturePreview("GRADLE_METADATA") 18 | '''.stripIndent() 19 | } 20 | 21 | rulesJsonFile = new File(projectDir, "rules.json") 22 | rulesJsonFile.createNewFile() 23 | 24 | buildFile << """\ 25 | plugins { 26 | id 'com.netflix.nebula.resolution-rules' 27 | id 'java' 28 | } 29 | dependencies { 30 | resolutionRules files('$rulesJsonFile') 31 | } 32 | """.stripIndent() 33 | 34 | settingsFile << """\ 35 | rootProject.name = '${moduleName}' 36 | """.stripIndent() 37 | 38 | logLevel = LogLevel.INFO 39 | } 40 | 41 | def 'align rules and force to latest.release'() { 42 | def graph = new DependencyGraphBuilder() 43 | .addModule('test.nebula:a:1.0.0') 44 | .addModule('test.nebula:a:1.0.1') 45 | .addModule('test.nebula:a:1.1.0') 46 | .addModule('test.nebula:b:1.0.0') 47 | .addModule('test.nebula:b:1.0.1') 48 | .addModule('test.nebula:b:1.1.0') 49 | .build() 50 | def mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen") 51 | mavenrepo.generateTestMavenRepo() 52 | 53 | rulesJsonFile << alignTestNebulaRule() 54 | 55 | buildFile << """\ 56 | repositories { 57 | ${mavenrepo.mavenRepositoryBlock} 58 | } 59 | dependencies { 60 | implementation 'test.nebula:a:1.0.0' 61 | implementation 'test.nebula:b:1.1.0' 62 | } 63 | configurations.all { 64 | resolutionStrategy { 65 | force 'test.nebula:a:latest.release' 66 | } 67 | } 68 | """.stripIndent() 69 | 70 | when: 71 | def result = runTasks('dependencyInsight', '--dependency', 'test.nebula') 72 | 73 | then: 74 | def resultingVersion = "1.1.0" 75 | dependencyInsightContains(result.output, "test.nebula:a", resultingVersion) 76 | dependencyInsightContains(result.output, "test.nebula:b", resultingVersion) 77 | 78 | result.output.contains 'belongs to platform aligned-platform:rules-0-for-test.nebula-or-test.nebula.ext:1.1.0' 79 | } 80 | 81 | def 'align rules and force to latest.release when brought in transitively'() { 82 | def graph = new DependencyGraphBuilder() 83 | .addModule('test.nebula:a:1.0.0') 84 | .addModule('test.nebula:a:1.0.1') 85 | .addModule('test.nebula:a:1.1.0') 86 | .addModule('test.nebula:b:1.0.0') 87 | .addModule('test.nebula:b:1.0.1') 88 | .addModule('test.nebula:b:1.1.0') 89 | .addModule(new ModuleBuilder('test.other:brings-a:1.0.0').addDependency('test.nebula:a:1.0.3').build()) 90 | .addModule(new ModuleBuilder('test.other:also-brings-a:1.0.0').addDependency('test.nebula:a:1.1.0').build()) 91 | .addModule(new ModuleBuilder('test.other:brings-b:1.0.0').addDependency('test.nebula:b:1.1.0').build()) 92 | .build() 93 | def mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen") 94 | mavenrepo.generateTestMavenRepo() 95 | 96 | rulesJsonFile << alignTestNebulaRule() 97 | 98 | buildFile << """\ 99 | repositories { 100 | ${mavenrepo.mavenRepositoryBlock} 101 | } 102 | dependencies { 103 | implementation 'test.other:brings-a:latest.release' 104 | implementation 'test.other:also-brings-a:latest.release' 105 | implementation 'test.other:brings-b:latest.release' 106 | } 107 | configurations.all { 108 | resolutionStrategy { 109 | force 'test.nebula:a:latest.release' 110 | } 111 | } 112 | """.stripIndent() 113 | 114 | when: 115 | def result = runTasks('dependencyInsight', '--dependency', 'test.nebula') 116 | 117 | then: 118 | def resultingVersion = "1.1.0" 119 | dependencyInsightContains(result.output, "test.nebula:a", resultingVersion) 120 | dependencyInsightContains(result.output, "test.nebula:b", resultingVersion) 121 | } 122 | 123 | def 'multiple align rules'() { 124 | def graph = new DependencyGraphBuilder() 125 | .addModule('test.nebula:a:1.0.0') 126 | .addModule('test.nebula:a:1.1.0') 127 | .addModule('test.nebula:b:1.0.0') 128 | .addModule('test.nebula:b:1.1.0') 129 | .addModule('test.other:c:0.12.2') 130 | .addModule('test.other:c:1.0.0') 131 | .addModule('test.other:d:0.12.2') 132 | .addModule('test.other:d:1.0.0') 133 | .build() 134 | def mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen") 135 | mavenrepo.generateTestMavenRepo() 136 | 137 | rulesJsonFile << '''\ 138 | { 139 | "deny": [], "reject": [], "substitute": [], "replace": [], 140 | "align": [ 141 | { 142 | "name": "testNebula", 143 | "group": "test.nebula", 144 | "reason": "Align test.nebula dependencies", 145 | "author": "Example Person ", 146 | "date": "2016-03-17T20:21:20.368Z" 147 | }, 148 | { 149 | "name": "testOther", 150 | "group": "test.other", 151 | "reason": "Aligning test", 152 | "author": "Example Tester ", 153 | "date": "2016-04-05T19:19:49.495Z" 154 | } 155 | ] 156 | } 157 | '''.stripIndent() 158 | 159 | buildFile << """\ 160 | repositories { 161 | ${mavenrepo.mavenRepositoryBlock} 162 | } 163 | dependencies { 164 | implementation 'test.nebula:a:1.0.0' 165 | implementation 'test.nebula:b:1.1.0' 166 | implementation 'test.other:c:1.0.0' 167 | implementation 'test.other:d:0.12.+' 168 | } 169 | """.stripIndent() 170 | 171 | when: 172 | def result = runTasks('dependencies', '--configuration', 'compileClasspath') 173 | 174 | then: 175 | result.output.contains 'test.nebula:a:1.0.0 -> 1.1.0\n' 176 | result.output.contains 'test.nebula:b:1.1.0\n' 177 | result.output.contains 'test.other:c:1.0.0\n' 178 | result.output.contains 'test.other:d:0.12.+ -> 1.0.0\n' 179 | } 180 | 181 | @Unroll 182 | def 'core alignment uses versions observed during resolution'() { 183 | // test case from https://github.com/nebula-plugins/gradle-nebula-integration/issues/52 184 | // higher version transitive aligning parent dependency 185 | given: 186 | rulesJsonFile << """ 187 | { 188 | "align": [ 189 | { 190 | "name": "exampleapp-client-align", 191 | "group": "test.nebula", 192 | "includes": [ "exampleapp-.*" ], 193 | "excludes": [], 194 | "reason": "Library all together", 195 | "author": "example@example.com", 196 | "date": "2018-03-01" 197 | } 198 | ], 199 | "deny": [], 200 | "exclude": [], 201 | "reject": [], 202 | "replace": [], 203 | "substitute": [] 204 | } 205 | """.stripIndent() 206 | 207 | def mavenrepo = createDependenciesForExampleAppDependencies() 208 | 209 | buildFile << """ 210 | repositories { 211 | ${mavenrepo.mavenRepositoryBlock} 212 | } 213 | dependencies { 214 | implementation 'test.nebula:exampleapp-client:80.0.139' 215 | } 216 | """.stripIndent() 217 | when: 218 | def dependenciesResult = runTasks('dependencies') 219 | def result = runTasks(*tasks()) 220 | 221 | then: 222 | dependencyInsightContains(result.output, "test.nebula:exampleapp-client", resultingVersion) 223 | 224 | assert dependenciesResult.output.contains(""" 225 | \\--- test.nebula:exampleapp-client:80.0.139 -> 80.0.225 226 | +--- test.nebula:exampleapp-common:80.0.249 227 | \\--- test.nebula:exampleapp-smart-client:80.0.10 228 | """.stripIndent()) 229 | 230 | where: 231 | resultingVersion << ["80.0.225"] 232 | } 233 | 234 | private static def tasks(Boolean usingCoreBomSupport = false, String groupForInsight = 'test.nebula') { 235 | return [ 236 | 'dependencyInsight', 237 | '--dependency', 238 | groupForInsight, 239 | "-Dnebula.features.coreBomSupport=$usingCoreBomSupport" 240 | ] 241 | } 242 | 243 | private static void dependencyInsightContains(String resultOutput, String groupAndName, String resultingVersion) { 244 | def content = "$groupAndName:.*$resultingVersion\n" 245 | assert resultOutput.findAll(content).size() >= 1 246 | } 247 | 248 | private static String alignTestNebulaRule() { 249 | return '''\ 250 | { 251 | "deny": [], "reject": [], "substitute": [], "replace": [], 252 | "align": [ 253 | { 254 | "name": "testNebula", 255 | "group": "(test.nebula|test.nebula.ext)", 256 | "reason": "Align test.nebula dependencies", 257 | "author": "Example Person ", 258 | "date": "2016-03-17T20:21:20.368Z" 259 | } 260 | ] 261 | } 262 | '''.stripIndent() 263 | } 264 | 265 | private GradleDependencyGenerator createDependenciesForExampleAppDependencies() { 266 | def client = 'test.nebula:exampleapp-client' 267 | def common = 'test.nebula:exampleapp-common' 268 | def model = 'test.nebula:exampleapp-model' 269 | def smartClient = 'test.nebula:exampleapp-smart-client' 270 | def graph = new DependencyGraphBuilder() 271 | .addModule(new ModuleBuilder("$client:80.0.139") 272 | .addDependency("$common:80.0.154") 273 | .build()) 274 | .addModule(new ModuleBuilder("$client:80.0.154") 275 | .addDependency("$common:80.0.177") 276 | .build()) 277 | .addModule(new ModuleBuilder("$client:80.0.177") 278 | .addDependency("$common:80.0.201") 279 | .build()) 280 | .addModule(new ModuleBuilder("$client:80.0.201") 281 | .addDependency("$common:80.0.225") 282 | .build()) 283 | .addModule(new ModuleBuilder("$client:80.0.225") 284 | .addDependency("$common:80.0.249") 285 | .addDependency("$smartClient:80.0.10") 286 | .build()) 287 | .addModule(new ModuleBuilder("$client:80.0.236") 288 | .addDependency("$common:80.0.260") 289 | .addDependency("$smartClient:80.0.21") 290 | .build()) 291 | 292 | .addModule("$common:80.0.154") 293 | .addModule("$common:80.0.177") 294 | .addModule("$common:80.0.201") 295 | .addModule("$common:80.0.225") 296 | .addModule("$common:80.0.249") 297 | .addModule("$common:80.0.260") 298 | 299 | .addModule("$model:80.0.15") 300 | 301 | .addModule("$smartClient:80.0.10") 302 | .addModule(new ModuleBuilder("$smartClient:80.0.21") 303 | .addDependency("$model:80.0.15") 304 | .build()) 305 | .build() 306 | def mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen") 307 | mavenrepo.generateTestMavenRepo() 308 | return mavenrepo 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AlignRulesDirectDependenciesSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | import nebula.test.dependencies.DependencyGraphBuilder 4 | import nebula.test.dependencies.GradleDependencyGenerator 5 | import spock.lang.Unroll 6 | 7 | class AlignRulesDirectDependenciesSpec extends AbstractAlignRulesSpec { 8 | 9 | @Unroll 10 | def 'can align direct dependencies if necessary'() { 11 | def graph = new DependencyGraphBuilder() 12 | .addModule('test.nebula:a:1.0.0') 13 | .addModule('test.nebula:a:0.15.0') 14 | .addModule('test.nebula:b:1.0.0') 15 | .addModule('test.nebula:b:0.15.0') 16 | .build() 17 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 18 | 19 | rulesJsonFile << '''\ 20 | { 21 | "deny": [], "reject": [], "substitute": [], "replace": [], 22 | "align": [ 23 | { 24 | "name": "testNebula", 25 | "group": "test.nebula", 26 | "reason": "Align test.nebula dependencies", 27 | "author": "Example Person ", 28 | "date": "2016-03-17T20:21:20.368Z" 29 | } 30 | ] 31 | } 32 | '''.stripIndent() 33 | 34 | buildFile << """\ 35 | repositories { 36 | maven { url = '${mavenrepo.absolutePath}' } 37 | } 38 | dependencies { 39 | implementation 'test.nebula:a:1.0.0' 40 | implementation 'test.nebula:b:0.15.0' 41 | } 42 | """.stripIndent() 43 | 44 | when: 45 | def result = runTasks('dependencies', '--configuration', 'compileClasspath') 46 | 47 | then: 48 | result.output.contains '+--- test.nebula:a:1.0.0\n' 49 | result.output.contains '\\--- test.nebula:b:0.15.0 -> 1.0.0\n' 50 | 51 | } 52 | 53 | @Unroll 54 | def 'can align direct dependencies from ivy repositories'() { 55 | def graph = new DependencyGraphBuilder() 56 | .addModule('test.nebula:a:1.0.0') 57 | .addModule('test.nebula:a:0.15.0') 58 | .addModule('test.nebula:b:1.0.0') 59 | .addModule('test.nebula:b:0.15.0') 60 | .build() 61 | GradleDependencyGenerator ivyrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen") 62 | ivyrepo.generateTestIvyRepo() 63 | 64 | rulesJsonFile << '''\ 65 | { 66 | "deny": [], "reject": [], "substitute": [], "replace": [], 67 | "align": [ 68 | { 69 | "name": "testNebula", 70 | "group": "test.nebula", 71 | "reason": "Align test.nebula dependencies", 72 | "author": "Example Person ", 73 | "date": "2016-03-17T20:21:20.368Z" 74 | } 75 | ] 76 | } 77 | '''.stripIndent() 78 | 79 | buildFile << """\ 80 | repositories { 81 | ${ivyrepo.ivyRepositoryBlock} 82 | } 83 | dependencies { 84 | implementation 'test.nebula:a:1.0.0' 85 | implementation 'test.nebula:b:0.15.0' 86 | } 87 | """.stripIndent() 88 | 89 | when: 90 | def result = runTasks('dependencies', '--configuration', 'compileClasspath') 91 | 92 | then: 93 | result.output.contains '+--- test.nebula:a:1.0.0\n' 94 | result.output.contains '\\--- test.nebula:b:0.15.0 -> 1.0.0\n' 95 | } 96 | 97 | @Unroll 98 | def 'can align dynamic dependencies'() { 99 | def graph = new DependencyGraphBuilder() 100 | .addModule('test.nebula:a:1.0.0') 101 | .addModule('test.nebula:a:1.0.1') 102 | .build() 103 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 104 | 105 | rulesJsonFile << '''\ 106 | { 107 | "deny": [], "reject": [], "substitute": [], "replace": [], 108 | "align": [ 109 | { 110 | "name": "testNebula", 111 | "group": "test.nebula", 112 | "reason": "Align test.nebula dependencies", 113 | "author": "Example Person ", 114 | "date": "2016-03-17T20:21:20.368Z" 115 | } 116 | ] 117 | } 118 | '''.stripIndent() 119 | 120 | buildFile << """\ 121 | repositories { 122 | maven { url = '${mavenrepo.absolutePath}' } 123 | } 124 | dependencies { 125 | implementation 'test.nebula:a:1.+' 126 | } 127 | """.stripIndent() 128 | 129 | when: 130 | def result = runTasks('dependencies', '--configuration', 'compileClasspath') 131 | 132 | then: 133 | result.output.contains '\\--- test.nebula:a:1.+ -> 1.0.1\n' 134 | } 135 | 136 | @Unroll 137 | def 'can align dynamic range dependencies'() { 138 | def graph = new DependencyGraphBuilder() 139 | .addModule('test.nebula:a:1.0.0') 140 | .addModule('test.nebula:a:1.0.1') 141 | .build() 142 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 143 | 144 | rulesJsonFile << '''\ 145 | { 146 | "deny": [], "reject": [], "substitute": [], "replace": [], 147 | "align": [ 148 | { 149 | "name": "testNebula", 150 | "group": "test.nebula", 151 | "reason": "Align test.nebula dependencies", 152 | "author": "Example Person ", 153 | "date": "2016-03-17T20:21:20.368Z" 154 | } 155 | ] 156 | } 157 | '''.stripIndent() 158 | 159 | buildFile << """\ 160 | repositories { 161 | maven { url = '${mavenrepo.absolutePath}' } 162 | } 163 | dependencies { 164 | implementation 'test.nebula:a:[1.0.0, 2.0.0)' 165 | } 166 | """.stripIndent() 167 | 168 | when: 169 | def result = runTasks('dependencies', '--configuration', 'compileClasspath') 170 | 171 | then: 172 | result.output.contains '\\--- test.nebula:a:[1.0.0, 2.0.0) -> 1.0.1\n' 173 | } 174 | 175 | @Unroll 176 | def 'unresolvable dependencies cause assemble to fail'() { 177 | rulesJsonFile << '''\ 178 | { 179 | "deny": [], "reject": [], "substitute": [], "replace": [], 180 | "align": [ 181 | { 182 | "name": "testNebula", 183 | "group": "com.google.guava", 184 | "reason": "Align guava", 185 | "author": "Example Person ", 186 | "date": "2016-03-17T20:21:20.368Z" 187 | } 188 | ] 189 | } 190 | '''.stripIndent() 191 | 192 | buildFile << """\ 193 | repositories { mavenCentral() } 194 | dependencies { 195 | implementation 'org.slf4j:slf4j-api:1.7.21' 196 | implementation 'com.google.guava:guava:oops' 197 | } 198 | """ 199 | 200 | writeHelloWorld('com.netflix.nebula') 201 | 202 | when: 203 | org.gradle.testkit.runner.BuildResult result = runTasksAndFail('assemble') 204 | 205 | then: 206 | result.output.contains("Could not resolve all files for configuration ':compileClasspath'.") 207 | result.output.contains("Could not find com.google.guava:guava:oops.") 208 | 209 | } 210 | 211 | @Unroll('unresolvable dependencies do not cause #tasks to fail') 212 | def 'unresolvable dependencies do not cause dependencies tasks to fail'() { 213 | buildFile.delete() 214 | buildFile << """\ 215 | apply plugin: 'java' 216 | 217 | repositories { mavenCentral() } 218 | 219 | dependencies { 220 | implementation 'org.slf4j:slf4j-api:1.7.21' 221 | implementation 'com.google.guava:guava:oops' 222 | } 223 | """.stripIndent() 224 | 225 | when: 226 | runTasks(*tasks) 227 | 228 | then: 229 | noExceptionThrown() 230 | 231 | where: 232 | tasks | _ 233 | ['dependencies', '--configuration', 'compileClasspath'] | _ 234 | ['dependencyInsight', '--dependency', 'guava'] | _ 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AlignRulesForceSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | import nebula.test.dependencies.DependencyGraphBuilder 4 | import nebula.test.dependencies.GradleDependencyGenerator 5 | import spock.lang.Unroll 6 | 7 | class AlignRulesForceSpec extends AbstractAlignRulesSpec { 8 | def setup() { 9 | keepFiles = true 10 | } 11 | 12 | @Unroll 13 | def 'alignment uses #name forced version'() { 14 | def graph = new DependencyGraphBuilder() 15 | .addModule('test.nebula:a:1.0.0') 16 | .addModule('test.nebula:a:0.15.0') 17 | .addModule('test.nebula:b:1.0.0') 18 | .addModule('test.nebula:b:0.15.0') 19 | .addModule('test.nebula:c:1.0.0') 20 | .addModule('test.nebula:c:0.15.0') 21 | .addModule('test.nebula.other:a:1.0.0') 22 | .build() 23 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 24 | 25 | rulesJsonFile << '''\ 26 | { 27 | "deny": [], "reject": [], "substitute": [], "replace": [], 28 | "align": [ 29 | { 30 | "name": "testNebula", 31 | "group": "test.nebula", 32 | "reason": "Align test.nebula dependencies", 33 | "author": "Example Person ", 34 | "date": "2016-03-17T20:21:20.368Z" 35 | } 36 | ] 37 | } 38 | '''.stripIndent() 39 | 40 | buildFile << """\ 41 | repositories { 42 | maven { url = '${mavenrepo.absolutePath}' } 43 | } 44 | dependencies { 45 | implementation 'test.nebula:a:1.0.0' 46 | implementation 'test.nebula:b:1.0.0' 47 | implementation 'test.nebula:c:0.15.0' 48 | implementation 'test.nebula.other:a:1.0.0' 49 | } 50 | $force 51 | """.stripIndent() 52 | 53 | when: 54 | def tasks = ['dependencies', '--configuration', 'compileClasspath', '--warning-mode', 'none', '-s'] 55 | def result = runTasks(*tasks) 56 | 57 | 58 | then: 59 | result.output.contains '+--- test.nebula:a:1.0.0 -> 0.15.0\n' 60 | result.output.contains '+--- test.nebula:b:1.0.0 -> 0.15.0\n' 61 | result.output.contains '+--- test.nebula:c:0.15.0\n' 62 | result.output.contains '--- test.nebula.other:a:1.0.0\n' 63 | 64 | where: 65 | name | force 66 | "all" | "configurations.all { resolutionStrategy { force 'test.nebula:a:0.15.0' } }" 67 | "configuration" | "configurations.compileClasspath { resolutionStrategy { force 'test.nebula:a:0.15.0' } }" 68 | } 69 | 70 | @Unroll 71 | def 'when multiple forces are present then Core alignment fails due to multiple forces'() { 72 | def graph = new DependencyGraphBuilder() 73 | .addModule('test.nebula:a:2.0.0') 74 | .addModule('test.nebula:a:1.0.0') 75 | .addModule('test.nebula:a:0.15.0') 76 | .addModule('test.nebula:b:2.0.0') 77 | .addModule('test.nebula:b:1.0.0') 78 | .addModule('test.nebula:b:0.15.0') 79 | .addModule('test.nebula:c:2.0.0') 80 | .addModule('test.nebula:c:1.0.0') 81 | .addModule('test.nebula:c:0.15.0') 82 | .build() 83 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 84 | 85 | rulesJsonFile << '''\ 86 | { 87 | "deny": [], "reject": [], "substitute": [], "replace": [], 88 | "align": [ 89 | { 90 | "name": "testNebula", 91 | "group": "test.nebula", 92 | "reason": "Align test.nebula dependencies", 93 | "author": "Example Person ", 94 | "date": "2016-03-17T20:21:20.368Z" 95 | } 96 | ] 97 | } 98 | '''.stripIndent() 99 | 100 | buildFile << """\ 101 | repositories { 102 | maven { url = '${mavenrepo.absolutePath}' } 103 | } 104 | dependencies { 105 | implementation 'test.nebula:a:2.0.0' 106 | implementation 'test.nebula:b:2.0.0' 107 | implementation 'test.nebula:c:1.0.0' 108 | } 109 | configurations.compileClasspath.resolutionStrategy { 110 | force 'test.nebula:a:2.0.0' 111 | force 'test.nebula:b:1.0.0' 112 | force 'test.nebula:c:0.15.0' 113 | } 114 | """.stripIndent() 115 | 116 | when: 117 | def result = runTasks('dependencies', '--configuration', 'compileClasspath', '--warning-mode', 'none') 118 | def dependencyInsightResult = runTasks('dependencyInsight', '--dependency', 'test.nebula', '--warning-mode', 'none') 119 | 120 | then: 121 | assert dependencyInsightResult.output.contains('Multiple forces on different versions for virtual platform ') 122 | assert dependencyInsightResult.output.contains('Could not resolve test.nebula:a:2.0.0') 123 | assert dependencyInsightResult.output.contains('Could not resolve test.nebula:b:1.0.0. (already reported)') 124 | assert dependencyInsightResult.output.contains('Could not resolve test.nebula:c:0.15.0. (already reported)') 125 | } 126 | 127 | @Unroll 128 | def 'when dynamic forces are present then Core alignment fails due to multiple forces'() { 129 | def graph = new DependencyGraphBuilder() 130 | .addModule('test.nebula:a:2.0.0') 131 | .addModule('test.nebula:a:1.0.0') 132 | .addModule('test.nebula:a:0.15.0') 133 | .addModule('test.nebula:b:2.0.0') 134 | .addModule('test.nebula:b:1.00.0') 135 | .addModule('test.nebula:b:0.15.0') 136 | .addModule('test.nebula:c:2.0.0') 137 | .addModule('test.nebula:c:1.0.0') 138 | .addModule('test.nebula:c:0.15.0') 139 | .build() 140 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 141 | 142 | rulesJsonFile << '''\ 143 | { 144 | "deny": [], "reject": [], "substitute": [], "replace": [], 145 | "align": [ 146 | { 147 | "name": "testNebula", 148 | "group": "test.nebula", 149 | "reason": "Align test.nebula dependencies", 150 | "author": "Example Person ", 151 | "date": "2016-03-17T20:21:20.368Z" 152 | } 153 | ] 154 | } 155 | '''.stripIndent() 156 | 157 | buildFile << """\ 158 | repositories { 159 | maven { url = '${mavenrepo.absolutePath}' } 160 | } 161 | dependencies { 162 | implementation 'test.nebula:a:2.0.0' 163 | implementation 'test.nebula:b:2.0.0' 164 | implementation 'test.nebula:c:1.0.0' 165 | } 166 | configurations.compileClasspath.resolutionStrategy { 167 | force 'test.nebula:a:latest.release' 168 | force 'test.nebula:b:1.+' 169 | force 'test.nebula:c:0.15.0' 170 | } 171 | """.stripIndent() 172 | 173 | when: 174 | def tasks = ['dependencies', '--configuration', 'compileClasspath', '--warning-mode', 'none'] 175 | def result = runTasks(*tasks) 176 | def dependencyInsightResult = runTasks('dependencyInsight', '--dependency', 'test.nebula', '--warning-mode', 'none') 177 | 178 | then: 179 | assert dependencyInsightResult.output.contains('Multiple forces on different versions for virtual platform ') 180 | assert dependencyInsightResult.output.contains('Could not resolve test.nebula:a:latest.release') 181 | assert dependencyInsightResult.output.contains('Could not resolve test.nebula:b:1.+. (already reported)') 182 | assert dependencyInsightResult.output.contains('Could not resolve test.nebula:c:0.15.0. (already reported)') 183 | 184 | } 185 | 186 | @Unroll 187 | def 'alignment with latest.release force'() { 188 | def graph = new DependencyGraphBuilder() 189 | .addModule('test.nebula:a:2.0.0') 190 | .addModule('test.nebula:a:1.0.0') 191 | .addModule('test.nebula:a:0.15.0') 192 | .addModule('test.nebula:b:2.0.0') 193 | .addModule('test.nebula:b:1.0.0') 194 | .addModule('test.nebula:b:0.15.0') 195 | .addModule('test.nebula:c:2.0.0') 196 | .addModule('test.nebula:c:1.0.0') 197 | .addModule('test.nebula:c:0.15.0') 198 | .build() 199 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 200 | 201 | rulesJsonFile << '''\ 202 | { 203 | "deny": [], "reject": [], "substitute": [], "replace": [], 204 | "align": [ 205 | { 206 | "name": "testNebula", 207 | "group": "test.nebula", 208 | "reason": "Align test.nebula dependencies", 209 | "author": "Example Person ", 210 | "date": "2016-03-17T20:21:20.368Z" 211 | } 212 | ] 213 | } 214 | '''.stripIndent() 215 | 216 | buildFile << """\ 217 | repositories { 218 | maven { url = '${mavenrepo.absolutePath}' } 219 | } 220 | dependencies { 221 | implementation 'test.nebula:a:2.0.0' 222 | implementation 'test.nebula:b:1.0.0' 223 | implementation 'test.nebula:c:0.15.0' 224 | } 225 | configurations.compileClasspath.resolutionStrategy { 226 | force 'test.nebula:a:latest.release' 227 | } 228 | """.stripIndent() 229 | 230 | when: 231 | def tasks = ['dependencies', '--configuration', 'compileClasspath', '--warning-mode', 'none'] 232 | def result = runTasks(*tasks) 233 | 234 | then: 235 | result.output.contains '+--- test.nebula:a:2.0.0\n' 236 | result.output.contains '+--- test.nebula:b:1.0.0 -> 2.0.0\n' 237 | result.output.contains '\\--- test.nebula:c:0.15.0 -> 2.0.0\n' 238 | 239 | } 240 | 241 | @Unroll 242 | def 'alignment with sub-version force'() { 243 | def graph = new DependencyGraphBuilder() 244 | .addModule('test.nebula:a:2.0.0') 245 | .addModule('test.nebula:a:1.0.0') 246 | .addModule('test.nebula:a:0.15.0') 247 | .addModule('test.nebula:b:2.0.0') 248 | .addModule('test.nebula:b:1.0.0') 249 | .addModule('test.nebula:b:0.15.0') 250 | .addModule('test.nebula:c:2.0.0') 251 | .addModule('test.nebula:c:1.0.0') 252 | .addModule('test.nebula:c:0.15.0') 253 | .build() 254 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 255 | 256 | rulesJsonFile << '''\ 257 | { 258 | "deny": [], "reject": [], "substitute": [], "replace": [], 259 | "align": [ 260 | { 261 | "name": "testNebula", 262 | "group": "test.nebula", 263 | "reason": "Align test.nebula dependencies", 264 | "author": "Example Person ", 265 | "date": "2016-03-17T20:21:20.368Z" 266 | } 267 | ] 268 | } 269 | '''.stripIndent() 270 | 271 | buildFile << """\ 272 | repositories { 273 | maven { url = '${mavenrepo.absolutePath}' } 274 | } 275 | dependencies { 276 | implementation 'test.nebula:a:2.0.0' 277 | implementation 'test.nebula:b:1.0.0' 278 | implementation 'test.nebula:c:0.15.0' 279 | } 280 | configurations.compileClasspath.resolutionStrategy { 281 | force 'test.nebula:a:1.+' 282 | } 283 | """.stripIndent() 284 | 285 | when: 286 | def tasks = ['dependencies', '--configuration', 'compileClasspath', '--warning-mode', 'none'] 287 | def result = runTasks(*tasks) 288 | 289 | 290 | then: 291 | 292 | result.output.contains '+--- test.nebula:a:2.0.0 -> 1.0.0\n' 293 | result.output.contains '+--- test.nebula:b:1.0.0\n' 294 | result.output.contains '\\--- test.nebula:c:0.15.0 -> 1.0.0\n' 295 | } 296 | 297 | @Unroll 298 | def 'with multiple specific dynamic versions then Core alignment fails due to multiple forces'() { 299 | def graph = new DependencyGraphBuilder() 300 | .addModule('test.nebula:a:3.0.0') 301 | .addModule('test.nebula:a:2.0.0') 302 | .addModule('test.nebula:a:1.0.0') 303 | .addModule('test.nebula:a:0.15.0') 304 | .addModule('test.nebula:b:2.0.0') 305 | .addModule('test.nebula:b:1.0.0') 306 | .addModule('test.nebula:b:0.15.0') 307 | .addModule('test.nebula:c:2.0.0') 308 | .addModule('test.nebula:c:1.0.0') 309 | .addModule('test.nebula:c:0.15.0') 310 | .build() 311 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 312 | 313 | rulesJsonFile << '''\ 314 | { 315 | "deny": [], "reject": [], "substitute": [], "replace": [], 316 | "align": [ 317 | { 318 | "name": "testNebula", 319 | "group": "test.nebula", 320 | "reason": "Align test.nebula dependencies", 321 | "author": "Example Person ", 322 | "date": "2016-03-17T20:21:20.368Z" 323 | } 324 | ] 325 | } 326 | '''.stripIndent() 327 | 328 | buildFile << """\ 329 | repositories { 330 | maven { url = '${mavenrepo.absolutePath}' } 331 | } 332 | dependencies { 333 | implementation 'test.nebula:a:2.0.0' 334 | implementation 'test.nebula:b:1.0.0' 335 | implementation 'test.nebula:c:0.15.0' 336 | } 337 | configurations.compileClasspath.resolutionStrategy { 338 | force 'test.nebula:a:latest.release' 339 | force 'test.nebula:b:1.+' 340 | force 'test.nebula:c:[1.0, 2.0)' 341 | } 342 | """.stripIndent() 343 | 344 | when: 345 | def tasks = ['dependencies', '--configuration', 'compileClasspath', '--warning-mode', 'none'] 346 | def result = runTasks(*tasks) 347 | def dependencyInsightResult = runTasks('dependencyInsight', '--dependency', 'test.nebula', '--warning-mode', 'none') 348 | 349 | 350 | then: 351 | assert dependencyInsightResult.output.contains('Multiple forces on different versions for virtual platform ') 352 | assert dependencyInsightResult.output.contains('Could not resolve test.nebula:a:latest.release') 353 | assert dependencyInsightResult.output.contains('Could not resolve test.nebula:b:1.+. (already reported)') 354 | assert dependencyInsightResult.output.contains('Could not resolve test.nebula:c:[1.0, 2.0). (already reported)') 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AlignRulesMultiprojectSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package nebula.plugin.resolutionrules 18 | 19 | import nebula.test.IntegrationSpec 20 | import nebula.test.dependencies.DependencyGraphBuilder 21 | import nebula.test.dependencies.GradleDependencyGenerator 22 | import spock.lang.Unroll 23 | 24 | class AlignRulesMultiprojectSpec extends IntegrationSpec { 25 | def rulesJsonFile 26 | def aDir 27 | def bDir 28 | 29 | def setup() { 30 | // Avoid deprecation warnings during parallel resolution while we look for a solution 31 | System.setProperty('ignoreDeprecations', 'true') 32 | System.setProperty('ignoreMutableProjectStateWarnings', 'true') 33 | 34 | fork = false 35 | rulesJsonFile = new File(projectDir, "${moduleName}.json") 36 | buildFile << """\ 37 | allprojects { 38 | ${applyPlugin(ResolutionRulesPlugin)} 39 | 40 | 41 | group = 'test.nebula' 42 | } 43 | 44 | project(':a') { 45 | apply plugin: 'java' 46 | } 47 | 48 | project(':b') { 49 | apply plugin: 'java-library' 50 | } 51 | 52 | dependencies { 53 | resolutionRules files('$rulesJsonFile') 54 | } 55 | """.stripIndent() 56 | 57 | settingsFile << '''\ 58 | rootProject.name = 'aligntest' 59 | '''.stripIndent() 60 | 61 | aDir = addSubproject('a') 62 | bDir = addSubproject('b') 63 | } 64 | 65 | @Unroll 66 | def 'align rules do not interfere with a multiproject that produces the jars being aligned (parallel #parallel)'() { 67 | rulesJsonFile << '''\ 68 | { 69 | "deny": [], "reject": [], "substitute": [], "replace": [], 70 | "align": [ 71 | { 72 | "name": "testNebula", 73 | "group": "test.nebula", 74 | "includes": ["a", "b"], 75 | "reason": "Align test.nebula dependencies", 76 | "author": "Example Person ", 77 | "date": "2016-03-17T20:21:20.368Z" 78 | } 79 | ] 80 | } 81 | '''.stripIndent() 82 | 83 | // project b depends on a 84 | new File(bDir, 'build.gradle') << '''\ 85 | dependencies { 86 | implementation project(':a') 87 | } 88 | '''.stripIndent() 89 | 90 | buildFile << '''\ 91 | subprojects { 92 | apply plugin: 'maven-publish' 93 | 94 | publishing { 95 | publications { 96 | test(MavenPublication) { 97 | from components.java 98 | } 99 | } 100 | repositories { 101 | maven { 102 | name 'repo' 103 | url = 'build/repo' 104 | } 105 | } 106 | } 107 | } 108 | '''.stripIndent() 109 | 110 | when: 111 | def tasks = [':b:dependencies', '--configuration', 'compileClasspath'] 112 | if (parallel) { 113 | tasks += "--parallel" 114 | } 115 | def results = runTasks(*tasks) 116 | 117 | then: 118 | results.standardOutput.contains('\\--- project :a\n') 119 | 120 | where: 121 | parallel << [false, true] 122 | } 123 | 124 | @Unroll 125 | def 'cycle like behavior (parallel #parallel)'() { 126 | rulesJsonFile << '''\ 127 | { 128 | "deny": [], "reject": [], "substitute": [], "replace": [], 129 | "align": [ 130 | { 131 | "name": "testNebula", 132 | "group": "test", 133 | "reason": "Align test.nebula dependencies", 134 | "author": "Example Person ", 135 | "date": "2016-03-17T20:21:20.368Z" 136 | } 137 | ] 138 | } 139 | '''.stripIndent() 140 | 141 | new File(aDir, 'build.gradle') << '''\ 142 | dependencies { 143 | testImplementation project(':b') 144 | } 145 | '''.stripIndent() 146 | 147 | new File(bDir, 'build.gradle') << '''\ 148 | dependencies { 149 | implementation project(':a') 150 | } 151 | '''.stripIndent() 152 | 153 | when: 154 | def tasks = [':a:dependencies', ':b:dependencies', 'assemble'] 155 | if (parallel) { 156 | tasks += "--parallel" 157 | } 158 | runTasks(*tasks) 159 | 160 | then: 161 | noExceptionThrown() 162 | 163 | where: 164 | parallel << [true, false] 165 | } 166 | 167 | @Unroll 168 | def 'can align project dependencies (parallel #parallel)'() { 169 | def graph = new DependencyGraphBuilder() 170 | .addModule('other.nebula:a:0.42.0') 171 | .addModule('other.nebula:a:1.0.0') 172 | .addModule('other.nebula:a:1.1.0') 173 | .addModule('other.nebula:b:0.42.0') 174 | .addModule('other.nebula:b:1.0.0') 175 | .addModule('other.nebula:b:1.1.0') 176 | .addModule('other.nebula:c:0.42.0') 177 | .addModule('other.nebula:c:1.0.0') 178 | .addModule('other.nebula:c:1.1.0') 179 | .build() 180 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 181 | 182 | rulesJsonFile << '''\ 183 | { 184 | "deny": [], "reject": [], "substitute": [], "replace": [], 185 | "align": [ 186 | { 187 | "group": "other.nebula", 188 | "includes": [ "a", "b" ], 189 | "reason": "Align test.nebula dependencies", 190 | "author": "Example Person ", 191 | "date": "2016-03-17T20:21:20.368Z" 192 | } 193 | ] 194 | } 195 | '''.stripIndent() 196 | 197 | buildFile << """\ 198 | subprojects { 199 | repositories { 200 | maven { url = '${mavenrepo.absolutePath}' } 201 | } 202 | } 203 | 204 | project(':a') { 205 | dependencies { 206 | implementation project(':b') 207 | } 208 | } 209 | 210 | project(':b') { 211 | dependencies { 212 | api 'other.nebula:a:1.0.0' 213 | api 'other.nebula:b:1.1.0' 214 | api 'other.nebula:c:0.42.0' 215 | } 216 | } 217 | """.stripIndent() 218 | 219 | when: 220 | def tasks = [':a:dependencies', '--configuration', 'compileClasspath'] 221 | if (parallel) { 222 | tasks += "--parallel" 223 | } 224 | def result = runTasks(*tasks) 225 | 226 | then: 227 | result.standardOutput.contains '+--- other.nebula:a:1.0.0 -> 1.1.0' 228 | result.standardOutput.contains '+--- other.nebula:b:1.1.0' 229 | result.standardOutput.contains '\\--- other.nebula:c:0.42.0' 230 | 231 | where: 232 | parallel << [true, false] 233 | } 234 | 235 | @Unroll 236 | def 'root project can depend on subprojects (parallel #parallel)'() { 237 | def graph = new DependencyGraphBuilder() 238 | .addModule('other.nebula:a:0.42.0') 239 | .addModule('other.nebula:a:1.0.0') 240 | .addModule('other.nebula:a:1.1.0') 241 | .addModule('other.nebula:b:0.42.0') 242 | .addModule('other.nebula:b:1.0.0') 243 | .addModule('other.nebula:b:1.1.0') 244 | .addModule('other.nebula:c:0.42.0') 245 | .addModule('other.nebula:c:1.0.0') 246 | .addModule('other.nebula:c:1.1.0') 247 | .build() 248 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 249 | 250 | rulesJsonFile << '''\ 251 | { 252 | "deny": [], "reject": [], "substitute": [], "replace": [], 253 | "align": [ 254 | { 255 | "group": "other.nebula", 256 | "includes": [ "a", "b" ], 257 | "reason": "Align test.nebula dependencies", 258 | "author": "Example Person ", 259 | "date": "2016-03-17T20:21:20.368Z" 260 | } 261 | ] 262 | } 263 | '''.stripIndent() 264 | 265 | buildFile << """\ 266 | apply plugin: 'java' 267 | 268 | subprojects { 269 | repositories { 270 | maven { url = '${mavenrepo.absolutePath}' } 271 | } 272 | } 273 | 274 | dependencies { 275 | implementation project(':a') 276 | implementation project(':b') 277 | } 278 | 279 | project(':a') { 280 | dependencies { 281 | implementation project(':b') 282 | } 283 | } 284 | 285 | project(':b') { 286 | dependencies { 287 | api 'other.nebula:a:1.0.0' 288 | api 'other.nebula:b:1.1.0' 289 | api 'other.nebula:c:0.42.0' 290 | } 291 | } 292 | """.stripIndent() 293 | 294 | when: 295 | def tasks = [':a:dependencies', '--configuration', 'compileClasspath'] 296 | if (parallel) { 297 | tasks += "--parallel" 298 | } 299 | def result = runTasks(*tasks) 300 | 301 | then: 302 | result.standardOutput.contains '+--- other.nebula:a:1.0.0 -> 1.1.0' 303 | result.standardOutput.contains '+--- other.nebula:b:1.1.0' 304 | result.standardOutput.contains '\\--- other.nebula:c:0.42.0' 305 | 306 | where: 307 | parallel << [true, false] 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AlignRulesVersionMatchSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | import nebula.test.dependencies.DependencyGraphBuilder 4 | import nebula.test.dependencies.GradleDependencyGenerator 5 | import nebula.test.dependencies.ModuleBuilder 6 | import org.junit.Ignore 7 | 8 | @Ignore("we do not currently use VersionMatchers") 9 | class AlignRulesVersionMatchSpec extends AbstractAlignRulesSpec { 10 | def 'match excluding differences in version results in no alignment'() { 11 | def graph = new DependencyGraphBuilder() 12 | .addModule('test.nebula:a:1.0.0') 13 | .addModule(new ModuleBuilder('test.nebula:b:1.0.0-1').addDependency('test.nebula:a:1.0.0').build()) 14 | .build() 15 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 16 | 17 | rulesJsonFile << '''\ 18 | { 19 | "deny": [], "reject": [], "substitute": [], "replace": [], 20 | "align": [ 21 | { 22 | "name": "testNebula", 23 | "group": "test.nebula", 24 | "match": "EXCLUDE_SUFFIXES", 25 | "reason": "Align test.nebula dependencies", 26 | "author": "Example Person ", 27 | "date": "2016-03-17T20:21:20.368Z" 28 | } 29 | ] 30 | } 31 | '''.stripIndent() 32 | 33 | buildFile << """\ 34 | repositories { 35 | maven { url = '${mavenrepo.absolutePath}' } 36 | } 37 | dependencies { 38 | implementation 'test.nebula:b:1.0.0-1' 39 | } 40 | """.stripIndent() 41 | 42 | when: 43 | def output = runTasks('dependencies', '--configuration', 'compileClasspath').output 44 | 45 | then: 46 | output.contains '\\--- test.nebula:b:1.0.0-1\n' 47 | output.contains '\\--- test.nebula:a:1.0.0\n' 48 | } 49 | 50 | def 'match regex version alignment'() { 51 | def graph = new DependencyGraphBuilder() 52 | .addModule('test.nebula:a:1.0.0') 53 | .addModule(new ModuleBuilder('test.nebula:b:1.0.0-1').addDependency('test.nebula:a:1.0.0').build()) 54 | .build() 55 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 56 | 57 | rulesJsonFile << '''\ 58 | { 59 | "deny": [], "reject": [], "substitute": [], "replace": [], 60 | "align": [ 61 | { 62 | "name": "testNebula", 63 | "group": "test.nebula", 64 | "match": "^(\\\\d+\\\\.)?(\\\\d+\\\\.)?(\\\\*|\\\\d+)", 65 | "reason": "Align test.nebula dependencies", 66 | "author": "Example Person ", 67 | "date": "2016-03-17T20:21:20.368Z" 68 | } 69 | ] 70 | } 71 | '''.stripIndent() 72 | 73 | buildFile << """\ 74 | repositories { 75 | maven { url = '${mavenrepo.absolutePath}' } 76 | } 77 | dependencies { 78 | implementation 'test.nebula:b:1.0.0-1' 79 | } 80 | """.stripIndent() 81 | 82 | when: 83 | def output = runTasks('dependencies', '--configuration', 'compileClasspath').output 84 | 85 | then: 86 | output.contains '\\--- test.nebula:b:1.0.0-1\n' 87 | output.contains '\\--- test.nebula:a:1.0.0\n' 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/AlignRulesVersionSuffixesSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | import nebula.test.dependencies.DependencyGraphBuilder 4 | import nebula.test.dependencies.GradleDependencyGenerator 5 | import nebula.test.dependencies.ModuleBuilder 6 | import spock.lang.Unroll 7 | 8 | class AlignRulesVersionSuffixesSpec extends AbstractAlignRulesSpec { 9 | 10 | @Unroll 11 | def 'requesting a specific version with no release version available'() { 12 | def graph = new DependencyGraphBuilder() 13 | .addModule('test.nebula:a:1.0.0') 14 | .addModule(new ModuleBuilder('test.nebula:b:1.0.0-1').addDependency('test.nebula:a:1.0.0').build()) 15 | .addModule(new ModuleBuilder('test.nebula:c:1.0.0-eap-1').addDependency('test.nebula:a:1.0.0').build()) 16 | .addModule(new ModuleBuilder('test.nebula:d:1.0.0.pr.1').addDependency('test.nebula:a:1.0.0').build()) 17 | .build() 18 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 19 | 20 | rulesJsonFile << '''\ 21 | { 22 | "deny": [], "reject": [], "substitute": [], "replace": [], 23 | "align": [ 24 | { 25 | "name": "testNebula", 26 | "group": "test.nebula", 27 | "reason": "Align test.nebula dependencies", 28 | "author": "Example Person ", 29 | "date": "2016-03-17T20:21:20.368Z" 30 | } 31 | ] 32 | } 33 | '''.stripIndent() 34 | 35 | buildFile << """\ 36 | repositories { 37 | maven { url = '${mavenrepo.absolutePath}' } 38 | } 39 | dependencies { 40 | implementation 'test.nebula:b:1.0.0-1' 41 | implementation 'test.nebula:c:1.0.0-eap-1' 42 | implementation 'test.nebula:d:1.0.0.pr.1' 43 | } 44 | """.stripIndent() 45 | 46 | when: 47 | def results = runTasks('dependencies', '--configuration', 'compileClasspath') 48 | def insightResults = runTasks('dependencyInsight', '--dependency', 'test.nebula') 49 | 50 | then: 51 | results.output.contains '--- test.nebula:b:1.0.0-1\n' 52 | results.output.contains '--- test.nebula:c:1.0.0-eap-1\n' 53 | results.output.contains '--- test.nebula:d:1.0.0.pr.1\n' 54 | results.output.contains '\\--- test.nebula:a:1.0.0\n' 55 | assert insightResults.output.contains("belongs to platform aligned-platform:$moduleName-0-for-test.nebula:1.0.0") 56 | assert insightResults.output.findAll("belongs to platform aligned-platform:$moduleName-0-for-test.nebula:1.0.0").size() == 4 57 | } 58 | 59 | @Unroll 60 | def 'requesting a specific version with a release version available'() { 61 | def graph = new DependencyGraphBuilder() 62 | .addModule('test.nebula:a:1.0.0') 63 | .addModule(new ModuleBuilder('test.nebula:b:1.0.0-1').addDependency('test.nebula:a:1.0.0').build()) 64 | .addModule(new ModuleBuilder('test.nebula:b:1.0.0').addDependency('test.nebula:a:1.0.0').build()) 65 | .addModule(new ModuleBuilder('test.nebula:c:1.0.0-eap-1').addDependency('test.nebula:a:1.0.0').build()) 66 | .addModule(new ModuleBuilder('test.nebula:c:1.0.0').addDependency('test.nebula:a:1.0.0').build()) 67 | .addModule(new ModuleBuilder('test.nebula:d:1.0.0.pr.1').addDependency('test.nebula:a:1.0.0').build()) 68 | .addModule(new ModuleBuilder('test.nebula:d:1.0.0').addDependency('test.nebula:a:1.0.0').build()) 69 | .build() 70 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 71 | 72 | rulesJsonFile << '''\ 73 | { 74 | "deny": [], "reject": [], "substitute": [], "replace": [], 75 | "align": [ 76 | { 77 | "name": "testNebula", 78 | "group": "test.nebula", 79 | "reason": "Align test.nebula dependencies", 80 | "author": "Example Person ", 81 | "date": "2016-03-17T20:21:20.368Z" 82 | } 83 | ] 84 | } 85 | '''.stripIndent() 86 | 87 | buildFile << """\ 88 | repositories { 89 | maven { url = '${mavenrepo.absolutePath}' } 90 | } 91 | dependencies { 92 | implementation 'test.nebula:b:1.0.0-1' 93 | implementation 'test.nebula:c:1.0.0-eap-1' 94 | implementation 'test.nebula:d:1.0.0.pr.1' 95 | } 96 | """.stripIndent() 97 | 98 | when: 99 | def results = runTasks('dependencies', '--configuration', 'compileClasspath') 100 | def insightResults = runTasks('dependencyInsight', '--dependency', 'test.nebula') 101 | 102 | then: 103 | results.output.contains '--- test.nebula:b:1.0.0-1\n' 104 | results.output.contains '--- test.nebula:c:1.0.0-eap-1 -> 1.0.0\n' 105 | results.output.contains '--- test.nebula:d:1.0.0.pr.1 -> 1.0.0\n' 106 | results.output.contains '\\--- test.nebula:a:1.0.0\n' 107 | assert insightResults.output.contains("belongs to platform aligned-platform:$moduleName-0-for-test.nebula:1.0.0") 108 | assert insightResults.output.findAll("belongs to platform aligned-platform:$moduleName-0-for-test.nebula:1.0.0").size() == 4 109 | 110 | } 111 | 112 | @Unroll 113 | def 'requesting major.+ with no release version available'() { 114 | def graph = new DependencyGraphBuilder() 115 | .addModule('test.nebula:a:1.0.0') 116 | .addModule(new ModuleBuilder('test.nebula:b:1.0.0-1').addDependency('test.nebula:a:1.0.0').build()) 117 | .addModule(new ModuleBuilder('test.nebula:c:1.0.0-eap-1').addDependency('test.nebula:a:1.0.0').build()) 118 | .addModule(new ModuleBuilder('test.nebula:d:1.0.0.pr.1').addDependency('test.nebula:a:1.0.0').build()) 119 | .build() 120 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 121 | 122 | rulesJsonFile << '''\ 123 | { 124 | "deny": [], "reject": [], "substitute": [], "replace": [], 125 | "align": [ 126 | { 127 | "name": "testNebula", 128 | "group": "test.nebula", 129 | "reason": "Align test.nebula dependencies", 130 | "author": "Example Person ", 131 | "date": "2016-03-17T20:21:20.368Z" 132 | } 133 | ] 134 | } 135 | '''.stripIndent() 136 | 137 | buildFile << """\ 138 | repositories { 139 | maven { url = '${mavenrepo.absolutePath}' } 140 | } 141 | dependencies { 142 | implementation 'test.nebula:b:1.+' 143 | implementation 'test.nebula:c:1.+' 144 | implementation 'test.nebula:d:1.+' 145 | } 146 | """.stripIndent() 147 | 148 | when: 149 | def results = runTasks('dependencies', '--configuration', 'compileClasspath') 150 | def insightResults = runTasks('dependencyInsight', '--dependency', 'test.nebula') 151 | insightResults.output.contains("belongs to platform aligned-platform:$moduleName-0-for-test.nebula:1.0.0") 152 | insightResults.output.findAll("belongs to platform aligned-platform:$moduleName-0-for-test.nebula:1.0.0").size() == 4 153 | 154 | then: 155 | results.output.contains '--- test.nebula:b:1.+ -> 1.0.0-1\n' 156 | results.output.contains '--- test.nebula:c:1.+ -> 1.0.0-eap-1\n' 157 | results.output.contains '--- test.nebula:d:1.+ -> 1.0.0.pr.1\n' 158 | results.output.contains '\\--- test.nebula:a:1.0.0\n' 159 | } 160 | 161 | @Unroll 162 | def 'requesting major.+ with a release version available'() { 163 | def graph = new DependencyGraphBuilder() 164 | .addModule('test.nebula:a:1.0.0') 165 | .addModule(new ModuleBuilder('test.nebula:b:1.0.0-1').addDependency('test.nebula:a:1.0.0').build()) 166 | .addModule(new ModuleBuilder('test.nebula:b:1.0.0').addDependency('test.nebula:a:1.0.0').build()) 167 | .addModule(new ModuleBuilder('test.nebula:c:1.0.0-eap-1').addDependency('test.nebula:a:1.0.0').build()) 168 | .addModule(new ModuleBuilder('test.nebula:c:1.0.0').addDependency('test.nebula:a:1.0.0').build()) 169 | .addModule(new ModuleBuilder('test.nebula:d:1.0.0.pr.1').addDependency('test.nebula:a:1.0.0').build()) 170 | .addModule(new ModuleBuilder('test.nebula:d:1.0.0').addDependency('test.nebula:a:1.0.0').build()) 171 | .build() 172 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 173 | 174 | rulesJsonFile << '''\ 175 | { 176 | "deny": [], "reject": [], "substitute": [], "replace": [], 177 | "align": [ 178 | { 179 | "name": "testNebula", 180 | "group": "test.nebula", 181 | "reason": "Align test.nebula dependencies", 182 | "author": "Example Person ", 183 | "date": "2016-03-17T20:21:20.368Z" 184 | } 185 | ] 186 | } 187 | '''.stripIndent() 188 | 189 | buildFile << """\ 190 | repositories { 191 | maven { url = '${mavenrepo.absolutePath}' } 192 | } 193 | dependencies { 194 | implementation 'test.nebula:b:1.+' 195 | implementation 'test.nebula:c:1.+' 196 | implementation 'test.nebula:d:1.+' 197 | } 198 | """.stripIndent() 199 | 200 | when: 201 | def results = runTasks('dependencies', '--configuration', 'compileClasspath') 202 | def insightResults = runTasks('dependencyInsight', '--dependency', 'test.nebula') 203 | 204 | then: 205 | results.output.contains '--- test.nebula:b:1.+ -> 1.0.0-1\n' 206 | results.output.contains '--- test.nebula:c:1.+ -> 1.0.0\n' 207 | results.output.contains '--- test.nebula:d:1.+ -> 1.0.0\n' 208 | results.output.contains '\\--- test.nebula:a:1.0.0\n' 209 | assert insightResults.output.contains("belongs to platform aligned-platform:$moduleName-0-for-test.nebula:1.0.0") 210 | assert insightResults.output.findAll("belongs to platform aligned-platform:$moduleName-0-for-test.nebula:1.0.0").size() == 4 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/IgnoredConfigurationsWithRulesSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | import spock.lang.IgnoreIf 4 | 5 | 6 | class IgnoredConfigurationsWithRulesSpec extends AbstractIntegrationTestKitSpec { 7 | File rulesJsonFile 8 | 9 | def setup() { 10 | rulesJsonFile = new File(projectDir, "${moduleName}.json") 11 | definePluginOutsideOfPluginBlock = true 12 | 13 | buildFile << """ 14 | apply plugin: 'java' 15 | apply plugin: 'com.netflix.nebula.resolution-rules' 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | 21 | dependencies { 22 | resolutionRules files("$rulesJsonFile") 23 | } 24 | """.stripIndent() 25 | 26 | rulesJsonFile << """ 27 | { 28 | "substitute": [ 29 | { 30 | "module" : "bouncycastle:bcmail-jdk16", 31 | "with" : "org.bouncycastle:bcmail-jdk16:latest.release", 32 | "reason" : "The latest version of BC is required, using the new coordinate", 33 | "author" : "Danny Thomas ", 34 | "date" : "2015-10-07T20:21:20.368Z" 35 | }, 36 | { 37 | "module": "com.google.guava:guava:19.0-rc2", 38 | "with": "com.google.guava:guava:19.0-rc1", 39 | "reason" : "Guava 19.0-rc2 is not permitted, use previous release", 40 | "author" : "Danny Thomas ", 41 | "date" : "2015-10-07T20:21:20.368Z" 42 | } 43 | ] 44 | } 45 | """.stripIndent() 46 | } 47 | 48 | 49 | def 'does not substitute dependency if the configuration is ignored'() { 50 | given: 51 | buildFile << """ 52 | 53 | configurations { 54 | myIgnoredConfiguration 55 | myExtraIgnoredConfiguration 56 | } 57 | 58 | dependencies { 59 | myIgnoredConfiguration 'com.google.guava:guava:19.0-rc2' 60 | myExtraIgnoredConfiguration'bouncycastle:bcmail-jdk16:1.40' 61 | } 62 | """.stripIndent() 63 | 64 | when: 65 | def result = runTasks('dependencies', '--configuration', 'compileClasspath', '-PresolutionRulesIgnoredConfigurations=myIgnoredConfiguration,myExtraIgnoredConfiguration') 66 | 67 | then: 68 | !result.output.contains('com.google.guava:guava:19.0-rc2 -> 19.0-rc1') 69 | !result.output.contains('bouncycastle:bcmail-jdk16:1.40 -> org.bouncycastle:bcmail-jdk16:') 70 | } 71 | 72 | 73 | @IgnoreIf({ !jvm.isJava17Compatible() }) 74 | def 'does not apply for configurations housing only built artifacts'() { 75 | given: 76 | System.setProperty('ignoreDeprecations', 'true') 77 | 78 | forwardOutput = true 79 | keepFiles = true 80 | def intermediateBuildFileText = buildFile.text 81 | buildFile.delete() 82 | buildFile.createNewFile() 83 | buildFile << """ 84 | buildscript { 85 | repositories { 86 | maven { 87 | url = uri("https://plugins.gradle.org/m2/") 88 | } 89 | } 90 | dependencies { 91 | classpath("org.springframework.boot:spring-boot-gradle-plugin:3.+") 92 | } 93 | }""".stripIndent() 94 | buildFile << intermediateBuildFileText 95 | buildFile << """ 96 | apply plugin: 'org.springframework.boot' 97 | dependencies { 98 | implementation 'com.google.guava:guava:19.0-rc2' 99 | implementation 'bouncycastle:bcmail-jdk16:1.40' 100 | } 101 | tasks.named("bootJar") { 102 | mainClass = 'com.test.HelloWorldApp' 103 | } 104 | project.tasks.register("viewSpecificConfigurations").configure { 105 | it.dependsOn project.tasks.named('bootJar') 106 | it.dependsOn project.tasks.named('assemble') 107 | doLast { 108 | project.configurations.matching { it.name == 'bootArchives' || it.name == 'archives' }.each { 109 | println "Dependencies for \${it}: " + it.allDependencies 110 | println "Artifacts for \${it}: " + it.allArtifacts 111 | } 112 | } 113 | } 114 | """.stripIndent() 115 | writeJavaSourceFile(""" 116 | package com.test; 117 | 118 | class HelloWorldApp { 119 | public static void main(String[] args) { 120 | System.out.println("Hello World"); 121 | } 122 | }""".stripIndent()) 123 | new File(projectDir, 'gradle.properties').text = '''org.gradle.configuration-cache=false'''.stripIndent() 124 | 125 | when: 126 | def result = runTasks( 'bootJar', 'assemble') 127 | def resolutionResult = runTasks( 'viewSpecificConfigurations') 128 | 129 | then: 130 | !result.output.contains('FAIL') 131 | !resolutionResult.output.contains('FAIL') 132 | resolutionResult.output.contains(':jar') 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/SubstituteRulesSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | 4 | class SubstituteRulesSpec extends AbstractIntegrationTestKitSpec { 5 | File rulesJsonFile 6 | 7 | def setup() { 8 | rulesJsonFile = new File(projectDir, "${moduleName}.json") 9 | definePluginOutsideOfPluginBlock = true 10 | 11 | buildFile << """ 12 | apply plugin: 'java' 13 | apply plugin: 'com.netflix.nebula.resolution-rules' 14 | 15 | repositories { 16 | mavenCentral() 17 | } 18 | 19 | dependencies { 20 | resolutionRules files("$rulesJsonFile") 21 | } 22 | """.stripIndent() 23 | 24 | rulesJsonFile << """ 25 | { 26 | "substitute": [ 27 | { 28 | "module" : "bouncycastle:bcmail-jdk16", 29 | "with" : "org.bouncycastle:bcmail-jdk16:latest.release", 30 | "reason" : "The latest version of BC is required, using the new coordinate", 31 | "author" : "Danny Thomas ", 32 | "date" : "2015-10-07T20:21:20.368Z" 33 | }, 34 | { 35 | "module": "com.google.guava:guava:19.0-rc2", 36 | "with": "com.google.guava:guava:19.0-rc1", 37 | "reason" : "Guava 19.0-rc2 is not permitted, use previous release", 38 | "author" : "Danny Thomas ", 39 | "date" : "2015-10-07T20:21:20.368Z" 40 | }, 41 | { 42 | "module": "com.sun.jersey:jersey-bundle:(,1.18)", 43 | "with": "com.sun.jersey:jersey-bundle:1.18", 44 | "reason" : "Use a minimum version of 1.18", 45 | "author" : "Danny Thomas ", 46 | "date" : "2015-10-07T20:21:20.368Z" 47 | } 48 | ] 49 | } 50 | """.stripIndent() 51 | } 52 | 53 | def 'substitute dependency without version'() { 54 | given: 55 | buildFile << """ 56 | dependencies { 57 | implementation'bouncycastle:bcmail-jdk16:1.40' 58 | } 59 | """.stripIndent() 60 | 61 | when: 62 | def result = runTasks('dependencies', '--configuration', 'compileClasspath') 63 | 64 | then: 65 | result.output.contains('bouncycastle:bcmail-jdk16:1.40 -> org.bouncycastle:bcmail-jdk16:') 66 | } 67 | 68 | def 'substitute details are shown by dependencyInsight'() { 69 | given: 70 | buildFile << """\ 71 | dependencies { 72 | implementation'bouncycastle:bcmail-jdk16:1.40' 73 | } 74 | """.stripIndent() 75 | 76 | when: 77 | def result = runTasks('dependencyInsight', '--configuration', 'compileClasspath', '--dependency', 'bcmail-jdk16') 78 | 79 | then: 80 | !result.output.contains('org.bouncycastle:bcmail-jdk16:1.40') 81 | result.output.contains('org.bouncycastle:bcmail-jdk16:') 82 | result.output.contains('The latest version of BC is required, using the new coordinate') 83 | } 84 | 85 | def 'substitute dependency with version'() { 86 | given: 87 | buildFile << """ 88 | dependencies { 89 | implementation'com.google.guava:guava:19.0-rc2' 90 | } 91 | """.stripIndent() 92 | 93 | when: 94 | def result = runTasks('dependencies', '--configuration', 'compileClasspath') 95 | 96 | then: 97 | result.output.contains('com.google.guava:guava:19.0-rc2 -> 19.0-rc1') 98 | } 99 | 100 | def 'substitute dependency outside allowed range'() { 101 | given: 102 | buildFile << """ 103 | dependencies { 104 | implementation'com.sun.jersey:jersey-bundle:1.17' 105 | } 106 | """.stripIndent() 107 | 108 | when: 109 | def result = runTasks('dependencies', '--configuration', 'compileClasspath') 110 | 111 | then: 112 | result.output.contains('om.sun.jersey:jersey-bundle:1.17 -> 1.18') 113 | } 114 | 115 | def 'do not substitute dependency above allowed range'() { 116 | given: 117 | buildFile << """ 118 | dependencies { 119 | implementation'com.sun.jersey:jersey-bundle:1.18' 120 | } 121 | """.stripIndent() 122 | 123 | when: 124 | def result = runTasks('dependencies', '--configuration', 'compileClasspath') 125 | 126 | then: 127 | result.output.contains('om.sun.jersey:jersey-bundle:1.18\n') 128 | } 129 | 130 | def 'missing version in substitution rule'() { 131 | given: 132 | rulesJsonFile.delete() 133 | rulesJsonFile << """ 134 | { 135 | "substitute": [ 136 | { 137 | "module" : "asm:asm", 138 | "with" : "org.ow2.asm:asm", 139 | "reason" : "The asm group id changed for 4.0 and later", 140 | "author" : "Danny Thomas ", 141 | "date" : "2015-10-07T20:21:20.368Z" 142 | } 143 | ] 144 | } 145 | """.stripIndent() 146 | 147 | buildFile << """ 148 | dependencies { 149 | implementation'asm:asm:3.3.1' 150 | } 151 | """.stripIndent() 152 | 153 | when: 154 | def result = runTasksAndFail('dependencies', '--configuration', 'compileClasspath') 155 | 156 | then: 157 | result.output.contains("The dependency to be substituted (org.ow2.asm:asm) must have a version. Rule missing-version-in-substitution-rule is invalid") 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/resolutionrules/SubstituteRulesWithRangesSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | 4 | import nebula.test.dependencies.DependencyGraphBuilder 5 | import nebula.test.dependencies.GradleDependencyGenerator 6 | import nebula.test.dependencies.ModuleBuilder 7 | import spock.lang.Unroll 8 | 9 | /** 10 | * Substitutions apply to declared dependencies, not resolved ones 11 | * See: https://github.com/nebula-plugins/gradle-nebula-integration/issues/50#issuecomment-433934842 12 | */ 13 | class SubstituteRulesWithRangesSpec extends AbstractIntegrationTestKitSpec { 14 | File rulesJsonFile 15 | 16 | def setup() { 17 | definePluginOutsideOfPluginBlock = true 18 | rulesJsonFile = new File(projectDir, "${moduleName}.json") 19 | 20 | def graph = new DependencyGraphBuilder() 21 | .addModule('test.nebula:apricot:1.0') 22 | .addModule('test.nebula:apricot:1.2') 23 | .addModule('test.nebula:apricot:1.4') 24 | .addModule('test.nebula:apricot:1.4.0-dev.1+mybranch.e1c43c7') // version in the form of ..-dev.#+. 25 | .addModule('test.nebula:apricot:1.6') 26 | .addModule('test.nebula:apricot:1.8') 27 | 28 | .addModule('test.nebula:apricot:2.0') 29 | .build() 30 | def mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen") 31 | mavenrepo.generateTestMavenRepo() 32 | 33 | buildFile << """ 34 | apply plugin: 'java' 35 | apply plugin: 'com.netflix.nebula.resolution-rules' 36 | 37 | repositories { 38 | mavenCentral() 39 | ${mavenrepo.mavenRepositoryBlock} 40 | } 41 | 42 | dependencies { 43 | resolutionRules files("$rulesJsonFile") 44 | } 45 | """.stripIndent() 46 | 47 | definePluginOutsideOfPluginBlock = true 48 | keepFiles = true 49 | } 50 | 51 | @Unroll 52 | def 'substitutions apply to declared dependencies when #description'() { 53 | given: 54 | createSubstitutionRule(substituteFromRange, substituteToVersion) 55 | 56 | buildFile << """ 57 | dependencies { 58 | implementation 'test.nebula:apricot:$definedVersion' 59 | } 60 | """.stripIndent() 61 | 62 | when: 63 | def result = runTasks('dependencyInsight', '--dependency', 'apricot') 64 | 65 | then: 66 | result.output.contains("test.nebula:apricot:$definedVersion -> $substituteToVersion") 67 | 68 | where: 69 | definedVersion | substituteFromRange | substituteToVersion | description 70 | '1.0' | "(,1.4]" | '1.6' | "x is less than or equal to" 71 | '1.0' | "(,1.4)" | '1.6' | "x is less than" 72 | '1.8' | "[1.6,)" | '1.4' | "x is greater than or equal to" 73 | '1.8' | "(1.6,)" | '1.4' | "x is greater than" 74 | '1.4' | "(1.2,1.6)" | '1.8' | "x is less than and greater than" 75 | '1.4' | "[1.2,1.6]" | '1.8' | "x is less than or equal to and greater than or equal to" 76 | '1.4.0-dev.1+mybranch.e1c43c7' | "[1.2,1.6]" | '1.8' | "version string contains a 'plus'" 77 | } 78 | 79 | @Unroll 80 | def 'do not substitute declared dependencies outside of range when #description'() { 81 | given: 82 | createSubstitutionRule(substituteFromRange, '1.0') 83 | 84 | buildFile << """ 85 | dependencies { 86 | implementation 'test.nebula:apricot:$definedVersion' 87 | } 88 | """.stripIndent() 89 | 90 | when: 91 | def result = runTasks('dependencyInsight', '--dependency', 'apricot') 92 | 93 | then: 94 | result.output.contains("test.nebula:apricot:$definedVersion\n") 95 | 96 | where: 97 | definedVersion | substituteFromRange | description 98 | '1.8' | "(,1.4]" | "x is not less than or equal to" 99 | '1.8' | "(,1.4)" | "x is not less than" 100 | '1.2' | "[1.6,)" | "x is not greater than or equal to" 101 | '1.2' | "(1.6,)" | "x is not greater than" 102 | '1.8' | "(1.2,1.6)" | "x is not less than and greater than" 103 | '1.8' | "[1.2,1.6]" | "x is not less than or equal to and greater than or equal to" 104 | } 105 | 106 | @Unroll 107 | def 'do not substitute dynamic major.+ dependency when #description'() { 108 | given: 109 | createSubstitutionRule(substituteFromRange, substituteToVersion) 110 | 111 | buildFile << """ 112 | dependencies { 113 | implementation 'test.nebula:apricot:$definedVersion' 114 | } 115 | """.stripIndent() 116 | 117 | when: 118 | def result = runTasks('dependencyInsight', '--dependency', 'apricot') 119 | 120 | then: 121 | result.output.contains("test.nebula:apricot:$definedVersion -> 1.8\n") 122 | 123 | where: 124 | definedVersion | substituteFromRange | substituteToVersion | description 125 | '1.+' | "[1.6,)" | '1.4' | "x is greater than or equal to" 126 | '1.+' | "(1.6,)" | '1.4' | "x is greater than" 127 | '1.+' | "(1.2,2.0)" | '1.0' | "x is less than and greater than" 128 | '1.+' | "[1.2,2.0]" | '1.0' | "x is less than or equal to and greater than or equal to" 129 | } 130 | 131 | @Unroll 132 | def 'do not substitute dynamic major.+ dependency outside of range when #description'() { 133 | given: 134 | createSubstitutionRule(substituteFromRange, '1.0') 135 | 136 | buildFile << """ 137 | dependencies { 138 | implementation 'test.nebula:apricot:$definedVersion' 139 | } 140 | """.stripIndent() 141 | 142 | when: 143 | def result = runTasks('dependencyInsight', '--dependency', 'apricot') 144 | 145 | then: 146 | result.output.contains("test.nebula:apricot:$definedVersion -> 1.8") 147 | 148 | where: 149 | definedVersion | substituteFromRange | description 150 | '1.+' | "(,1.4]" | "x is not less than or equal to" 151 | '1.+' | "(,1.4)" | "x is not less than" 152 | '1.+' | "[2.0,)" | "x is not greater than or equal to" 153 | '1.+' | "(2.0,)" | "x is not greater than" 154 | '1.+' | "(1.2,1.6)" | "x is not less than and greater than" 155 | '1.+' | "[1.2,1.6]" | "x is not less than or equal to and greater than or equal to" 156 | } 157 | 158 | @Unroll 159 | def 'do not substitute dynamic latest.release dependency when #description'() { 160 | given: 161 | createSubstitutionRule(substituteFromRange, substituteToVersion) 162 | 163 | buildFile << """ 164 | dependencies { 165 | implementation 'test.nebula:apricot:$definedVersion' 166 | } 167 | """.stripIndent() 168 | 169 | when: 170 | def result = runTasks('dependencyInsight', '--dependency', 'apricot') 171 | 172 | then: 173 | result.output.contains("test.nebula:apricot:$definedVersion -> 2.0\n") 174 | 175 | where: 176 | definedVersion | substituteFromRange | substituteToVersion | description 177 | 'latest.release' | "[1.6,)" | '1.4' | "x is greater than or equal to" 178 | 'latest.release' | "(1.6,)" | '1.4' | "x is greater than" 179 | } 180 | 181 | @Unroll 182 | def 'do not substitute dynamic latest.release dependency outside of range when #description'() { 183 | given: 184 | createSubstitutionRule(substituteFromRange, '1.0') 185 | 186 | buildFile << """ 187 | dependencies { 188 | implementation 'test.nebula:apricot:$definedVersion' 189 | } 190 | """.stripIndent() 191 | 192 | when: 193 | def result = runTasks('dependencyInsight', '--dependency', 'apricot') 194 | 195 | then: 196 | result.output.contains("test.nebula:apricot:$definedVersion -> 2.0\n") 197 | 198 | where: 199 | definedVersion | substituteFromRange | description 200 | 'latest.release' | "(,1.4]" | "x is not less than or equal to" 201 | 'latest.release' | "(,1.4)" | "x is not less than" 202 | 'latest.release' | "(1.2,1.6)" | "x is not less than and greater than" 203 | 'latest.release' | "[1.2,1.6]" | "x is not less than or equal to and greater than or equal to" 204 | } 205 | 206 | @Unroll 207 | def 'substitutions apply to transitive dependencies where #description'() { 208 | given: 209 | def graph = new DependencyGraphBuilder() 210 | .addModule(new ModuleBuilder('test.nebula:blueberry:5.0').addDependency("test.nebula:apricot:$definedVersion").build()) 211 | .build() 212 | def mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen") 213 | mavenrepo.generateTestMavenRepo() 214 | 215 | createSubstitutionRule(substituteFromRange, substituteToVersion) 216 | 217 | buildFile << """ 218 | dependencies { 219 | implementation 'test.nebula:blueberry:5.0' 220 | } 221 | """.stripIndent() 222 | 223 | when: 224 | def result = runTasks('dependencyInsight', '--dependency', 'apricot') 225 | 226 | then: 227 | result.output.contains("test.nebula:apricot:$definedVersion -> $substituteToVersion") 228 | 229 | where: 230 | definedVersion | substituteFromRange | substituteToVersion | description 231 | '1.0' | "(,1.4]" | '1.6' | "x is less than or equal to" 232 | '1.0' | "(,1.4)" | '1.6' | "x is less than" 233 | '1.8' | "[1.6,)" | '1.4' | "x is greater than or equal to" 234 | '1.8' | "(1.6,)" | '1.4' | "x is greater than" 235 | '1.4' | "(1.2,1.6)" | '1.8' | "x is less than and greater than" 236 | '1.4' | "[1.2,1.6]" | '1.8' | "x is less than or equal to and greater than or equal to" 237 | '1.4.0-dev.1+mybranch.e1c43c7' | "[1.2,1.6]" | '1.8' | "version string contains a 'plus'" 238 | } 239 | 240 | @Unroll 241 | def 'do not substitute transitive declared dependencies outside of range when #description'() { 242 | given: 243 | def graph = new DependencyGraphBuilder() 244 | .addModule(new ModuleBuilder('test.nebula:blueberry:5.0').addDependency("test.nebula:apricot:$definedVersion").build()) 245 | .build() 246 | def mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen") 247 | mavenrepo.generateTestMavenRepo() 248 | 249 | createSubstitutionRule(substituteFromRange, '1.0') 250 | 251 | buildFile << """ 252 | dependencies { 253 | implementation 'test.nebula:blueberry:5.0' 254 | } 255 | """.stripIndent() 256 | 257 | when: 258 | def result = runTasks('dependencyInsight', '--dependency', 'apricot') 259 | 260 | then: 261 | result.output.contains("test.nebula:apricot:$definedVersion\n") 262 | 263 | where: 264 | definedVersion | substituteFromRange | description 265 | '1.8' | "(,1.4]" | "x is not less than or equal to" 266 | '1.8' | "(,1.4)" | "x is not less than" 267 | '1.2' | "[1.6,)" | "x is not greater than or equal to" 268 | '1.2' | "(1.6,)" | "x is not greater than" 269 | '1.8' | "(1.2,1.6)" | "x is not less than and greater than" 270 | '1.8' | "[1.2,1.6]" | "x is not less than or equal to and greater than or equal to" 271 | } 272 | 273 | private File createSubstitutionRule(String substituteFromRange, substituteToVersion) { 274 | assert substituteFromRange != null 275 | 276 | rulesJsonFile << """ 277 | { 278 | "substitute": [ 279 | { 280 | "module": "test.nebula:apricot:$substituteFromRange", 281 | "with": "test.nebula:apricot:$substituteToVersion", 282 | "reason" : "this version is better", 283 | "author": "Example Person ", 284 | "date": "2016-03-17T20:21:20.368Z" 285 | } 286 | ] 287 | } 288 | """.stripIndent() 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/integTest/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 22 | 23 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/kotlin/nebula/plugin/resolutionrules/alignRule.kt: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.artifacts.* 5 | import org.gradle.api.artifacts.ModuleVersionIdentifier 6 | import org.gradle.api.logging.Logger 7 | import org.gradle.api.logging.Logging 8 | import java.io.Serializable 9 | import java.util.concurrent.ConcurrentHashMap 10 | import java.util.regex.Matcher 11 | import java.util.regex.Pattern 12 | import javax.inject.Inject 13 | 14 | data class AlignRule(val name: String?, 15 | val group: Regex, 16 | val includes: List = emptyList(), 17 | val excludes: List = emptyList(), 18 | val match: String?, 19 | override var ruleSet: String?, 20 | override val reason: String, 21 | override val author: String, 22 | override val date: String, 23 | var belongsToName: String?) : BasicRule, Serializable { 24 | 25 | private val groupPattern = group.toPattern() 26 | private val includesPatterns = includes.map { it.toPattern() } 27 | private val excludesPatterns = excludes.map { it.toPattern() } 28 | private val alignMatchers = ConcurrentHashMap() 29 | 30 | override fun apply(project: Project, 31 | configuration: Configuration, 32 | resolutionStrategy: ResolutionStrategy, 33 | extension: NebulaResolutionRulesExtension) { 34 | //TODO this rule is applied repeatedly for each configuration. Ideally it should be taken out and 35 | //applied only once per project 36 | if (configuration.name == "compileClasspath") { // This is one way to ensure it'll be run for only one configuration 37 | project.dependencies.components.all(AlignedPlatformMetadataRule::class.java) { 38 | it.params(this) 39 | } 40 | } 41 | } 42 | 43 | fun ruleMatches(dep: ModuleVersionIdentifier) = ruleMatches(dep.group, dep.name) 44 | 45 | fun ruleMatches(group: String, name: String) = alignMatchers.computeIfAbsent(Thread.currentThread()) { 46 | AlignMatcher(this, groupPattern, includesPatterns, excludesPatterns) 47 | }.matches(group, name) 48 | } 49 | 50 | class AlignMatcher(val rule: AlignRule, groupPattern: Pattern, includesPatterns: List, excludesPatterns: List) { 51 | private val groupMatcher = groupPattern.matcher("") 52 | private val includeMatchers = includesPatterns.map { it.matcher("") } 53 | private val excludeMatchers = excludesPatterns.map { it.matcher("") } 54 | 55 | private fun Matcher.matches(input: String, type: String): Boolean { 56 | reset(input) 57 | return try { 58 | matches() 59 | } catch (e: Exception) { 60 | throw java.lang.IllegalArgumentException("Failed to use matcher '$this' from type '$type' to match '$input'\n" + 61 | "Rule: $rule", e) 62 | } 63 | } 64 | 65 | fun matches(group: String, name: String): Boolean { 66 | return groupMatcher.matches(group, "group") && 67 | (includeMatchers.isEmpty() || includeMatchers.any { it.matches(name, "includes") }) && 68 | (excludeMatchers.isEmpty() || excludeMatchers.none { it.matches(name, "excludes") }) 69 | } 70 | } 71 | 72 | //@CacheableRule 73 | open class AlignedPlatformMetadataRule @Inject constructor(val rule: AlignRule) : ComponentMetadataRule, Serializable { 74 | private val logger: Logger = Logging.getLogger(AlignedPlatformMetadataRule::class.java) 75 | 76 | override fun execute(componentMetadataContext: ComponentMetadataContext?) { 77 | modifyDetails(componentMetadataContext!!.details) 78 | } 79 | 80 | fun modifyDetails(details: ComponentMetadataDetails) { 81 | if (rule.ruleMatches(details.id)) { 82 | details.belongsTo("aligned-platform:${rule.belongsToName}:${details.id.version}") 83 | logger.debug("Aligning platform based on '${details.id.group}:${details.id.name}:${details.id.version}' from align rule with group '${rule.group}'") 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/kotlin/nebula/plugin/resolutionrules/configurations.kt: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | import org.gradle.api.artifacts.Configuration 4 | import org.gradle.api.internal.artifacts.configurations.ConfigurationInternal 5 | import java.lang.reflect.Field 6 | import java.lang.reflect.Modifier 7 | 8 | 9 | /** 10 | * Various reflection hackiness follows due to deficiencies in the Gradle configuration APIs: 11 | * 12 | * - We can't add the configuration to the configuration container to get the addAction handlers, because it causes ConcurrentModificationExceptions 13 | * - We can't set the configuration name on copyRecursive, which makes for confusing logging output when we're resolving our configurations 14 | */ 15 | fun Any.setField(name: String, value: Any) { 16 | val field = javaClass.findDeclaredField(name) 17 | field.isAccessible = true 18 | 19 | lateinit var modifiersField: Field 20 | try { 21 | modifiersField = Field::class.java.getDeclaredField("modifiers") 22 | } catch (e: NoSuchFieldException) { 23 | try { 24 | val getDeclaredFields0 = Class::class.java.getDeclaredMethod("getDeclaredFields0", Boolean::class.javaPrimitiveType) 25 | val accessibleBeforeSet: Boolean = getDeclaredFields0.isAccessible 26 | getDeclaredFields0.isAccessible = true 27 | @Suppress("UNCHECKED_CAST") val declaredFields = getDeclaredFields0.invoke(Field::class.java, false) as Array 28 | getDeclaredFields0.isAccessible = accessibleBeforeSet 29 | for (declaredField in declaredFields) { 30 | if ("modifiers" == declaredField.name) { 31 | modifiersField = declaredField 32 | break 33 | } 34 | } 35 | } catch (ex: Exception) { 36 | e.addSuppressed(ex) 37 | throw e 38 | } 39 | } 40 | modifiersField.isAccessible = true 41 | modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv()) 42 | 43 | field.set(this, value) 44 | } 45 | 46 | tailrec fun Class.findDeclaredField(name: String): Field { 47 | val field = declaredFields 48 | .filter { it.name == name } 49 | .singleOrNull() 50 | if (field != null) { 51 | return field 52 | } else if (superclass != null) { 53 | return superclass.findDeclaredField(name) 54 | } 55 | throw IllegalArgumentException("Could not find field $name") 56 | } 57 | 58 | fun Configuration.getObservedState(): Configuration.State { 59 | val f: Field = this::class.java.findDeclaredField("observedState") 60 | f.isAccessible = true 61 | val resolvedState = f.get(this) as ConfigurationInternal.InternalState 62 | return if(resolvedState != ConfigurationInternal.InternalState.UNRESOLVED) 63 | Configuration.State.RESOLVED else Configuration.State.UNRESOLVED 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/main/kotlin/nebula/plugin/resolutionrules/extensions.kt: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | import org.gradle.api.Project 4 | 5 | inline fun Collection.mapToSet(transform: (T) -> R): Set { 6 | return mapTo(LinkedHashSet(size), transform) 7 | } 8 | 9 | fun Project.findStringProperty(name: String): String? = if (hasProperty(name)) property(name) as String? else null 10 | 11 | fun parseRuleNames(ruleNames: String): Set = 12 | ruleNames.split(",").map { it.trim() }.filter { it.isNotEmpty()} .toSet() -------------------------------------------------------------------------------- /src/main/kotlin/nebula/plugin/resolutionrules/json.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2017 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package nebula.plugin.resolutionrules 18 | 19 | import com.fasterxml.jackson.databind.* 20 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 21 | 22 | fun objectMapper(): ObjectMapper { 23 | return jacksonObjectMapper() 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/nebula/plugin/resolutionrules/plugin.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package nebula.plugin.resolutionrules 18 | 19 | import com.fasterxml.jackson.module.kotlin.readValue 20 | import com.netflix.nebula.interop.onExecute 21 | import org.gradle.api.Plugin 22 | import org.gradle.api.Project 23 | import org.gradle.api.artifacts.Configuration 24 | import org.gradle.api.artifacts.ConfigurationContainer 25 | import org.gradle.api.logging.Logger 26 | import org.gradle.api.logging.Logging 27 | import org.gradle.api.provider.Property 28 | import org.gradle.api.provider.Provider 29 | import org.gradle.api.services.BuildService 30 | import org.gradle.api.services.BuildServiceParameters 31 | import java.io.File 32 | import java.io.Serializable 33 | import java.util.* 34 | import java.util.stream.Collectors 35 | import java.util.stream.Stream 36 | import java.util.zip.ZipFile 37 | import javax.inject.Inject 38 | import kotlin.collections.ArrayList 39 | import kotlin.collections.LinkedHashMap 40 | 41 | const val RESOLUTION_RULES_CONFIG_NAME = "resolutionRules" 42 | 43 | class ResolutionRulesPlugin : Plugin { 44 | private lateinit var project: Project 45 | private lateinit var configurations: ConfigurationContainer 46 | private lateinit var extension: NebulaResolutionRulesExtension 47 | private val ignoredConfigurationPrefixes = listOf( 48 | RESOLUTION_RULES_CONFIG_NAME, 49 | SPRING_VERSION_MANAGEMENT_CONFIG_NAME, 50 | NEBULA_RECOMMENDER_BOM_CONFIG_NAME, 51 | SCALA_INCREMENTAL_ANALYSIS_CONFIGURATION_PREFIX, 52 | KTLINT_CONFIGURATION_PREFIX, 53 | REPOSITORY_CONTENT_DESCRIPTOR_CONFIGURATION_PREFIX, 54 | BOOT_ARCHIVES_CONFIGURATION_NAME, 55 | ARCHIVES_CONFIGURATION_NAME, 56 | ) 57 | private val ignoredConfigurationSuffixes = listOf(PMD_CONFIGURATION_SUFFIX) 58 | 59 | companion object { 60 | val Logger: Logger = Logging.getLogger(ResolutionRulesPlugin::class.java) 61 | 62 | const val NEBULA_RECOMMENDER_BOM_CONFIG_NAME: String = "nebulaRecommenderBom" 63 | const val SPRING_VERSION_MANAGEMENT_CONFIG_NAME = "versionManagement" 64 | const val KTLINT_CONFIGURATION_PREFIX = "ktlint" 65 | const val PMD_CONFIGURATION_SUFFIX = "PmdAuxClasspath" 66 | const val SCALA_INCREMENTAL_ANALYSIS_CONFIGURATION_PREFIX = "incrementalScalaAnalysis" 67 | const val REPOSITORY_CONTENT_DESCRIPTOR_CONFIGURATION_PREFIX = "repositoryContentDescriptor" 68 | const val BOOT_ARCHIVES_CONFIGURATION_NAME = "bootArchives" 69 | const val ARCHIVES_CONFIGURATION_NAME = "archives" 70 | const val OPTIONAL_PREFIX = "optional-" 71 | const val OPTIONAL_RULES_PROJECT_PROPERTY = "nebulaResolutionRules.optional" 72 | const val INCLUDE_RULES_PROJECT_PROPERTY = "nebulaResolutionRules.include" 73 | const val EXCLUDE_RULES_PROJECT_PROPERTY = "nebulaResolutionRules.exclude" 74 | } 75 | 76 | override fun apply(project: Project) { 77 | this.project = project 78 | configurations = project.configurations 79 | extension = 80 | project.extensions.create("nebulaResolutionRules", NebulaResolutionRulesExtension::class.java, project) 81 | addRulesFromProjectProperties(project, extension) 82 | val rootProject = project.rootProject 83 | val configuration = project.configurations.maybeCreate(RESOLUTION_RULES_CONFIG_NAME) 84 | if (project != rootProject) { 85 | configuration.isCanBeConsumed = false 86 | val rootProjectDependency = project.dependencies.project( 87 | mapOf("path" to rootProject.path, "configuration" to RESOLUTION_RULES_CONFIG_NAME) 88 | ) 89 | configuration.withDependencies { dependencies -> 90 | dependencies.add(rootProjectDependency) 91 | } 92 | } 93 | if (rootProject.extensions.findByType(NebulaResolutionRulesExtension::class.java) == null) { 94 | rootProject.extensions.create( 95 | "nebulaResolutionRules", 96 | NebulaResolutionRulesExtension::class.java, 97 | rootProject 98 | ) 99 | } 100 | 101 | project.configurations.all { config -> 102 | if (ignoredConfigurationPrefixes.any { config.name.startsWith(it) }) { 103 | return@all 104 | } 105 | 106 | if (ignoredConfigurationSuffixes.any { config.name.endsWith(it) }) { 107 | return@all 108 | } 109 | 110 | var dependencyRulesApplied = false 111 | project.onExecute { 112 | val ruleSet = extension.ruleSet() 113 | when { 114 | config.state != Configuration.State.UNRESOLVED || config.getObservedState() != Configuration.State.UNRESOLVED -> Logger.warn( 115 | "Dependency resolution rules will not be applied to $config, it was resolved before the project was executed" 116 | ) 117 | else -> { 118 | ruleSet.dependencyRulesPartOne().forEach { rule -> 119 | rule.apply(project, config, config.resolutionStrategy, extension) 120 | } 121 | 122 | ruleSet.dependencyRulesPartTwo().forEach { rule -> 123 | rule.apply(project, config, config.resolutionStrategy, extension) 124 | } 125 | dependencyRulesApplied = true 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | /** 133 | * Search for optional, include and exclude rules in project properties 134 | * Add them to the extension if found 135 | */ 136 | private fun addRulesFromProjectProperties( 137 | project: Project, 138 | extension: NebulaResolutionRulesExtension 139 | ) { 140 | val optionalRules = project.findStringProperty(OPTIONAL_RULES_PROJECT_PROPERTY) 141 | optionalRules?.let { rules -> parseRuleNames(rules).forEach { extension.optional.add(it) } } 142 | val includeRules = project.findStringProperty(INCLUDE_RULES_PROJECT_PROPERTY) 143 | includeRules?.let { rules -> parseRuleNames(rules).forEach { extension.include.add(it) } } 144 | val excludeRules = project.findStringProperty(EXCLUDE_RULES_PROJECT_PROPERTY) 145 | excludeRules?.let { rules -> parseRuleNames(rules).forEach { extension.exclude.add(it) } } 146 | } 147 | 148 | } 149 | 150 | abstract class NebulaResolutionRulesService : BuildService { 151 | companion object { 152 | private val Logger: Logger = Logging.getLogger(NebulaResolutionRulesService::class.java) 153 | private val Mapper = objectMapper() 154 | 155 | fun registerService(project: Project): Provider { 156 | return project.gradle.sharedServices.registerIfAbsent( 157 | "nebulaResolutionRules", 158 | NebulaResolutionRulesService::class.java 159 | ) { spec -> 160 | val resolutionRules = resolveResolutionRules(project) 161 | spec.parameters.getResolutionRules().set(ResolutionRules(resolutionRules)) 162 | } 163 | } 164 | 165 | private fun resolveResolutionRules(project: Project): Map { 166 | val configuration = project.configurations.getByName(RESOLUTION_RULES_CONFIG_NAME) 167 | configuration.resolve().stream().use { stream -> 168 | return stream.flatMap { file -> 169 | when (file.extension) { 170 | "json" -> { 171 | Logger.debug("nebula.resolution-rules uses: {}", file.name) 172 | Stream.of(file.absolutePath to file.readBytes()) 173 | } 174 | "jar", "zip" -> { 175 | Logger.info("nebula.resolution-rules is using ruleset: {}", file.name) 176 | val zipFile = ZipFile(file) 177 | Collections.list(zipFile.entries()).stream() 178 | .onClose(zipFile::close) 179 | .flatMap { entry -> 180 | val entryFile = File(entry.name) 181 | if (entryFile.extension == "json") { 182 | Stream.of("${file.absolutePath}!${entry.name}" to zipFile.getInputStream(entry).readBytes()) 183 | } else Stream.empty() 184 | } 185 | } 186 | else -> { 187 | Logger.debug("Unsupported rules file extension for {}", file) 188 | Stream.empty() 189 | } 190 | } 191 | }.parallel() 192 | .map { (path, bytes) -> 193 | val filePath = if (path.contains("!")) path.substringAfter("!") else path 194 | val file = File(filePath) 195 | val ruleSetName = file.nameWithoutExtension 196 | Logger.debug("Using {} ({}) a dependency rules source", ruleSetName, path) 197 | Mapper.readValue(bytes).withName(ruleSetName) 198 | }.collect( 199 | Collectors.toMap( 200 | { it.name }, 201 | { it }, 202 | { r1, r2 -> 203 | Logger.info("Found rules with the same name. Overriding existing ruleset {}", r1.name) 204 | r2 205 | }, 206 | { LinkedHashMap() }) 207 | ) 208 | } 209 | } 210 | } 211 | 212 | interface Params : BuildServiceParameters { 213 | fun getResolutionRules(): Property 214 | } 215 | 216 | class ResolutionRules(val byFile: Map) : Serializable 217 | } 218 | 219 | open class NebulaResolutionRulesExtension @Inject constructor(private val project: Project) { 220 | var include = ArrayList() 221 | set(value) { 222 | field.addAll(value) 223 | } 224 | 225 | var optional = ArrayList() 226 | set(value) { 227 | field.addAll(value) 228 | } 229 | var exclude = ArrayList() 230 | // Setter should add to the existing array rather than replacing all values 231 | set(value) { 232 | field.addAll(value) 233 | } 234 | 235 | fun ruleSet(): RuleSet { 236 | val service = NebulaResolutionRulesService.registerService(project).get() 237 | val rulesByFile = service.parameters 238 | .getResolutionRules() 239 | .get() 240 | .byFile 241 | return rulesByFile.filterKeys { ruleSet -> 242 | when { 243 | ruleSet.startsWith(ResolutionRulesPlugin.OPTIONAL_PREFIX) -> { 244 | val ruleSetWithoutPrefix = ruleSet.substring(ResolutionRulesPlugin.OPTIONAL_PREFIX.length) 245 | optional.contains(ruleSetWithoutPrefix) 246 | } 247 | include.isNotEmpty() -> include.contains(ruleSet) 248 | else -> !exclude.contains(ruleSet) 249 | } 250 | }.values.flatten() 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/main/kotlin/nebula/plugin/resolutionrules/rules.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package nebula.plugin.resolutionrules 18 | 19 | import com.netflix.nebula.interop.VersionWithSelector 20 | import org.gradle.api.Action 21 | import org.gradle.api.Project 22 | import org.gradle.api.artifacts.* 23 | import org.gradle.api.artifacts.component.ModuleComponentSelector 24 | import org.gradle.api.internal.artifacts.DefaultModuleIdentifier 25 | import org.gradle.api.internal.artifacts.DefaultModuleVersionIdentifier 26 | import org.gradle.api.internal.artifacts.ivyservice.dependencysubstitution.DefaultDependencySubstitutions 27 | import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.ExactVersionSelector 28 | import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.VersionSelector 29 | import java.io.Serializable 30 | 31 | interface Rule : Serializable { 32 | fun apply( 33 | project: Project, 34 | configuration: Configuration, 35 | resolutionStrategy: ResolutionStrategy, 36 | extension: NebulaResolutionRulesExtension 37 | ) 38 | } 39 | 40 | interface BasicRule : Rule { 41 | var ruleSet: String? 42 | val reason: String 43 | val author: String 44 | val date: String 45 | } 46 | 47 | interface ModuleRule : BasicRule { 48 | val module: String 49 | } 50 | 51 | data class RuleSet( 52 | val replace: List = emptyList(), 53 | val substitute: List = emptyList(), 54 | val reject: List = emptyList(), 55 | val deny: List = emptyList(), 56 | val exclude: List = emptyList(), 57 | val align: List = emptyList() 58 | ) : Serializable { 59 | 60 | lateinit var name: String 61 | 62 | fun dependencyRulesPartOne() = 63 | listOf(replace, deny, exclude).flatten() + listOf(SubstituteRules(substitute), RejectRules(reject)) 64 | 65 | fun dependencyRulesPartTwo() = listOf(align).flatten() 66 | 67 | fun generateAlignmentBelongsToName() { 68 | align.forEachIndexed { index, alignRule -> 69 | var abbreviatedAlignGroup = alignRule.group.toString() 70 | .replace("|", "-or-") 71 | 72 | val onlyAlphabeticalRegex = Regex("[^A-Za-z.\\-]") 73 | abbreviatedAlignGroup = onlyAlphabeticalRegex.replace(abbreviatedAlignGroup, "") 74 | 75 | alignRule.belongsToName = "$name-$index-for-$abbreviatedAlignGroup" 76 | } 77 | } 78 | } 79 | 80 | fun RuleSet.withName(ruleSetName: String): RuleSet { 81 | name = ruleSetName 82 | listOf(replace, substitute, reject, deny, exclude, align).flatten().forEach { it.ruleSet = ruleSetName } 83 | generateAlignmentBelongsToName() 84 | return this 85 | } 86 | 87 | fun Collection.flatten() = RuleSet( 88 | flatMap { it.replace }, 89 | flatMap { it.substitute }, 90 | flatMap { it.reject }, 91 | flatMap { it.deny }, 92 | flatMap { it.exclude }, 93 | flatMap { it.align }) 94 | 95 | data class ReplaceRule( 96 | override val module: String, 97 | val with: String, 98 | override var ruleSet: String?, 99 | override val reason: String, 100 | override val author: String, 101 | override val date: String 102 | ) : ModuleRule { 103 | private val moduleId = module.toModuleId() 104 | private val withId = with.toModuleId() 105 | 106 | override fun apply( 107 | project: Project, 108 | configuration: Configuration, 109 | resolutionStrategy: ResolutionStrategy, 110 | extension: NebulaResolutionRulesExtension 111 | ) { 112 | project.dependencies.modules.module(moduleId) { 113 | val details = it as ComponentModuleMetadataDetails 114 | val message = "replaced $module -> $with because '$reason' by rule $ruleSet" 115 | details.replacedBy(withId, message) 116 | } 117 | } 118 | } 119 | 120 | data class SubstituteRule( 121 | val module: String, val with: String, override var ruleSet: String?, 122 | override val reason: String, override val author: String, override val date: String 123 | ) : BasicRule, Serializable { 124 | @Transient lateinit var substitutedVersionId: ModuleVersionIdentifier 125 | @Transient lateinit var withComponentSelector: ModuleComponentSelector 126 | @Transient lateinit var versionSelector: VersionSelector 127 | 128 | override fun apply( 129 | project: Project, 130 | configuration: Configuration, 131 | resolutionStrategy: ResolutionStrategy, 132 | extension: NebulaResolutionRulesExtension 133 | ) { 134 | throw UnsupportedOperationException("Substitution rules cannot be applied directly and must be applied via SubstituteRules") 135 | } 136 | 137 | fun isInitialized(): Boolean = this::substitutedVersionId.isInitialized 138 | 139 | fun acceptsVersion(version: String): Boolean { 140 | return if (substitutedVersionId.version.isNotEmpty()) { 141 | when (VersionWithSelector(version).asSelector()) { 142 | is ExactVersionSelector -> versionSelector.accept(version) 143 | else -> false 144 | } 145 | } else true 146 | } 147 | } 148 | 149 | class SubstituteRules(val rules: List) : Rule { 150 | companion object { 151 | private val SUBSTITUTIONS_ADD_RULE = DefaultDependencySubstitutions::class.java.getDeclaredMethod( 152 | "addSubstitution", 153 | Action::class.java, 154 | Boolean::class.java 155 | ).apply { isAccessible = true } 156 | } 157 | 158 | @Transient private lateinit var rulesById: Map> 159 | 160 | override fun apply( 161 | project: Project, 162 | configuration: Configuration, 163 | resolutionStrategy: ResolutionStrategy, 164 | extension: NebulaResolutionRulesExtension 165 | ) { 166 | if (!this::rulesById.isInitialized) { 167 | val substitution = resolutionStrategy.dependencySubstitution 168 | rulesById = rules.map { rule -> 169 | if (!rule.isInitialized()) { 170 | rule.substitutedVersionId = rule.module.toModuleVersionId() 171 | val withModule = substitution.module(rule.with) 172 | if (withModule !is ModuleComponentSelector) { 173 | throw SubstituteRuleMissingVersionException(rule.with, rule) 174 | } 175 | rule.withComponentSelector = withModule 176 | rule.versionSelector = VersionWithSelector(rule.substitutedVersionId.version).asSelector() 177 | } 178 | rule 179 | }.groupBy { it.substitutedVersionId.module } 180 | .mapValues { entry -> entry.value.sortedBy { it.substitutedVersionId.version } } 181 | } 182 | 183 | val substitutionAction = Action { details -> 184 | val requested = details.requested 185 | if (requested is ModuleComponentSelector) { 186 | val rules = rulesById[requested.moduleIdentifier] ?: return@Action 187 | rules.forEach { rule -> 188 | val withComponentSelector = rule.withComponentSelector 189 | if (rule.acceptsVersion(requested.version)) { 190 | val message = 191 | "substituted ${rule.substitutedVersionId} with $withComponentSelector because '${rule.reason}' by rule ${rule.ruleSet}" 192 | details.useTarget( 193 | withComponentSelector, 194 | message 195 | ) 196 | return@Action 197 | } 198 | } 199 | } 200 | } 201 | 202 | /* 203 | * Unfortunately impossible to avoid an internal/protected method dependency for now: 204 | * 205 | * - We can't dependencySubstitutions.all because it causes the configuration to be resolved at task graph calculation time due to the possibility of project substitutions there 206 | * - Likewise eachDependency has it's own performance issues - https://github.com/gradle/gradle/issues/16151 207 | * 208 | * There's no alternative to all that only allows module substitution and we only ever substitute modules for modules, so this is completely safe. 209 | */ 210 | SUBSTITUTIONS_ADD_RULE.invoke(resolutionStrategy.dependencySubstitution, substitutionAction, false) 211 | } 212 | } 213 | 214 | data class RejectRule( 215 | override val module: String, 216 | override var ruleSet: String?, 217 | override val reason: String, 218 | override val author: String, 219 | override val date: String 220 | ) : ModuleRule { 221 | val moduleVersionId = module.toModuleVersionId() 222 | @Transient lateinit var versionSelector: VersionSelector 223 | 224 | override fun apply( 225 | project: Project, 226 | configuration: Configuration, 227 | resolutionStrategy: ResolutionStrategy, 228 | extension: NebulaResolutionRulesExtension 229 | ) { 230 | throw UnsupportedOperationException("Reject rules cannot be applied directly and must be applied via RejectRules") 231 | } 232 | 233 | fun hasVersionSelector(): Boolean = this::versionSelector.isInitialized 234 | } 235 | 236 | data class RejectRules(val rules: List) : Rule { 237 | private val ruleByModuleIdentifier = rules.groupBy { it.moduleVersionId.module } 238 | 239 | override fun apply( 240 | project: Project, 241 | configuration: Configuration, 242 | resolutionStrategy: ResolutionStrategy, 243 | extension: NebulaResolutionRulesExtension 244 | ) { 245 | resolutionStrategy.componentSelection.all { selection -> 246 | val candidate = selection.candidate 247 | val rules = ruleByModuleIdentifier[candidate.moduleIdentifier] ?: return@all 248 | rules.forEach { rule -> 249 | rule.versionSelector = VersionWithSelector(rule.moduleVersionId.version).asSelector() 250 | if (!rule.hasVersionSelector() || rule.versionSelector.accept(candidate.version)) { 251 | val message = "rejected by rule ${rule.ruleSet} because '${rule.reason}'" 252 | selection.reject(message) 253 | if (!rule.hasVersionSelector()) { 254 | return@forEach 255 | } 256 | } 257 | } 258 | } 259 | } 260 | } 261 | 262 | data class DenyRule( 263 | override val module: String, 264 | override var ruleSet: String?, 265 | override val reason: String, 266 | override val author: String, 267 | override val date: String 268 | ) : ModuleRule { 269 | private val moduleVersionId = module.toModuleVersionId() 270 | 271 | override fun apply( 272 | project: Project, 273 | configuration: Configuration, 274 | resolutionStrategy: ResolutionStrategy, 275 | extension: NebulaResolutionRulesExtension 276 | ) { 277 | val moduleId = moduleVersionId.module 278 | val match = configuration.allDependencies.find { 279 | it is ExternalModuleDependency && it.group == moduleId.group && it.name == moduleId.name 280 | } 281 | if (match != null && (moduleVersionId.version.isEmpty() || match.version == moduleVersionId.version)) { 282 | resolutionStrategy.componentSelection.withModule(moduleId) { selection -> 283 | val message = "denied by rule $ruleSet because '$reason'" 284 | selection.reject(message) 285 | } 286 | throw DependencyDeniedException(moduleVersionId, this) 287 | } 288 | } 289 | } 290 | 291 | data class ExcludeRule( 292 | override val module: String, 293 | override var ruleSet: String?, 294 | override val reason: String, 295 | override val author: String, 296 | override val date: String 297 | ) : ModuleRule { 298 | private val moduleId = module.toModuleId() 299 | 300 | @Override 301 | override fun apply( 302 | project: Project, 303 | configuration: Configuration, 304 | resolutionStrategy: ResolutionStrategy, 305 | extension: NebulaResolutionRulesExtension 306 | ) { 307 | val message = 308 | "excluded $moduleId and transitive dependencies for all dependencies of this configuration by rule $ruleSet" 309 | ResolutionRulesPlugin.Logger.debug(message) 310 | // TODO: would like a core Gradle feature that accepts a reason 311 | configuration.exclude(moduleId.group, moduleId.name) 312 | resolutionStrategy.componentSelection.withModule(moduleId.toString()) { selection -> 313 | selection.reject(message) 314 | } 315 | } 316 | } 317 | 318 | class DependencyDeniedException(moduleVersionId: ModuleVersionIdentifier, rule: DenyRule) : 319 | Exception("Dependency $moduleVersionId denied by rule ${rule.ruleSet}") 320 | 321 | class SubstituteRuleMissingVersionException(moduleId: String, rule: SubstituteRule) : 322 | Exception("The dependency to be substituted ($moduleId) must have a version. Rule ${rule.ruleSet} is invalid") 323 | 324 | fun Configuration.exclude(group: String, module: String) { 325 | exclude(mapOf("group" to group, "module" to module)) 326 | } 327 | 328 | fun String.toModuleId(): ModuleIdentifier { 329 | val parts = split(":") 330 | check(parts.size == 2) { "$this is an invalid module identifier" } 331 | return DefaultModuleIdentifier.newId(parts[0], parts[1]) 332 | } 333 | 334 | fun String.toModuleVersionId(): ModuleVersionIdentifier { 335 | val parts = split(":") 336 | val id = DefaultModuleIdentifier.newId(parts[0], parts[1]) 337 | check((2..3).contains(parts.size)) { "$this is an invalid module identifier" } 338 | return DefaultModuleVersionIdentifier.newId(id, if (parts.size == 3) parts[2] else "") 339 | } 340 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/resolutionrules/AlignRuleMatcherTest.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright 2014-2019 Netflix, Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package nebula.plugin.resolutionrules 20 | 21 | import kotlin.text.Regex 22 | import org.junit.Test 23 | import spock.lang.Specification 24 | 25 | class AlignRuleMatcherTest extends Specification { 26 | private static final String inputGroup = "test-group" 27 | public static final String ruleName = "test-rule" 28 | 29 | @Test 30 | void groupMatcherMatches() { 31 | given: 32 | def alignRule = createAlignRule() 33 | 34 | when: 35 | def matches = alignRule.ruleMatches(inputGroup, "test-name") 36 | 37 | then: 38 | assert matches 39 | } 40 | 41 | @Test 42 | void includesMatcherMatches() { 43 | given: 44 | def includes = new ArrayList() 45 | includes.add(new Regex("a")) 46 | includes.add(new Regex("b")) 47 | 48 | def excludes = new ArrayList() 49 | 50 | def alignRule = createAlignRule(includes, excludes) 51 | 52 | when: 53 | def matches = alignRule.ruleMatches(inputGroup, "a") 54 | 55 | then: 56 | assert matches 57 | } 58 | 59 | @Test 60 | void excludesMatcherMatches() { 61 | given: 62 | def includes = new ArrayList() 63 | 64 | def excludes = new ArrayList() 65 | excludes.add(new Regex("y")) 66 | excludes.add(new Regex("z")) 67 | 68 | def alignRule = createAlignRule(includes, excludes) 69 | 70 | when: 71 | def matches = alignRule.ruleMatches(inputGroup, "z") 72 | 73 | then: 74 | assert !matches 75 | } 76 | 77 | @Test 78 | void groupDoesNotMatch() { 79 | given: 80 | def alignRule = createAlignRule() 81 | 82 | when: 83 | def matches = alignRule.ruleMatches("other-group", "test-name") 84 | 85 | then: 86 | assert !matches 87 | } 88 | 89 | @Test 90 | void includesDoNotMatch() { 91 | given: 92 | def includes = new ArrayList() 93 | includes.add(new Regex("a")) 94 | includes.add(new Regex("b")) 95 | 96 | def excludes = new ArrayList() 97 | 98 | def alignRule = createAlignRule(includes, excludes) 99 | 100 | when: 101 | def matches = alignRule.ruleMatches(inputGroup, "something-else") 102 | 103 | then: 104 | assert !matches 105 | } 106 | 107 | @Test 108 | void excludesDoNotMatch() { 109 | given: 110 | def includes = new ArrayList() 111 | 112 | def excludes = new ArrayList() 113 | excludes.add(new Regex("y")) 114 | excludes.add(new Regex("z")) 115 | 116 | def alignRule = createAlignRule(includes, excludes) 117 | 118 | when: 119 | def matches = alignRule.ruleMatches(inputGroup, "something-else") 120 | 121 | then: 122 | assert matches 123 | } 124 | 125 | private static AlignRule createAlignRule(ArrayList includes = new ArrayList(), ArrayList excludes = new ArrayList()) { 126 | new AlignRule( 127 | ruleName, 128 | new Regex(inputGroup), 129 | includes, 130 | excludes, 131 | "match...", 132 | "test-rule-set", 133 | "reason", 134 | "author", 135 | "2015-10-07T20:21:20.368Z", 136 | "" 137 | ) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/resolutionrules/NebulaResolutionRulesExtensionTest.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.resolutionrules 2 | 3 | import org.gradle.api.Project 4 | import spock.lang.Specification 5 | 6 | class NebulaResolutionRulesExtensionTest extends Specification { 7 | 8 | Project project = Mock(Project) 9 | 10 | def 'can assign values'() { 11 | given: 12 | def extension = new NebulaResolutionRulesExtension(project) 13 | 14 | when: 15 | extension.include = ['something'] 16 | extension.optional = ['some-rule'] 17 | extension.exclude = ['foo'] 18 | 19 | then: 20 | extension.include.contains('something') 21 | extension.optional.contains('some-rule') 22 | extension.exclude.contains('foo') 23 | } 24 | 25 | def 'can assign and append to exclude value'() { 26 | given: 27 | def extension = new NebulaResolutionRulesExtension(project) 28 | 29 | when: 30 | extension.include = ['something'] 31 | extension.include.add('else') 32 | 33 | extension.exclude = ['foo'] 34 | extension.exclude.add('bar') 35 | 36 | extension.optional = ['some-rule'] 37 | extension.optional.add('another-rule') 38 | 39 | 40 | then: 41 | extension.include.contains('something') 42 | extension.include.contains('else') 43 | extension.exclude.contains('foo') 44 | extension.exclude.contains('bar') 45 | extension.optional.contains('some-rule') 46 | extension.optional.contains('another-rule') 47 | } 48 | 49 | def 'can assign and setter does not override existing values'() { 50 | given: 51 | def extension = new NebulaResolutionRulesExtension(project) 52 | 53 | when: 54 | 55 | extension.include = ['something'] 56 | extension.include = ['else'] 57 | 58 | extension.exclude = ['foo'] 59 | extension.exclude = ['bar'] 60 | 61 | extension.optional = ['some-rule'] 62 | extension.optional = ['another-rule'] 63 | 64 | then: 65 | extension.include.contains('something') 66 | extension.include.contains('else') 67 | extension.exclude.contains('foo') 68 | extension.exclude.contains('bar') 69 | extension.optional.contains('some-rule') 70 | extension.optional.contains('another-rule') 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/resolutionrules/RulesTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package nebula.plugin.resolutionrules 19 | 20 | import spock.lang.Specification 21 | 22 | /** 23 | * Tests for {@link RuleSet}. 24 | */ 25 | class RulesTest extends Specification { 26 | def 'json deserialised'() { 27 | when: 28 | String json = """{ 29 | "replace" : [ 30 | { 31 | "module" : "asm:asm", 32 | "with" : "org.ow2.asm:asm", 33 | "reason" : "The asm group id changed for 4.0 and later", 34 | "author" : "Danny Thomas ", 35 | "date" : "2015-10-07T20:21:20.368Z" 36 | } 37 | ], 38 | "substitute": [], 39 | "reject": [], 40 | "deny": [], 41 | "align": [], 42 | "exclude": [] 43 | }""" 44 | 45 | 46 | RuleSet rules = parseJsonText(json) 47 | 48 | then: 49 | !rules.replace.isEmpty() 50 | rules.replace[0].class == ReplaceRule 51 | } 52 | 53 | def 'json deserialised with one category of rules'() { 54 | when: 55 | String json = """{ 56 | "replace" : [ 57 | { 58 | "module" : "asm:asm", 59 | "with" : "org.ow2.asm:asm", 60 | "reason" : "The asm group id changed for 4.0 and later", 61 | "author" : "Danny Thomas ", 62 | "date" : "2015-10-07T20:21:20.368Z" 63 | } 64 | ] 65 | }""" 66 | 67 | 68 | RuleSet rules = parseJsonText(json) 69 | 70 | then: 71 | !rules.replace.isEmpty() 72 | rules.replace[0].class == ReplaceRule 73 | } 74 | 75 | static RuleSet parseJsonText(String json) { 76 | def ruleSet = JsonKt.objectMapper().readValue(json, RuleSet) 77 | return RulesKt.withName(ruleSet, "dummy") 78 | } 79 | } 80 | --------------------------------------------------------------------------------