├── .gitattributes ├── .github └── workflows │ ├── ci-build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── checkstyle │ ├── checkstyle.xml │ └── header.txt └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── renovate.json ├── settings.gradle.kts └── src ├── main └── java │ └── org │ └── gradlex │ └── javamodule │ └── moduleinfo │ ├── AutomaticModuleName.java │ ├── ExtraJavaModuleInfoPlugin.java │ ├── ExtraJavaModuleInfoPluginExtension.java │ ├── ExtraJavaModuleInfoTransform.java │ ├── FilePathToModuleCoordinates.java │ ├── IdValidator.java │ ├── KnownModule.java │ ├── ModuleInfo.java │ ├── ModuleNameUtil.java │ ├── ModuleSpec.java │ ├── PublishedMetadata.java │ └── tasks │ └── ModuleDescriptorRecommendation.java └── test └── groovy └── org └── gradlex └── javamodule └── moduleinfo ├── FilePathToModuleCoordinatesTest.groovy └── test ├── AbstractFunctionalTest.groovy ├── AddressCatalogEntriesFunctionalTest.groovy ├── AddressCoordinatesFunctionalTest.groovy ├── AddressJarFilesFunctionalTest.groovy ├── AllowAutomaticModulesFunctionalTest.groovy ├── ClassifiedJarsFunctionalTest.groovy ├── CombinationWithOtherPluginsFunctionalTest.groovy ├── ConfigurationDetailsFunctionalTest.groovy ├── EdgeCasesFunctionalTest.groovy ├── ExportsFunctionalTest.groovy ├── IdValidationFunctionalTest.groovy ├── IgnoreServiceProviderFunctionalTest.groovy ├── LocalJarTransformFunctionalTest.groovy ├── OpensFunctionalTest.groovy ├── PluginActivationFunctionalTest.groovy ├── RealModuleJarPatchingFunctionalTest.groovy ├── RealModuleJarPreservePatchingFunctionalTest.groovy ├── RecommendModuleSpecFunctionalTest.groovy ├── RemovePackageFunctionalTest.groovy ├── RequireAllDefinedDependenciesFunctionalTest.groovy └── fixture ├── GradleBuild.groovy └── LegacyLibraries.groovy /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior to LF, because checkstyle enforces it 2 | * text eol=lf 3 | 4 | *.bat eol=crlf 5 | 6 | *.jar binary 7 | *.jpg binary 8 | *.png binary 9 | *.ttf binary 10 | -------------------------------------------------------------------------------- /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | name: Build Plugin 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | gradle-build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: git clone 14 | uses: actions/checkout@v4 15 | - name: Set up JDK 16 | uses: actions/setup-java@v4 17 | with: 18 | distribution: temurin 19 | java-version: | 20 | 11 21 | 17 22 | - name: Set up Gradle 23 | uses: gradle/actions/setup-gradle@v4 24 | - run: "./gradlew build" -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | release-build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: git clone 11 | uses: actions/checkout@v4 12 | - name: Set up JDK 13 | uses: actions/setup-java@v4 14 | with: 15 | distribution: temurin 16 | java-version: 17 17 | - name: Set up Gradle 18 | uses: gradle/actions/setup-gradle@v4 19 | - run: "./gradlew :publishPlugin --no-configuration-cache" 20 | env: 21 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 22 | SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} 23 | GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} 24 | GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | /.idea/ 4 | *.iml 5 | out/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Extra Java Module Info Gradle Plugin - Changelog 2 | 3 | ## Version 1.12 4 | * [New] [#174](https://github.com/gradlex-org/extra-java-module-info/pull/174) - Add 'requiresStaticTransitive(...)' to module patch DSL 5 | * [New] [#172](https://github.com/gradlex-org/extra-java-module-info/pull/172) - Allow ignoring specific service provider implementations (Thanks [Ihor Herasymenko](https://github.com/iherasymenko) for contributing!) 6 | * [Fixed] [#171](https://github.com/gradlex-org/extra-java-module-info/pull/171) - In some cases, the plugin creates an empty providers set (Thanks [Larry North](https://github.com/LarryNorth) for reporting!) 7 | 8 | ## Version 1.11 9 | * [New] [#134](https://github.com/gradlex-org/extra-java-module-info/pull/134) - Add 'disable(...)' and 'enable(...)' options to control when the plugin is active 10 | * [New] [#161](https://github.com/gradlex-org/extra-java-module-info/pull/161) - Add 'skipLocalJars' option 11 | * [New] [#106](https://github.com/gradlex-org/extra-java-module-info/pull/106) - Actionable error message when plugin is used at configuration time 12 | 13 | ## Version 1.10.1 14 | * [Fixed] [#164](https://github.com/gradlex-org/extra-java-module-info/pull/164) - 'preserveExisting' does not duplicate 'provides' entries 15 | 16 | ## Version 1.10 17 | * [New] [#160](https://github.com/gradlex-org/extra-java-module-info/pull/160) - Add 'preserveExisting' option to patch real modules 18 | * [New] [#140](https://github.com/gradlex-org/extra-java-module-info/pull/140) - Add 'removePackage' option to deal with duplicated packages 19 | 20 | ## Version 1.9 21 | * [New] [#137](https://github.com/gradlex-org/extra-java-module-info/pull/137) - Configuration option for 'versionsProvidingConfiguration' 22 | * [New] [#130](https://github.com/gradlex-org/extra-java-module-info/pull/130) - Support classifier in coordinates notation 23 | * [New] [#138](https://github.com/gradlex-org/extra-java-module-info/pull/138) - 'javaModulesMergeJars' extends 'internal' if available 24 | * [Fixed] [#129](https://github.com/gradlex-org/extra-java-module-info/pull/129) - Find Jar for coordinates when version in Jar name differs 25 | * [Fixed] [#100](https://github.com/gradlex-org/extra-java-module-info/pull/100) - Fix error message about automatic module name mismatch 26 | 27 | ## Version 1.8 28 | * [New] [#99](https://github.com/gradlex-org/extra-java-module-info/issues/99) - Default behavior for 'module(id, name)' notation without configuration block 29 | * [New] - Use custom mappings from 'java-module-dependencies' for 'known modules' (if available) 30 | * [Fixed] [#96](https://github.com/gradlex-org/extra-java-module-info/pull/96) - Scope computation of 'requireAllDefinedDependencies' 31 | 32 | ## Version 1.7 33 | * [New] [#95](https://github.com/gradlex-org/extra-java-module-info/issues/95) - Granular exports and opens declarations (Thanks [Ihor Herasymenko](https://github.com/iherasymenko) for contributing!) 34 | * [Fixed] [#94](https://github.com/gradlex-org/extra-java-module-info/issues/94) - 'requireAllDefinedDependencies' skips platform dependencies 35 | 36 | ## Version 1.6.2 37 | * [New] - Use shared mappings from 'java-module-dependencies' for 'known modules' (if available) 38 | 39 | ## Version 1.6.1 40 | * [Fixed] [#89](https://github.com/gradlex-org/extra-java-module-info/issues/89) - Make Jar patching reproducible 41 | 42 | ## Version 1.6 43 | * [New] [#74](https://github.com/gradlex-org/extra-java-module-info/issues/74) - Add 'deriveAutomaticModuleNamesFromFileNames' option (Thanks [Mike Wacker](https://github.com/mikewacker) for suggesting!) 44 | * [New] [#85](https://github.com/gradlex-org/extra-java-module-info/issues/85) - Add check that existing 'Automatic-Module-Names' are not changed accidentally 45 | * [Fixed] [#77](https://github.com/gradlex-org/extra-java-module-info/issues/77) - 'exportAllPackages' breaks module for multi-release Jars (Thanks [Christopher Schnick](https://github.com/crschnick) for reporting!) 46 | * [Fixed] [#78](https://github.com/gradlex-org/extra-java-module-info/issues/78) - Empty Jars created when Intellij refreshes project (Thanks [Kostas Pagratis](https://github.com/kpagratis) for reporting!) 47 | * [Fixed] [#81](https://github.com/gradlex-org/extra-java-module-info/issues/81) - 'requireAllDefinedDependencies' does not work reliably for 'annotationProcessor' 48 | 49 | ## Version 1.5 50 | * [New] [#75](https://github.com/gradlex-org/extra-java-module-info/issues/75) - Add 'failOnAutomaticModules' option (Thanks [Ihor Herasymenko](https://github.com/iherasymenko) for contributing!) 51 | * [New] [#75](https://github.com/gradlex-org/extra-java-module-info/issues/75) - Support patching of real modules (Thanks [Ihor Herasymenko](https://github.com/iherasymenko) for contributing!) 52 | * [New] [#75](https://github.com/gradlex-org/extra-java-module-info/issues/75) - Add 'moduleDescriptorRecommendations' help task (Thanks [Ihor Herasymenko](https://github.com/iherasymenko) for contributing!) 53 | 54 | ## Version 1.4.2 55 | * [Fixed] [#45](https://github.com/gradlex-org/extra-java-module-info/issues/45) - Preserve sub-folders of 'META-INF/services' in merged Jars 56 | 57 | ## Version 1.4.1 58 | * [Fixed] [#50](https://github.com/gradlex-org/extra-java-module-info/issues/50) - Remove merged Jars from classpath even if they are (automatic) modules 59 | 60 | ## Version 1.4 61 | * [New] Minimal Gradle version is now 6.8 for integration with recently added features like the Dependency Version Catalog 62 | * [New] [#46](https://github.com/gradlex-org/extra-java-module-info/issues/46) - Validation coordinates and module names 63 | * [New] [#41](https://github.com/gradlex-org/extra-java-module-info/issues/41) - Support version catalog accessors to express dependency coordinates (Thanks [Giuseppe Barbieri](https://github.com/elect86) for suggesting!) 64 | * [New] [#30](https://github.com/gradlex-org/extra-java-module-info/issues/30) - Add 'opens(...)' to module DSL (Thanks [Wexalian](https://github.com/Wexalian) for suggesting!) 65 | * [Fixed] [#47](https://github.com/gradlex-org/extra-java-module-info/issues/47) - requireAllDefinedDependencies() gives error when dependency only appears on runtime path (Thanks [Sola](https://github.com/unlimitedsola) for reporting!) 66 | * [Fixed] [#45](https://github.com/gradlex-org/extra-java-module-info/issues/45) - Sub-folders in 'META-INF/services' are not ignored (Thanks [Jonas Beyer](https://github.com/j-beyer) for reporting!) 67 | * [Fixed] [#44](https://github.com/gradlex-org/extra-java-module-info/issues/44) - Name resolution for jars with '-' character fails if Jars are in local .m2 repository (Thanks [Aidan Do](https://github.com/REslim30) for reporting!) 68 | 69 | ## Version 1.3 70 | * [New] [#42](https://github.com/gradlex-org/extra-java-module-info/issues/42) - Added support for 'uses' directives (Thanks [Stefan Reek](https://github.com/StefanReek) for contributing!) 71 | 72 | ## Version 1.2 73 | * [New] [#40](https://github.com/gradlex-org/extra-java-module-info/issues/40) - Add requireAllDefinedDependencies() functionality 74 | * [New] [#38](https://github.com/gradlex-org/extra-java-module-info/issues/38) - Add exportAllPackages() functionality (Thanks [Hendrik Ebbers](https://github.com/hendrikebbers) for suggesting!) 75 | * [New] [#37](https://github.com/gradlex-org/extra-java-module-info/issues/37) - Merge Jars - fully support merging Zip into Jar 76 | 77 | ## Version 1.1 78 | * [Fixed] [#36](https://github.com/gradlex-org/extra-java-module-info/issues/36) - mergeJar can lead to unnecessary build failures (Thanks [nieqian1230](https://github.com/nieqian1230) for reporting!) 79 | 80 | ## Version 1.0 81 | * Moved project to [GradleX](https://gradlex.org) - new plugin ID: `org.gradlex.extra-java-module-info` 82 | 83 | ## Version 0.15 84 | * [New] [#34](https://github.com/gradlex-org/extra-java-module-info/issues/34) - Merge Jars - merge service provider files 85 | 86 | ## Version 0.14 87 | * [Fixed] [#33](https://github.com/gradlex-org/extra-java-module-info/issues/33) - Map 'Jar File Path' to 'group:name' correctly for Jars cached in local .m2 repository (Thanks [Leon Linhart](https://github.com/TheMrMilchmann) for reporting!) 88 | 89 | ## Version 0.13 90 | * [New] [#32](https://github.com/gradlex-org/extra-java-module-info/issues/32) - Add license information to POM (Thanks [Edward McKnight](https://github.com/EM-Creations) for reporting!) 91 | 92 | ## Version 0.12 93 | * [New] [#31](https://github.com/gradlex-org/extra-java-module-info/issues/31) - Address Jars by 'group:name' coordinates (instead of file name with version) 94 | * [New] [#1](https://github.com/gradlex-org/extra-java-module-info/issues/1) - Merging several legacy Jars into one Module Jar 95 | 96 | ## Version 0.11 97 | * [Fixed] [#27](https://github.com/gradlex-org/extra-java-module-info/issues/27) - Avoid 'invalid entry compressed size' on Java < 16 (Thanks [@carlosame](https://github.com/carlosame) for reporting and [Ihor Herasymenko](https://github.com/iherasymenko) 98 | for fixing!) 99 | 100 | ## Version 0.10 101 | * [Fixed] [#23](https://github.com/gradlex-org/extra-java-module-info/issues/23) - Ignore `MANIFEST.MF` files not correctly positioned in Jar (Thanks [Michael Spahn](https://github.com/michael-spahn) reporting!) 102 | 103 | ## Version 0.9 104 | * [Fixed] [#17](https://github.com/gradlex-org/extra-java-module-info/issues/17) - Exclude signatures from signed Jars (Thanks [Philipp Schneider](https://github.com/p-schneider) for fixing!) 105 | * [New] [#16](https://github.com/gradlex-org/extra-java-module-info/issues/16) - Prevent duplicates in ModuleInfo DSL (Thanks [Ihor Herasymenko](https://github.com/iherasymenko) for contributing!) 106 | 107 | ## Version 0.8 108 | * [Fixed] [#15](https://github.com/gradlex-org/extra-java-module-info/issues/15) - Error when importing certain multi-project builds in the IDE (Thanks [Michael Spahn](https://github.com/michael-spahn) reporting!) 109 | 110 | ## Version 0.7 111 | * [New] [#14](https://github.com/gradlex-org/extra-java-module-info/issues/14) - DSL method for omitting unwanted service provider (Thanks [Ihor Herasymenko](https://github.com/iherasymenko) for contributing!) 112 | 113 | ## Version 0.6 114 | * [New] [#11](https://github.com/gradlex-org/extra-java-module-info/issues/11) - Transform results are cached (Thanks [Carsten Otto](https://github.com/C-Otto) reporting!) 115 | 116 | ## Version 0.5 117 | * [New] [#9](https://github.com/gradlex-org/extra-java-module-info/issues/9) - Automatically add descriptors to 'module-info.class' (Thanks [Ihor Herasymenko](https://github.com/iherasymenko) for contributing!) 118 | * [New] [#9](https://github.com/gradlex-org/extra-java-module-info/issues/9) - Support 'requires static' in DSL (Thanks [Ihor Herasymenko](https://github.com/iherasymenko) for contributing!) 119 | 120 | ## Version 0.4 121 | * [Fixed] Issue with 'failOnMissingModuleInfo.set(false)' option 122 | 123 | ## Version 0.3 124 | * [New] [#3](https://github.com/gradlex-org/extra-java-module-info/issues/3) - Add 'failOnMissingModuleInfo.set(false)' option 125 | 126 | ## Version 0.2 127 | * [Fixed] [#2](https://github.com/gradlex-org/extra-java-module-info/issues/2) - Handle Jars without manifest (Thanks [Ihor Herasymenko](https://github.com/iherasymenko) for fixing!) 128 | 129 | ## Version 0.1 130 | * [New] Initial release following discussions in https://github.com/gradle/gradle/issues/12630 131 | 132 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | team@gradlex.org. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 Louis Jacomet 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("groovy") 3 | id("org.gradlex.internal.plugin-publish-conventions") version "0.6" 4 | } 5 | 6 | group = "org.gradlex" 7 | version = "1.12" 8 | 9 | java { 10 | sourceCompatibility = JavaVersion.VERSION_1_8 11 | targetCompatibility = JavaVersion.VERSION_1_8 12 | } 13 | 14 | dependencies { 15 | implementation("org.ow2.asm:asm:9.8") 16 | 17 | testImplementation("org.spockframework:spock-core:2.3-groovy-3.0") 18 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 19 | } 20 | 21 | pluginPublishConventions { 22 | id("${project.group}.${project.name}") 23 | implementationClass("org.gradlex.javamodule.moduleinfo.ExtraJavaModuleInfoPlugin") 24 | displayName("Extra Java Module Info Gradle Plugin") 25 | description("Add module information to legacy Java libraries.") 26 | tags("gradlex", "java", "modularity", "jigsaw", "jpms") 27 | gitHub("https://github.com/gradlex-org/extra-java-module-info") 28 | developer { 29 | id.set("jjohannes") 30 | name.set("Jendrik Johannes") 31 | email.set("jendrik@gradlex.org") 32 | } 33 | } 34 | 35 | tasks.test { 36 | description = "Runs tests against the Gradle version the plugin is built with" 37 | classpath = sourceSets.test.get().runtimeClasspath 38 | useJUnitPlatform() 39 | maxParallelForks = 4 40 | } 41 | 42 | listOf("6.8.3", "6.9.2", "7.0.2", "7.6.1").forEach { gradleVersionUnderTest -> 43 | val testGradle = tasks.register("testGradle$gradleVersionUnderTest") { 44 | group = "verification" 45 | description = "Runs tests against Gradle $gradleVersionUnderTest" 46 | testClassesDirs = sourceSets.test.get().output.classesDirs 47 | classpath = sourceSets.test.get().runtimeClasspath 48 | useJUnitPlatform() 49 | maxParallelForks = 4 50 | systemProperty("gradleVersionUnderTest", gradleVersionUnderTest) 51 | javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(11) } 52 | } 53 | tasks.check { 54 | dependsOn(testGradle) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.configuration-cache=true -------------------------------------------------------------------------------- /gradle/checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /gradle/checkstyle/header.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright the GradleX team. 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 | */ -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gradlex-org/extra-java-module-info/a55822151f37b9db5eb12959aa68cb9c03e579d5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=845952a9d6afa783db70bb3b0effaae45ae5542ca2bb7929619e8af49cb634cf 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-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 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.gradle.develocity") version "4.0.1" 3 | } 4 | 5 | rootProject.name = "extra-java-module-info" 6 | 7 | dependencyResolutionManagement { 8 | repositories.mavenCentral() 9 | } 10 | 11 | develocity { 12 | buildScan { 13 | val isCi = providers.environmentVariable("CI").getOrElse("false").toBoolean() 14 | if (isCi) { 15 | termsOfUseUrl = "https://gradle.com/help/legal-terms-of-use" 16 | termsOfUseAgree = "yes" 17 | } else { 18 | publishing.onlyIf { false } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/gradlex/javamodule/moduleinfo/AutomaticModuleName.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright the GradleX team. 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 org.gradlex.javamodule.moduleinfo; 18 | 19 | public class AutomaticModuleName extends ModuleSpec { 20 | 21 | AutomaticModuleName(String identifier, String moduleName) { 22 | super(identifier, moduleName); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright the GradleX team. 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 org.gradlex.javamodule.moduleinfo; 18 | 19 | import org.gradle.api.NonNullApi; 20 | import org.gradle.api.Plugin; 21 | import org.gradle.api.Project; 22 | import org.gradle.api.Transformer; 23 | import org.gradle.api.artifacts.Configuration; 24 | import org.gradle.api.artifacts.component.ComponentIdentifier; 25 | import org.gradle.api.artifacts.component.ModuleComponentIdentifier; 26 | import org.gradle.api.artifacts.dsl.DependencyHandler; 27 | import org.gradle.api.artifacts.result.ResolvedArtifactResult; 28 | import org.gradle.api.artifacts.result.ResolvedComponentResult; 29 | import org.gradle.api.attributes.Attribute; 30 | import org.gradle.api.attributes.Category; 31 | import org.gradle.api.attributes.Usage; 32 | import org.gradle.api.file.Directory; 33 | import org.gradle.api.file.ProjectLayout; 34 | import org.gradle.api.file.RegularFile; 35 | import org.gradle.api.file.RegularFileProperty; 36 | import org.gradle.api.plugins.HelpTasksPlugin; 37 | import org.gradle.api.plugins.JavaPlugin; 38 | import org.gradle.api.provider.Provider; 39 | import org.gradle.api.tasks.SourceSetContainer; 40 | import org.gradle.util.GradleVersion; 41 | import org.gradlex.javamodule.moduleinfo.tasks.ModuleDescriptorRecommendation; 42 | 43 | import java.io.CharArrayReader; 44 | import java.io.File; 45 | import java.io.IOException; 46 | import java.lang.reflect.Method; 47 | import java.util.Collection; 48 | import java.util.Collections; 49 | import java.util.Comparator; 50 | import java.util.List; 51 | import java.util.Map; 52 | import java.util.Properties; 53 | import java.util.Set; 54 | import java.util.stream.Collectors; 55 | 56 | import static org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE; 57 | import static org.gradle.api.attributes.Category.LIBRARY; 58 | import static org.gradle.api.attributes.Usage.JAVA_RUNTIME; 59 | import static org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE; 60 | import static org.gradle.api.plugins.JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME; 61 | import static org.gradlex.javamodule.moduleinfo.ExtraJavaModuleInfoPluginExtension.JAVA_MODULE_ATTRIBUTE; 62 | 63 | /** 64 | * Entry point of the plugin. 65 | */ 66 | @NonNullApi 67 | public abstract class ExtraJavaModuleInfoPlugin implements Plugin { 68 | 69 | @Override 70 | public void apply(Project project) { 71 | if (GradleVersion.current().compareTo(GradleVersion.version("6.8")) < 0) { 72 | throw new RuntimeException("This plugin requires Gradle 6.8+"); 73 | } 74 | 75 | // register the plugin extension as 'extraJavaModuleInfo {}' configuration block 76 | ExtraJavaModuleInfoPluginExtension extension = project.getExtensions().create("extraJavaModuleInfo", ExtraJavaModuleInfoPluginExtension.class); 77 | extension.getFailOnMissingModuleInfo().convention(true); 78 | extension.getFailOnAutomaticModules().convention(false); 79 | extension.getSkipLocalJars().convention(false); 80 | extension.getDeriveAutomaticModuleNamesFromFileNames().convention(false); 81 | 82 | // setup the transform and the tasks for all projects in the build 83 | project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> { 84 | configureTransform(project, extension); 85 | configureModuleDescriptorTasks(project); 86 | }); 87 | } 88 | 89 | private void configureModuleDescriptorTasks(Project project) { 90 | project.getExtensions().getByType(SourceSetContainer.class).all(sourceSet -> { 91 | String name = sourceSet.getTaskName("", "moduleDescriptorRecommendations"); 92 | project.getTasks().register(name, ModuleDescriptorRecommendation.class, task -> { 93 | Transformer, Configuration> artifactsTransformer = configuration -> { 94 | //noinspection CodeBlock2Expr 95 | return configuration.getIncoming() 96 | .getArtifacts() 97 | .getArtifacts() 98 | .stream() 99 | .sorted(Comparator.comparing(artifact -> artifact.getId().getComponentIdentifier().toString())) 100 | .map(ResolvedArtifactResult::getFile) 101 | .collect(Collectors.toList()); 102 | }; 103 | 104 | Transformer, Configuration> componentsTransformer = configuration -> { 105 | Set artifacts = configuration.getIncoming() 106 | .getArtifacts() 107 | .getArtifacts() 108 | .stream() 109 | .map(artifact -> artifact.getId().getComponentIdentifier()) 110 | .collect(Collectors.toSet()); 111 | return configuration.getIncoming() 112 | .getResolutionResult() 113 | .getAllComponents() 114 | .stream() 115 | .filter(component -> artifacts.contains(component.getId())) 116 | .sorted(Comparator.comparing(artifact -> artifact.getId().toString())) 117 | .collect(Collectors.toList()); 118 | }; 119 | 120 | Provider compileClasspath = project.getConfigurations().named(sourceSet.getCompileClasspathConfigurationName()); 121 | task.getCompileArtifacts().set(compileClasspath.map(artifactsTransformer)); 122 | task.getCompileResolvedComponentResults().set(compileClasspath.map(componentsTransformer)); 123 | 124 | Provider runtimeClasspath = project.getConfigurations().named(sourceSet.getRuntimeClasspathConfigurationName()); 125 | task.getRuntimeArtifacts().set(runtimeClasspath.map(artifactsTransformer)); 126 | task.getRuntimeResolvedComponentResults().set(runtimeClasspath.map(componentsTransformer)); 127 | 128 | task.getRelease().convention(21); 129 | 130 | task.setGroup(HelpTasksPlugin.HELP_GROUP); 131 | task.setDescription("Generates module descriptors for 'org.gradlex.extra-java-module-info' plugin based on the dependency and class file analysis of automatic modules and non-modular dependencies"); 132 | }); 133 | }); 134 | } 135 | 136 | private void configureTransform(Project project, ExtraJavaModuleInfoPluginExtension extension) { 137 | Configuration javaModulesMergeJars = project.getConfigurations().create("javaModulesMergeJars", c -> { 138 | c.setVisible(false); 139 | c.setCanBeConsumed(false); 140 | c.setCanBeResolved(true); 141 | c.getAttributes().attribute(USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, JAVA_RUNTIME)); 142 | c.getAttributes().attribute(CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, LIBRARY)); 143 | 144 | // Automatically add dependencies for Jars where we know the coordinates 145 | c.withDependencies(d -> extension.getModuleSpecs().get().values().stream().flatMap(m -> 146 | m.getMergedJars().stream()).filter(s -> s.contains(":")).forEach(s -> 147 | d.add(project.getDependencies().create(s)))); 148 | 149 | // Automatically get versions from the runtime classpath 150 | if (GradleVersion.current().compareTo(GradleVersion.version("6.8")) >= 0) { 151 | //noinspection UnstableApiUsage 152 | c.shouldResolveConsistentlyWith(project.getConfigurations().getByName(RUNTIME_CLASSPATH_CONFIGURATION_NAME)); 153 | } 154 | }); 155 | 156 | // If 'internal' is added by 'org.gradlex.jvm-dependency-conflict-resolution', extend from it to get access to versions 157 | project.getConfigurations().all(otherConfiguration -> { 158 | if ("internal".equals(otherConfiguration.getName())) { 159 | javaModulesMergeJars.extendsFrom(otherConfiguration); 160 | } 161 | }); 162 | 163 | Attribute artifactType = Attribute.of("artifactType", String.class); 164 | 165 | project.getExtensions().getByType(SourceSetContainer.class).all(sourceSet -> { 166 | // by default, activate plugin for all source sets 167 | extension.activate(sourceSet); 168 | 169 | // outgoing variants may express that they already provide a modular Jar and can hence skip the transform altogether 170 | Configuration runtimeElements = project.getConfigurations().findByName(sourceSet.getRuntimeElementsConfigurationName()); 171 | Configuration apiElements = project.getConfigurations().findByName(sourceSet.getApiElementsConfigurationName()); 172 | if (GradleVersion.current().compareTo(GradleVersion.version("7.4")) >= 0) { 173 | if (runtimeElements != null) { 174 | runtimeElements.getOutgoing().getAttributes().attributeProvider(JAVA_MODULE_ATTRIBUTE, extension.getSkipLocalJars()); 175 | } 176 | if (apiElements != null) { 177 | apiElements.getOutgoing().getAttributes().attributeProvider(JAVA_MODULE_ATTRIBUTE, extension.getSkipLocalJars()); 178 | } 179 | } else { 180 | project.afterEvaluate(p -> { 181 | if (runtimeElements != null) { 182 | runtimeElements.getOutgoing().getAttributes().attribute(JAVA_MODULE_ATTRIBUTE, extension.getSkipLocalJars().get()); 183 | } 184 | if (apiElements != null) { 185 | apiElements.getOutgoing().getAttributes().attribute(JAVA_MODULE_ATTRIBUTE, extension.getSkipLocalJars().get()); 186 | } 187 | }); 188 | } 189 | }); 190 | 191 | // Jars may be transformed (or merged into) Module Jars 192 | registerTransform("jar", project, extension, javaModulesMergeJars, artifactType, JAVA_MODULE_ATTRIBUTE); 193 | // Classpath entries may also be zip files that may be merged into Module Jars (from the docs: "Class paths to the .jar, .zip or .class files)" 194 | registerTransform("zip", project, extension, javaModulesMergeJars, artifactType, JAVA_MODULE_ATTRIBUTE); 195 | } 196 | 197 | private void registerTransform(String fileExtension, Project project, ExtraJavaModuleInfoPluginExtension extension, Configuration javaModulesMergeJars, Attribute artifactType, Attribute javaModule) { 198 | DependencyHandler dependencies = project.getDependencies(); 199 | 200 | // all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification 201 | dependencies.getArtifactTypes().maybeCreate(fileExtension).getAttributes().attribute(javaModule, false); 202 | 203 | // register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter 204 | dependencies.registerTransform(ExtraJavaModuleInfoTransform.class, t -> { 205 | t.parameters(p -> { 206 | p.getModuleSpecs().set(extension.getModuleSpecs()); 207 | p.getFailOnMissingModuleInfo().set(extension.getFailOnMissingModuleInfo()); 208 | p.getFailOnAutomaticModules().set(extension.getFailOnAutomaticModules()); 209 | p.getDeriveAutomaticModuleNamesFromFileNames().set(extension.getDeriveAutomaticModuleNamesFromFileNames()); 210 | 211 | // See: https://github.com/adammurdoch/dependency-graph-as-task-inputs/blob/main/plugins/src/main/java/TestPlugin.java 212 | Provider> artifacts = project.provider(() -> 213 | javaModulesMergeJars.getIncoming().artifactView(v -> v.lenient(true)).getArtifacts().getArtifacts()); 214 | p.getMergeJarIds().set(artifacts.map(new IdExtractor())); 215 | p.getMergeJars().set(artifacts.map(new FileExtractor(project.getLayout()))); 216 | 217 | Provider> componentsOfInterest = componentsOfInterest(extension); 218 | p.getRequiresFromMetadata().set(componentsOfInterest.map(gaSet -> gaSet.stream() 219 | .collect(Collectors.toMap(ga -> ga, ga -> new PublishedMetadata(ga, project, extension))))); 220 | p.getAdditionalKnownModules().set(extractFromModuleDependenciesPlugin(project)); 221 | }); 222 | t.getFrom().attribute(artifactType, fileExtension).attribute(javaModule, false); 223 | t.getTo().attribute(artifactType, fileExtension).attribute(javaModule, true); 224 | }); 225 | } 226 | 227 | private Provider> extractFromModuleDependenciesPlugin(Project project) { 228 | return project.provider(() -> { 229 | Object javaModuleDependencies = project.getExtensions().findByName("javaModuleDependencies"); 230 | if (javaModuleDependencies == null) { 231 | return Collections.emptyMap(); 232 | } 233 | try { 234 | Method getModulesProperties = javaModuleDependencies.getClass().getMethod("getModulesProperties"); 235 | RegularFileProperty file = (RegularFileProperty) getModulesProperties.invoke(javaModuleDependencies); 236 | return project.getProviders().fileContents(file).getAsText().map(c -> { 237 | Properties p = new Properties(); 238 | try { 239 | p.load(new CharArrayReader(c.toCharArray())); 240 | } catch (IOException e) { 241 | throw new RuntimeException(e); 242 | } 243 | @SuppressWarnings({"rawtypes", "unchecked"}) 244 | Map result = (Map) p; 245 | return result; 246 | }).getOrElse(Collections.emptyMap()); 247 | } catch (ReflectiveOperationException e) { 248 | throw new RuntimeException(e); 249 | } 250 | }); 251 | } 252 | 253 | private static Provider> componentsOfInterest(ExtraJavaModuleInfoPluginExtension extension) { 254 | return extension.getModuleSpecs().map(specs -> specs.values().stream() 255 | .filter(ExtraJavaModuleInfoPlugin::needsDependencies) 256 | .map(ModuleSpec::getIdentifier) 257 | .collect(Collectors.toSet())); 258 | } 259 | 260 | private static boolean needsDependencies(ModuleSpec moduleSpec) { 261 | return moduleSpec instanceof ModuleInfo 262 | && ((ModuleInfo) moduleSpec).requireAllDefinedDependencies 263 | && IdValidator.isCoordinates(moduleSpec.getIdentifier()); 264 | } 265 | 266 | static String ga(ComponentIdentifier id) { 267 | if (id instanceof ModuleComponentIdentifier) { 268 | return ((ModuleComponentIdentifier) id).getGroup() + ":" + ((ModuleComponentIdentifier) id).getModule(); 269 | } 270 | return id.getDisplayName(); 271 | } 272 | 273 | private static class IdExtractor implements Transformer, Collection> { 274 | @Override 275 | public List transform(Collection artifacts) { 276 | return artifacts.stream().map(a -> { 277 | ComponentIdentifier componentIdentifier = a.getId().getComponentIdentifier(); 278 | if (componentIdentifier instanceof ModuleComponentIdentifier) { 279 | return ((ModuleComponentIdentifier) componentIdentifier).getModuleIdentifier().toString(); 280 | } else { 281 | return componentIdentifier.getDisplayName(); 282 | } 283 | }).collect(Collectors.toList()); 284 | } 285 | } 286 | 287 | private static class FileExtractor implements Transformer, Collection> { 288 | private final ProjectLayout projectLayout; 289 | 290 | public FileExtractor(ProjectLayout projectLayout) { 291 | this.projectLayout = projectLayout; 292 | } 293 | 294 | @Override 295 | public List transform(Collection artifacts) { 296 | Directory projectDirectory = projectLayout.getProjectDirectory(); 297 | return artifacts.stream().map(a -> projectDirectory.file(a.getFile().getAbsolutePath())).collect(Collectors.toList()); 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoPluginExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright the GradleX team. 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 org.gradlex.javamodule.moduleinfo; 18 | 19 | import org.gradle.api.Action; 20 | import org.gradle.api.NamedDomainObjectProvider; 21 | import org.gradle.api.artifacts.Configuration; 22 | import org.gradle.api.artifacts.ConfigurationContainer; 23 | import org.gradle.api.artifacts.MinimalExternalModuleDependency; 24 | import org.gradle.api.attributes.Attribute; 25 | import org.gradle.api.model.ObjectFactory; 26 | import org.gradle.api.provider.MapProperty; 27 | import org.gradle.api.provider.Property; 28 | import org.gradle.api.provider.Provider; 29 | import org.gradle.api.tasks.SourceSet; 30 | 31 | import javax.annotation.Nullable; 32 | import javax.inject.Inject; 33 | 34 | /** 35 | * A data class to collect all the module information we want to add. 36 | * Here the class is used as extension that can be configured in the build script 37 | * and as input to the ExtraModuleInfoTransform that add the information to Jars. 38 | */ 39 | @SuppressWarnings("unused") 40 | public abstract class ExtraJavaModuleInfoPluginExtension { 41 | static Attribute JAVA_MODULE_ATTRIBUTE = Attribute.of("javaModule", Boolean.class); 42 | 43 | @Inject 44 | protected abstract ObjectFactory getObjects(); 45 | 46 | @Inject 47 | protected abstract ConfigurationContainer getConfigurations(); 48 | 49 | public abstract MapProperty getModuleSpecs(); 50 | public abstract Property getFailOnMissingModuleInfo(); 51 | public abstract Property getFailOnAutomaticModules(); 52 | public abstract Property getSkipLocalJars(); 53 | public abstract Property getDeriveAutomaticModuleNamesFromFileNames(); 54 | public abstract Property getVersionsProvidingConfiguration(); 55 | 56 | /** 57 | * Add full module information for a given Jar file. 58 | * 59 | * @param identifier group:name coordinates _or_ Jar file name 60 | * @param moduleName the Module Name of the Module to construct 61 | */ 62 | public void module(String identifier, String moduleName) { 63 | module(identifier, moduleName, (String) null); 64 | } 65 | 66 | /** 67 | * Add full module information for a given Jar file. 68 | * 69 | * @param alias group:name coordinates alias from version catalog 70 | * @param moduleName the Module Name of the Module to construct 71 | */ 72 | public void module(Provider alias, String moduleName) { 73 | module(alias.get().getModule().toString(), moduleName); 74 | } 75 | 76 | /** 77 | * Add full module information for a given Jar file. 78 | * 79 | * @param identifier group:name coordinates _or_ Jar file name 80 | * @param moduleName the Module Name of the Module to construct 81 | * @param moduleVersion version to write into the module-info.class 82 | */ 83 | public void module(String identifier, String moduleName, String moduleVersion) { 84 | module(identifier, moduleName, moduleVersion, m -> { 85 | m.exportAllPackages(); 86 | if (identifier.contains(":")) { // only if the identifier is a coordinates (not a Jar) 87 | m.requireAllDefinedDependencies(); 88 | } 89 | }); 90 | } 91 | 92 | /** 93 | * Add full module information for a given Jar file. 94 | * 95 | * @param alias group:name coordinates alias from version catalog 96 | * @param moduleName the Module Name of the Module to construct 97 | * @param moduleVersion version to write into the module-info.class 98 | */ 99 | public void module(Provider alias, String moduleName, String moduleVersion) { 100 | module(alias.get().getModule().toString(), moduleName, moduleVersion); 101 | } 102 | 103 | /** 104 | * Add full module information for a given Jar file. 105 | * 106 | * @param identifier group:name coordinates _or_ Jar file name 107 | * @param moduleName the Module Name of the Module to construct 108 | * @param conf configure exported packages and dependencies, see {@link ModuleInfo} 109 | */ 110 | public void module(String identifier, String moduleName, @Nullable Action conf) { 111 | module(identifier, moduleName, null, conf); 112 | } 113 | 114 | /** 115 | * Add full module information for a given Jar file. 116 | * 117 | * @param alias group:name coordinates alias from version catalog 118 | * @param moduleName the Module Name of the Module to construct 119 | * @param conf configure exported packages and dependencies, see {@link ModuleInfo} 120 | */ 121 | public void module(Provider alias, String moduleName, @Nullable Action conf) { 122 | module(alias.get().getModule().toString(), moduleName, conf); 123 | } 124 | 125 | /** 126 | * Add full module information for a given Jar file. 127 | * 128 | * @param identifier group:name coordinates _or_ Jar file name 129 | * @param moduleName the Module Name of the Module to construct 130 | * @param moduleVersion version to write into the module-info.class 131 | * @param conf configure exported packages, dependencies and Jar merging, see {@link ModuleInfo} 132 | */ 133 | public void module(String identifier, String moduleName, @Nullable String moduleVersion, @Nullable Action conf) { 134 | ModuleInfo moduleInfo = new ModuleInfo(identifier, moduleName, moduleVersion, getObjects()); 135 | if (conf != null) { 136 | conf.execute(moduleInfo); 137 | } 138 | this.getModuleSpecs().put(identifier, moduleInfo); 139 | } 140 | 141 | /** 142 | * Add full module information for a given Jar file. 143 | * 144 | * @param alias group:name coordinates alias from version catalog 145 | * @param moduleName the Module Name of the Module to construct 146 | * @param moduleVersion version to write into the module-info.class 147 | * @param conf configure exported packages, dependencies and Jar merging, see {@link ModuleInfo} 148 | */ 149 | public void module(Provider alias, String moduleName, @Nullable String moduleVersion, @Nullable Action conf) { 150 | module(alias.get().getModule().toString(), moduleName, moduleVersion, conf); 151 | } 152 | 153 | /** 154 | * Add an Automatic-Module-Name to a given Jar file. 155 | * 156 | * @param identifier group:name coordinates _or_ Jar file name 157 | * @param moduleName the Module Name of the Module to construct 158 | */ 159 | public void automaticModule(String identifier, String moduleName) { 160 | automaticModule(identifier, moduleName, null); 161 | } 162 | 163 | /** 164 | * Add an Automatic-Module-Name to a given Jar file. 165 | * 166 | * @param alias group:name coordinates alias from version catalog 167 | * @param moduleName the Module Name of the Module to construct 168 | */ 169 | public void automaticModule(Provider alias, String moduleName) { 170 | automaticModule(alias.get().getModule().toString(), moduleName, null); 171 | } 172 | 173 | /** 174 | * Add an Automatic-Module-Name to a given Jar file. 175 | * 176 | * @param identifier group:name coordinates _or_ Jar file name 177 | * @param moduleName the Module Name of the Module to construct 178 | * @param conf configure Jar merging, see {@link AutomaticModuleName} 179 | */ 180 | public void automaticModule(String identifier, String moduleName, @Nullable Action conf) { 181 | AutomaticModuleName automaticModuleName = new AutomaticModuleName(identifier, moduleName); 182 | if (conf != null) { 183 | conf.execute(automaticModuleName); 184 | } 185 | getModuleSpecs().put(identifier, automaticModuleName); 186 | } 187 | 188 | 189 | /** 190 | * Add an Automatic-Module-Name to a given Jar file. 191 | * 192 | * @param alias group:name coordinates alias from version catalog 193 | * @param moduleName the Module Name of the Module to construct 194 | * @param conf configure Jar merging, see {@link AutomaticModuleName} 195 | */ 196 | public void automaticModule(Provider alias, String moduleName, @Nullable Action conf) { 197 | automaticModule(alias.get().getModule().toString(), moduleName, conf); 198 | } 199 | 200 | /** 201 | * Let the plugin know about an existing module on the module path. 202 | * This may be needed when 'requiresDirectivesFromMetadata(true)' is used. 203 | * 204 | * @param coordinates group:name coordinates 205 | * @param moduleName the Module Name of the Module referred to by the coordinates 206 | */ 207 | public void knownModule(String coordinates, String moduleName) { 208 | getModuleSpecs().put(coordinates, new KnownModule(coordinates, moduleName)); 209 | } 210 | 211 | /** 212 | * Let the plugin know about an existing module on the module path. 213 | * This may be needed when 'requiresDirectivesFromMetadata(true)' is used. 214 | * 215 | * @param alias group:name coordinates alias from version catalog 216 | * @param moduleName the Module Name of the Module referred to by the coordinates 217 | */ 218 | public void knownModule(Provider alias, String moduleName) { 219 | knownModule(alias.get().getModule().toString(), moduleName); 220 | } 221 | 222 | /** 223 | * Activate the plugin's functionality for dependencies of all scopes of the given source set 224 | * (runtimeClasspath, compileClasspath, annotationProcessor). 225 | * Note that the plugin activates the functionality for all source sets by default. 226 | * Therefore, this method only has an effect for source sets for which a {@link #deactivate(SourceSet)} 227 | * has been performed. 228 | * 229 | * @param sourceSet the Source Set to activate (e.g. sourceSets.test) 230 | */ 231 | public void activate(SourceSet sourceSet) { 232 | Configuration runtimeClasspath = getConfigurations().getByName(sourceSet.getRuntimeClasspathConfigurationName()); 233 | Configuration compileClasspath = getConfigurations().getByName(sourceSet.getCompileClasspathConfigurationName()); 234 | Configuration annotationProcessor = getConfigurations().getByName(sourceSet.getAnnotationProcessorConfigurationName()); 235 | 236 | activate(runtimeClasspath); 237 | activate(compileClasspath); 238 | activate(annotationProcessor); 239 | } 240 | 241 | /** 242 | * Activate the plugin's functionality for a single resolvable Configuration. 243 | * This is useful to use the plugins for scopes that are not tied to a Source Set, 244 | * for which the plugin does not activate automatically. 245 | * 246 | * @param resolvable a resolvable Configuration (e.g. configurations["customClasspath"]) 247 | */ 248 | public void activate(Configuration resolvable) { 249 | resolvable.getAttributes().attribute(JAVA_MODULE_ATTRIBUTE, true); 250 | } 251 | 252 | /** 253 | * Variant of {@link #activate(SourceSet)} and {@link #activate(Configuration)} that accepts either a 254 | * Provider of {@link SourceSet} or a Provider of {@link Configuration}. This is a convenience to use 255 | * notations like 'activate(sourceSets.main)' in Kotlin DSL. 256 | * 257 | * @param sourceSetOrResolvable the Source Set or Configuration to activate 258 | */ 259 | public void activate(NamedDomainObjectProvider sourceSetOrResolvable) { 260 | Object realized = sourceSetOrResolvable.get(); 261 | if (realized instanceof SourceSet) { 262 | activate((SourceSet) realized); 263 | } else if (realized instanceof Configuration) { 264 | activate((Configuration) realized); 265 | } else { 266 | throw new RuntimeException("Not SourceSet or Configuration: " + realized); 267 | } 268 | } 269 | 270 | /** 271 | * Deactivate the plugin's functionality for dependencies of all scopes of the given source set 272 | * (runtimeClasspath, compileClasspath, annotationProcessor). 273 | * 274 | * @param sourceSet the Source Set to deactivate (e.g. sourceSets.test) 275 | */ 276 | public void deactivate(SourceSet sourceSet) { 277 | Configuration runtimeClasspath = getConfigurations().getByName(sourceSet.getRuntimeClasspathConfigurationName()); 278 | Configuration compileClasspath = getConfigurations().getByName(sourceSet.getCompileClasspathConfigurationName()); 279 | Configuration annotationProcessor = getConfigurations().getByName(sourceSet.getAnnotationProcessorConfigurationName()); 280 | 281 | deactivate(runtimeClasspath); 282 | deactivate(compileClasspath); 283 | deactivate(annotationProcessor); 284 | } 285 | 286 | /** 287 | * Deactivate the plugin's functionality for a single resolvable Configuration. 288 | * This is useful if selected scopes do not use the Module Path and therefore 289 | * module information is not required. 290 | * 291 | * @param resolvable a resolvable Configuration (e.g. configurations.annotationProcessor) 292 | */ 293 | public void deactivate(Configuration resolvable) { 294 | resolvable.getAttributes().attribute(JAVA_MODULE_ATTRIBUTE, false); 295 | } 296 | 297 | /** 298 | * Variant of {@link #deactivate(SourceSet)} and {@link #deactivate(Configuration)} that accepts either a 299 | * Provider of {@link SourceSet} or a Provider of {@link Configuration}. This is a convenience to use 300 | * notations like 'deactivate(sourceSets.test)' in Kotlin DSL. 301 | * 302 | * @param sourceSetOrResolvable the Source Set or Configuration to activate 303 | */ 304 | public void deactivate(NamedDomainObjectProvider sourceSetOrResolvable) { 305 | Object realized = sourceSetOrResolvable.get(); 306 | if (realized instanceof SourceSet) { 307 | deactivate((SourceSet) realized); 308 | } else if (realized instanceof Configuration) { 309 | deactivate((Configuration) realized); 310 | } else { 311 | throw new RuntimeException("Not SourceSet or Configuration: " + realized); 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/main/java/org/gradlex/javamodule/moduleinfo/FilePathToModuleCoordinates.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright the GradleX team. 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 org.gradlex.javamodule.moduleinfo; 18 | 19 | import javax.annotation.Nullable; 20 | import java.nio.file.Path; 21 | import java.util.stream.Collectors; 22 | import java.util.stream.StreamSupport; 23 | 24 | /** 25 | * Attempts to parse 'group', 'name', 'version' coordinates from a paths like: 26 | * .gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.36/6c62681a2f655b49963a5983b8b0950a6120ae14/slf4j-api-1.7.36.jar 27 | * .m2/repository/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar 28 | */ 29 | final class FilePathToModuleCoordinates { 30 | 31 | @Nullable 32 | static String versionFromFilePath(Path path) { 33 | if (isInGradleCache(path)) { 34 | return getVersionFromGradleCachePath(path); 35 | } 36 | if (isInM2Cache(path)) { 37 | return getVersionFromM2CachePath(path); 38 | } 39 | return null; 40 | } 41 | 42 | static boolean gaCoordinatesFromFilePathMatch(Path path, String ga) { 43 | String name = nameCoordinateFromFilePath(path); 44 | String group = groupCoordinateFromFilePath(path); 45 | if (name == null || group == null) { 46 | return false; 47 | } 48 | return (isInGradleCache(path) && ga.equals(group + ":" + name)) || (isInM2Cache(path) && (group + ":" + name).endsWith("." + ga)); 49 | } 50 | 51 | @Nullable 52 | private static String groupCoordinateFromFilePath(Path path) { 53 | if (isInGradleCache(path)) { 54 | return path.getName(path.getNameCount() - 5).toString(); 55 | } 56 | if (isInM2Cache(path)) { 57 | return StreamSupport.stream(path.subpath(0, path.getNameCount() - 3).spliterator(), false).map(Path::toString).collect(Collectors.joining(".")); 58 | } 59 | return null; 60 | } 61 | 62 | static boolean isInGradleCache(Path path) { 63 | String name = nameCoordinateFromFilePath(path); 64 | if (name == null) { 65 | return false; 66 | } 67 | String version = getVersionFromGradleCachePath(path); 68 | return matchesPath(path, name, version); 69 | } 70 | 71 | static boolean isInM2Cache(Path path) { 72 | String name = nameCoordinateFromFilePath(path); 73 | if (name == null) { 74 | return false; 75 | } 76 | String version = getVersionFromM2CachePath(path); 77 | return matchesPath(path, name, version); 78 | } 79 | 80 | @Nullable 81 | private static String nameCoordinateFromFilePath(Path path) { 82 | if (path.getNameCount() < 5) { 83 | return null; 84 | } 85 | 86 | String nameFromGradleCachePath = path.getName(path.getNameCount() - 4).toString(); 87 | String versionFromGradleCachePath = getVersionFromGradleCachePath(path); 88 | if (matchesPath(path, nameFromGradleCachePath, versionFromGradleCachePath)) { 89 | return nameFromGradleCachePath; 90 | } 91 | String nameFromM2CachePath = path.getName(path.getNameCount() - 3).toString(); 92 | String versionFromM2CachePath = getVersionFromM2CachePath(path); 93 | if (matchesPath(path, nameFromM2CachePath, versionFromM2CachePath)) { 94 | return nameFromM2CachePath; 95 | } 96 | 97 | return null; 98 | } 99 | 100 | private static String getVersionFromGradleCachePath(Path path) { 101 | return path.getName(path.getNameCount() - 3).toString(); 102 | } 103 | 104 | private static String getVersionFromM2CachePath(Path path) { 105 | return path.getName(path.getNameCount() - 2).toString(); 106 | } 107 | 108 | private static boolean matchesPath(Path path, String name, String version) { 109 | String jarFileName = path.getFileName().toString(); 110 | return jarFileName.startsWith(name + "-") && !jarFileName.startsWith(version); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/org/gradlex/javamodule/moduleinfo/IdValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright the GradleX team. 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 org.gradlex.javamodule.moduleinfo; 18 | 19 | class IdValidator { 20 | static private final String COORDINATES_PATTERN = "^[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+(\\|[a-zA-Z0-9._-]+)?$"; 21 | static private final String FILE_NAME_PATTERN = "^[a-zA-Z0-9._-]+\\.(jar|zip)$"; 22 | 23 | static void validateIdentifier(String identifier) { 24 | if (!identifier.matches(COORDINATES_PATTERN) && !identifier.matches(FILE_NAME_PATTERN)) { 25 | throw new RuntimeException("'" + identifier + "' are not valid coordinates (group:name) / is not a valid file name (name-1.0.jar)"); 26 | } 27 | } 28 | 29 | static boolean isCoordinates(String identifier) { 30 | return identifier.matches(COORDINATES_PATTERN); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/gradlex/javamodule/moduleinfo/KnownModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright the GradleX team. 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 org.gradlex.javamodule.moduleinfo; 18 | 19 | public class KnownModule extends ModuleSpec { 20 | 21 | KnownModule(String identifier, String moduleName) { 22 | super(identifier, moduleName); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/gradlex/javamodule/moduleinfo/ModuleInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright the GradleX team. 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 org.gradlex.javamodule.moduleinfo; 18 | 19 | import org.gradle.api.model.ObjectFactory; 20 | 21 | import java.util.Arrays; 22 | import java.util.LinkedHashMap; 23 | import java.util.LinkedHashSet; 24 | import java.util.Map; 25 | import java.util.Set; 26 | 27 | /** 28 | * Data class to hold the information that should be added as module-info.class to an existing Jar file. 29 | */ 30 | @SuppressWarnings("unused") 31 | public class ModuleInfo extends ModuleSpec { 32 | 33 | private final String moduleVersion; 34 | 35 | boolean openModule = true; 36 | final Map> exports = new LinkedHashMap<>(); 37 | final Map> opens = new LinkedHashMap<>(); 38 | final Set requires = new LinkedHashSet<>(); 39 | final Set requiresTransitive = new LinkedHashSet<>(); 40 | final Set requiresStatic = new LinkedHashSet<>(); 41 | final Set requiresStaticTransitive = new LinkedHashSet<>(); 42 | final Map> ignoreServiceProviders = new LinkedHashMap<>(); 43 | final Set uses = new LinkedHashSet<>(); 44 | 45 | boolean exportAllPackages; 46 | boolean requireAllDefinedDependencies; 47 | boolean patchRealModule; 48 | boolean preserveExisting; 49 | 50 | ModuleInfo(String identifier, String moduleName, String moduleVersion, ObjectFactory objectFactory) { 51 | super(identifier, moduleName); 52 | this.moduleVersion = moduleVersion; 53 | } 54 | 55 | /** 56 | * Should this be a 'module' instead of an 'open module'? 57 | */ 58 | public void closeModule() { 59 | openModule = false; 60 | } 61 | 62 | /** 63 | * Calling this method at least once automatically makes this a "closed" module: 'module' instead of 'open module'. 64 | * 65 | * @param opens corresponds to the directive in a 'module-info.java' file 66 | * @param to modules this package should be opened to. 67 | */ 68 | public void opens(String opens, String... to) { 69 | closeModule(); 70 | addOrThrow(this.opens, opens, to); 71 | } 72 | 73 | /** 74 | * @param exports corresponds to the directive in a 'module-info.java' file 75 | * @param to modules this package should be exported to. 76 | */ 77 | public void exports(String exports, String... to) { 78 | addOrThrow(this.exports, exports, to); 79 | } 80 | 81 | /** 82 | * @param requires corresponds to the directive in a 'module-info.java' file 83 | */ 84 | public void requires(String requires) { 85 | addOrThrow(this.requires, requires); 86 | } 87 | 88 | /** 89 | * @param requiresTransitive corresponds to the directive in a 'module-info.java' file 90 | */ 91 | public void requiresTransitive(String requiresTransitive) { 92 | addOrThrow(this.requiresTransitive, requiresTransitive); 93 | } 94 | 95 | /** 96 | * @param requiresStatic corresponds to the directive in a 'module-info.java' file 97 | */ 98 | public void requiresStatic(String requiresStatic) { 99 | addOrThrow(this.requiresStatic, requiresStatic); 100 | } 101 | 102 | /** 103 | * @param requiresStaticTransitive corresponds to the directive in a 'module-info.java' file 104 | */ 105 | public void requiresStaticTransitive(String requiresStaticTransitive) { 106 | addOrThrow(this.requiresStaticTransitive, requiresStaticTransitive); 107 | } 108 | 109 | /** 110 | * @param uses corresponds to the directive in a 'module-info.java' file 111 | */ 112 | public void uses(String uses) { 113 | addOrThrow(this.uses, uses); 114 | } 115 | 116 | /** 117 | * @param provider do not transfer service provider to the 'module-info.class' 118 | * @param implementations the array of specific implementations to skip 119 | */ 120 | public void ignoreServiceProvider(String provider, String... implementations) { 121 | addOrThrow(this.ignoreServiceProviders, provider, implementations); 122 | } 123 | 124 | /** 125 | * @return configured version of the Module 126 | */ 127 | public String getModuleVersion() { 128 | return moduleVersion; 129 | } 130 | 131 | /** 132 | * Automatically export all packages of the Jar. Can be used instead of individual 'exports()' statements. 133 | */ 134 | public void exportAllPackages() { 135 | this.exportAllPackages = true; 136 | } 137 | 138 | /** 139 | * Automatically add 'requires' statements for all dependencies defined in the metadata of the component. 140 | */ 141 | public void requireAllDefinedDependencies() { 142 | this.requireAllDefinedDependencies = true; 143 | } 144 | 145 | /** 146 | * Allow patching real (JARs with module-info.class) modules by overriding the existing module-info.class. 147 | */ 148 | public void patchRealModule() { 149 | this.patchRealModule = true; 150 | } 151 | 152 | /** 153 | * Allow patching real (JARs with module-info.class) by extending the existing module-info.class. 154 | */ 155 | public void preserveExisting() { 156 | this.patchRealModule = true; 157 | this.preserveExisting = true; 158 | } 159 | 160 | private static void addOrThrow(Set target, String element) { 161 | if (!target.add(element)) { 162 | throw new IllegalArgumentException("The element '" + element + "' is already specified"); 163 | } 164 | } 165 | 166 | private static void addOrThrow(Map> target, String key, String... elements) { 167 | if (target.put(key, new LinkedHashSet<>(Arrays.asList(elements))) != null) { 168 | throw new IllegalArgumentException("The element '" + key + "' is already specified"); 169 | } 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/org/gradlex/javamodule/moduleinfo/ModuleNameUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright the GradleX team. 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 org.gradlex.javamodule.moduleinfo; 18 | 19 | import java.io.File; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | import java.util.regex.Matcher; 23 | import java.util.regex.Pattern; 24 | 25 | /** 26 | * Implementation based on 'jdk.internal.module.ModulePath#deriveModuleDescriptor' and related methods. 27 | */ 28 | class ModuleNameUtil { 29 | 30 | private static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))"); 31 | private static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]"); 32 | private static final Pattern REPEATING_DOTS = Pattern.compile("(\\.)(\\1)+"); 33 | private static final Pattern LEADING_DOTS = Pattern.compile("^\\."); 34 | private static final Pattern TRAILING_DOTS = Pattern.compile("\\.$"); 35 | 36 | static String automaticModulNameFromFileName(File jarFile) { 37 | // Derive the version, and the module name if needed, from JAR file name 38 | String fn = jarFile.getName(); 39 | int i = fn.lastIndexOf(File.separator); 40 | if (i != -1) 41 | fn = fn.substring(i + 1); 42 | 43 | // drop ".jar" 44 | String name = fn.substring(0, fn.length() - 4); 45 | 46 | // find first occurrence of -${NUMBER}. or -${NUMBER}$ 47 | Matcher matcher = DASH_VERSION.matcher(name); 48 | if (matcher.find()) { 49 | name = name.substring(0, matcher.start()); 50 | } 51 | return validateModuleName(cleanModuleName(name)); 52 | } 53 | 54 | static String validateModuleName(String name) { 55 | int next; 56 | int off = 0; 57 | while ((next = name.indexOf('.', off)) != -1) { 58 | String id = name.substring(off, next); 59 | if (!isJavaIdentifier(id)) { 60 | throw new IllegalArgumentException(name + ": Invalid module name" 61 | + ": '" + id + "' is not a Java identifier"); 62 | } 63 | off = next+1; 64 | } 65 | String last = name.substring(off); 66 | if (!isJavaIdentifier(last)) { 67 | throw new IllegalArgumentException(name + ": Invalid module name" 68 | + ": '" + last + "' is not a Java identifier"); 69 | } 70 | return name; 71 | } 72 | 73 | private static String cleanModuleName(String mn) { 74 | // replace non-alphanumeric 75 | mn = NON_ALPHANUM.matcher(mn).replaceAll("."); 76 | 77 | // collapse repeating dots 78 | mn = REPEATING_DOTS.matcher(mn).replaceAll("."); 79 | 80 | // drop leading dots 81 | if (!mn.isEmpty() && mn.charAt(0) == '.') 82 | mn = LEADING_DOTS.matcher(mn).replaceAll(""); 83 | 84 | // drop trailing dots 85 | int len = mn.length(); 86 | if (len > 0 && mn.charAt(len-1) == '.') 87 | mn = TRAILING_DOTS.matcher(mn).replaceAll(""); 88 | 89 | return mn; 90 | } 91 | 92 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") 93 | private static boolean isJavaIdentifier(String str) { 94 | if (str.isEmpty() || RESERVED.contains(str)) 95 | return false; 96 | 97 | int first = Character.codePointAt(str, 0); 98 | if (!Character.isJavaIdentifierStart(first)) 99 | return false; 100 | 101 | int i = Character.charCount(first); 102 | while (i < str.length()) { 103 | int cp = Character.codePointAt(str, i); 104 | if (!Character.isJavaIdentifierPart(cp)) 105 | return false; 106 | i += Character.charCount(cp); 107 | } 108 | 109 | return true; 110 | } 111 | 112 | // keywords, boolean and null literals, not allowed in identifiers 113 | private static final List RESERVED = Arrays.asList( 114 | "abstract", 115 | "assert", 116 | "boolean", 117 | "break", 118 | "byte", 119 | "case", 120 | "catch", 121 | "char", 122 | "class", 123 | "const", 124 | "continue", 125 | "default", 126 | "do", 127 | "double", 128 | "else", 129 | "enum", 130 | "extends", 131 | "final", 132 | "finally", 133 | "float", 134 | "for", 135 | "goto", 136 | "if", 137 | "implements", 138 | "import", 139 | "instanceof", 140 | "int", 141 | "interface", 142 | "long", 143 | "native", 144 | "new", 145 | "package", 146 | "private", 147 | "protected", 148 | "public", 149 | "return", 150 | "short", 151 | "static", 152 | "strictfp", 153 | "super", 154 | "switch", 155 | "synchronized", 156 | "this", 157 | "throw", 158 | "throws", 159 | "transient", 160 | "try", 161 | "void", 162 | "volatile", 163 | "while", 164 | "true", 165 | "false", 166 | "null", 167 | "_" 168 | ); 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/org/gradlex/javamodule/moduleinfo/ModuleSpec.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright the GradleX team. 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 org.gradlex.javamodule.moduleinfo; 18 | 19 | import org.gradle.api.artifacts.MinimalExternalModuleDependency; 20 | import org.gradle.api.provider.Provider; 21 | 22 | import java.io.Serializable; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | import static org.gradlex.javamodule.moduleinfo.IdValidator.validateIdentifier; 27 | import static org.gradlex.javamodule.moduleinfo.ModuleNameUtil.validateModuleName; 28 | 29 | /** 30 | * Details that real Modules and Automatic-Module-Names share. 31 | */ 32 | @SuppressWarnings("unused") 33 | public abstract class ModuleSpec implements Serializable { 34 | 35 | private final String identifier; 36 | private final String classifier; // optional 37 | private final String moduleName; 38 | private final List removedPackages = new ArrayList<>(); 39 | private final List mergedJars = new ArrayList<>(); 40 | 41 | boolean overrideModuleName; 42 | 43 | protected ModuleSpec(String identifier, String moduleName) { 44 | validateIdentifier(identifier); 45 | validateModuleName(moduleName); 46 | if (identifier.contains("|")) { 47 | this.identifier = identifier.split("\\|")[0]; 48 | this.classifier = identifier.split("\\|")[1]; 49 | } else { 50 | this.identifier = identifier; 51 | this.classifier = null; 52 | } 53 | this.moduleName = moduleName; 54 | } 55 | 56 | /** 57 | * @return group:name coordinates _or_ Jar file name 58 | */ 59 | public String getIdentifier() { 60 | return identifier; 61 | } 62 | 63 | /** 64 | * @return classifier, as an addition to group:name coordinates, if defined 65 | */ 66 | public String getClassifier() { 67 | return classifier; 68 | } 69 | 70 | /** 71 | * @return Module Name of the Module to construct 72 | */ 73 | public String getModuleName() { 74 | return moduleName; 75 | } 76 | 77 | /** 78 | * @param packageName a package to remove from the Jar because it is a duplicate 79 | */ 80 | public void removePackage(String packageName) { 81 | removedPackages.add(packageName); 82 | } 83 | 84 | /** 85 | * @return packages that are removed by the transform 86 | */ 87 | public List getRemovedPackages() { 88 | return removedPackages; 89 | } 90 | 91 | /** 92 | * @param identifier group:name coordinates _or_ Jar file name (of the Jar file to merge) 93 | */ 94 | public void mergeJar(String identifier) { 95 | mergedJars.add(identifier); 96 | } 97 | 98 | /** 99 | * @param alias group:name coordinates alias from version catalog 100 | */ 101 | public void mergeJar(Provider alias) { 102 | mergeJar(alias.get().getModule().toString()); 103 | } 104 | 105 | /** 106 | * @return all merged Jar identifiers 107 | */ 108 | public List getMergedJars() { 109 | return mergedJars; 110 | } 111 | 112 | /** 113 | * If the Module already has an Automatic-Module-Name, allow changing that name 114 | */ 115 | public void overrideModuleName() { 116 | this.overrideModuleName = true; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/org/gradlex/javamodule/moduleinfo/PublishedMetadata.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright the GradleX team. 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 org.gradlex.javamodule.moduleinfo; 18 | 19 | import org.gradle.api.Project; 20 | import org.gradle.api.artifacts.Configuration; 21 | import org.gradle.api.artifacts.ConfigurationContainer; 22 | import org.gradle.api.artifacts.result.DependencyResult; 23 | import org.gradle.api.artifacts.result.ResolvedDependencyResult; 24 | import org.gradle.api.artifacts.result.UnresolvedDependencyResult; 25 | import org.gradle.api.attributes.Attribute; 26 | import org.gradle.api.attributes.Bundling; 27 | import org.gradle.api.attributes.Category; 28 | import org.gradle.api.attributes.LibraryElements; 29 | import org.gradle.api.attributes.Usage; 30 | import org.gradle.api.attributes.java.TargetJvmEnvironment; 31 | import org.gradle.api.model.ObjectFactory; 32 | import org.gradle.api.provider.Provider; 33 | import org.gradle.api.tasks.SourceSet; 34 | import org.gradle.api.tasks.SourceSetContainer; 35 | import org.gradle.util.GradleVersion; 36 | 37 | import java.io.Serializable; 38 | import java.util.ArrayList; 39 | import java.util.List; 40 | import java.util.stream.Collectors; 41 | import java.util.stream.Stream; 42 | 43 | import static java.util.Collections.emptyList; 44 | import static java.util.Objects.requireNonNull; 45 | import static org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE; 46 | import static org.gradle.api.attributes.Category.LIBRARY; 47 | import static org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE; 48 | 49 | public class PublishedMetadata implements Serializable { 50 | private static final Attribute CATEGORY_ATTRIBUTE_UNTYPED = Attribute.of(CATEGORY_ATTRIBUTE.getName(), String.class); 51 | private static final String DEFAULT_VERSION_SOURCE_CONFIGURATION = "definedDependenciesVersions"; 52 | 53 | private final String gav; 54 | private final List requires = new ArrayList<>(); 55 | private final List requiresTransitive = new ArrayList<>(); 56 | private final List requiresStaticTransitive = new ArrayList<>(); 57 | private String errorMessage = null; 58 | 59 | PublishedMetadata(String gav, Project project, ExtraJavaModuleInfoPluginExtension extension) { 60 | this.gav = gav; 61 | 62 | List compileDependencies = componentVariant(extension.getVersionsProvidingConfiguration(), project, Usage.JAVA_API); 63 | List runtimeDependencies = componentVariant(extension.getVersionsProvidingConfiguration(), project, Usage.JAVA_RUNTIME); 64 | 65 | Stream.concat(compileDependencies.stream(), runtimeDependencies.stream()).distinct().forEach(ga -> { 66 | if (compileDependencies.contains(ga) && runtimeDependencies.contains(ga)) { 67 | requiresTransitive.add(ga); 68 | } else if (runtimeDependencies.contains(ga)) { 69 | requires.add(ga); 70 | } else if (compileDependencies.contains(ga)) { 71 | requiresStaticTransitive.add(ga); 72 | } 73 | }); 74 | } 75 | 76 | private List componentVariant(Provider versionsProvidingConfiguration, Project project, String usage) { 77 | Configuration versionsSource; 78 | if (versionsProvidingConfiguration.isPresent()) { 79 | versionsSource = project.getConfigurations().getByName(versionsProvidingConfiguration.get()); 80 | } else { 81 | // version provider is not configured, create on adhoc based on ALL classpaths of the project 82 | versionsSource = maybeCreateDefaultVersionSourcConfiguration(project.getConfigurations(), project.getObjects(), 83 | project.getExtensions().findByType(SourceSetContainer.class)); 84 | } 85 | 86 | Configuration singleComponentVariantResolver = project.getConfigurations().detachedConfiguration(project.getDependencies().create(gav)); 87 | singleComponentVariantResolver.setCanBeConsumed(false); 88 | singleComponentVariantResolver.shouldResolveConsistentlyWith(versionsSource); 89 | versionsSource.getAttributes().keySet().forEach(a -> { 90 | @SuppressWarnings("rawtypes") Attribute untypedAttributeKey = a; 91 | //noinspection unchecked 92 | singleComponentVariantResolver.getAttributes().attribute(untypedAttributeKey, requireNonNull(versionsSource.getAttributes().getAttribute(a))); 93 | }); 94 | singleComponentVariantResolver.getAttributes().attribute(USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, usage)); 95 | return firstAndOnlyComponentDependencies(singleComponentVariantResolver); 96 | } 97 | 98 | private Configuration maybeCreateDefaultVersionSourcConfiguration(ConfigurationContainer configurations, ObjectFactory objects, SourceSetContainer sourceSets) { 99 | String name = DEFAULT_VERSION_SOURCE_CONFIGURATION; 100 | Configuration existing = configurations.findByName(name); 101 | if (existing != null) { 102 | return existing; 103 | } 104 | 105 | return configurations.create(name, c -> { 106 | c.setCanBeResolved(true); 107 | c.setCanBeConsumed(false); 108 | c.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.class, Usage.JAVA_RUNTIME)); 109 | c.getAttributes().attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.class, Category.LIBRARY)); 110 | c.getAttributes().attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, LibraryElements.JAR)); 111 | c.getAttributes().attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.class, Bundling.EXTERNAL)); 112 | if (GradleVersion.current().compareTo(GradleVersion.version("7.0")) >= 0) { 113 | c.getAttributes().attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, 114 | objects.named(TargetJvmEnvironment.class, TargetJvmEnvironment.STANDARD_JVM)); 115 | } 116 | 117 | if (sourceSets != null) { 118 | for (SourceSet sourceSet : sourceSets) { 119 | Configuration implementation = configurations.getByName(sourceSet.getImplementationConfigurationName()); 120 | Configuration compileOnly = configurations.getByName(sourceSet.getCompileOnlyConfigurationName()); 121 | Configuration runtimeOnly = configurations.getByName(sourceSet.getRuntimeOnlyConfigurationName()); 122 | Configuration annotationProcessor = configurations.getByName(sourceSet.getAnnotationProcessorConfigurationName()); 123 | c.extendsFrom(implementation, compileOnly, runtimeOnly, annotationProcessor); 124 | } 125 | } 126 | }); 127 | } 128 | 129 | private List firstAndOnlyComponentDependencies(Configuration singleComponentVariantResolver) { 130 | DependencyResult result = singleComponentVariantResolver 131 | .getIncoming().getResolutionResult().getRoot() 132 | .getDependencies().iterator().next(); 133 | 134 | if (result instanceof UnresolvedDependencyResult) { 135 | errorMessage = ((UnresolvedDependencyResult) result).getFailure().getMessage(); 136 | return emptyList(); 137 | } else { 138 | return ((ResolvedDependencyResult) result).getSelected().getDependencies().stream() 139 | .filter(PublishedMetadata::filterComponentDependencies) 140 | .map(PublishedMetadata::ga) 141 | .collect(Collectors.toList()); 142 | } 143 | } 144 | 145 | private static boolean filterComponentDependencies(DependencyResult d) { 146 | if (d instanceof ResolvedDependencyResult) { 147 | Category category = ((ResolvedDependencyResult) d).getResolvedVariant().getAttributes().getAttribute(CATEGORY_ATTRIBUTE); 148 | String categoryUntyped = ((ResolvedDependencyResult) d).getResolvedVariant().getAttributes().getAttribute(CATEGORY_ATTRIBUTE_UNTYPED); 149 | return LIBRARY.equals(categoryUntyped) || (category != null && LIBRARY.equals(category.getName())); 150 | } 151 | return false; 152 | } 153 | 154 | private static String ga(DependencyResult d) { 155 | if (d instanceof ResolvedDependencyResult) { 156 | return ExtraJavaModuleInfoPlugin.ga(((ResolvedDependencyResult) d).getSelected().getId()); 157 | } 158 | return d.getRequested().getDisplayName(); 159 | } 160 | 161 | public String getGav() { 162 | return gav; 163 | } 164 | 165 | public List getRequires() { 166 | return requires; 167 | } 168 | 169 | public List getRequiresTransitive() { 170 | return requiresTransitive; 171 | } 172 | 173 | public List getRequiresStaticTransitive() { 174 | return requiresStaticTransitive; 175 | } 176 | 177 | public String getErrorMessage() { 178 | return errorMessage; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/org/gradlex/javamodule/moduleinfo/tasks/ModuleDescriptorRecommendation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright the GradleX team. 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 org.gradlex.javamodule.moduleinfo.tasks; 18 | 19 | import org.gradle.api.DefaultTask; 20 | import org.gradle.api.artifacts.ModuleIdentifier; 21 | import org.gradle.api.artifacts.ModuleVersionIdentifier; 22 | import org.gradle.api.artifacts.component.ComponentIdentifier; 23 | import org.gradle.api.artifacts.component.ModuleComponentIdentifier; 24 | import org.gradle.api.artifacts.result.DependencyResult; 25 | import org.gradle.api.artifacts.result.ResolvedComponentResult; 26 | import org.gradle.api.artifacts.result.ResolvedDependencyResult; 27 | import org.gradle.api.file.FileSystemOperations; 28 | import org.gradle.api.provider.ListProperty; 29 | import org.gradle.api.provider.Property; 30 | import org.gradle.api.tasks.Input; 31 | import org.gradle.api.tasks.InputFiles; 32 | import org.gradle.api.tasks.TaskAction; 33 | 34 | import javax.inject.Inject; 35 | import java.io.File; 36 | import java.io.IOException; 37 | import java.io.PrintWriter; 38 | import java.io.StringWriter; 39 | import java.nio.file.Files; 40 | import java.nio.file.Path; 41 | import java.util.ArrayList; 42 | import java.util.Collection; 43 | import java.util.Comparator; 44 | import java.util.HashMap; 45 | import java.util.HashSet; 46 | import java.util.List; 47 | import java.util.Map; 48 | import java.util.Set; 49 | import java.util.SortedSet; 50 | import java.util.TreeSet; 51 | import java.util.function.Function; 52 | import java.util.regex.Matcher; 53 | import java.util.regex.Pattern; 54 | import java.util.spi.ToolProvider; 55 | 56 | public abstract class ModuleDescriptorRecommendation extends DefaultTask { 57 | 58 | private static final class Artifact { 59 | 60 | final ModuleIdentifier coordinates; 61 | 62 | final Set runtimeDependencies = new HashSet<>(); 63 | 64 | final Set compileDependencies = new HashSet<>(); 65 | 66 | final File jar; 67 | 68 | final SortedSet requires = new TreeSet<>(); 69 | final SortedSet requiresTransitive = new TreeSet<>(); 70 | final SortedSet requiresStatic = new TreeSet<>(); 71 | final SortedSet exports = new TreeSet<>(); 72 | 73 | final SortedSet provides = new TreeSet<>(); 74 | 75 | String moduleName; 76 | 77 | boolean automatic; 78 | 79 | Artifact(ModuleIdentifier coordinates, File jar) { 80 | this.coordinates = coordinates; 81 | this.jar = jar; 82 | } 83 | 84 | Set allDependencies() { 85 | Set out = new HashSet<>(); 86 | out.addAll(compileDependencies); 87 | out.addAll(runtimeDependencies); 88 | return out; 89 | } 90 | 91 | boolean containsAnyRequires(String moduleName) { 92 | return requires.contains(moduleName) || requiresTransitive.contains(moduleName) || requiresStatic.contains(moduleName); 93 | } 94 | 95 | String dsl() { 96 | List out = new ArrayList<>(); 97 | String group = this.coordinates.getGroup(); 98 | String name = this.coordinates.getName(); 99 | String moduleName = this.moduleName; 100 | out.add("module('" + group + ":" + name + "', '" + moduleName + "') {"); 101 | out.add(" closeModule()"); 102 | for (String item : this.requiresTransitive) { 103 | out.add(" requiresTransitive('" + item + "')"); 104 | } 105 | for (String item : this.requiresStatic) { 106 | out.add(" requiresStatic('" + item + "')"); 107 | } 108 | for (String item : this.requires) { 109 | out.add(" requires('" + item + "')"); 110 | } 111 | for (String item : this.exports) { 112 | out.add(" exports('" + item + "')"); 113 | } 114 | for (String item : this.provides) { 115 | out.add(" // ignoreServiceProvider('" + item + "')"); 116 | } 117 | out.add("}"); 118 | return String.join("\n", out) 119 | .replace('\'', '"'); 120 | } 121 | 122 | } 123 | 124 | interface Java8SafeToolProvider { 125 | 126 | int run(PrintWriter out, PrintWriter err, String... args); 127 | 128 | @SuppressWarnings("Since15") 129 | static Java8SafeToolProvider findFirst(String name) { 130 | try { 131 | ToolProvider tool = ToolProvider.findFirst(name) 132 | .orElseThrow(() -> new RuntimeException("The JDK does not bundle " + name)); 133 | return tool::run; 134 | } catch (NoClassDefFoundError e) { 135 | throw new RuntimeException("This functionality requires Gradle to be powered by JDK 11+", e); 136 | } 137 | } 138 | 139 | } 140 | 141 | @InputFiles 142 | public abstract ListProperty getRuntimeArtifacts(); 143 | @Input 144 | public abstract ListProperty getRuntimeResolvedComponentResults(); 145 | 146 | @InputFiles 147 | public abstract ListProperty getCompileArtifacts(); 148 | @Input 149 | public abstract ListProperty getCompileResolvedComponentResults(); 150 | 151 | @Input 152 | public abstract Property getRelease(); 153 | 154 | @Inject 155 | protected abstract FileSystemOperations getFileSystemOperations(); 156 | 157 | @TaskAction 158 | public void execute() throws IOException { 159 | Java8SafeToolProvider jdepsTool = Java8SafeToolProvider.findFirst("jdeps"); 160 | Java8SafeToolProvider jarTool = Java8SafeToolProvider.findFirst("jar"); 161 | 162 | Map artifacts = new HashMap<>(); 163 | extractArtifactsAndTheirDependencies(artifacts, getRuntimeArtifacts().get(), getRuntimeResolvedComponentResults().get(), artifact -> artifact.runtimeDependencies); 164 | extractArtifactsAndTheirDependencies(artifacts, getCompileArtifacts().get(), getCompileResolvedComponentResults().get(), artifact -> artifact.compileDependencies); 165 | 166 | Path temporaryFolder = Files.createTempDirectory("jdeps-task"); 167 | for (Map.Entry entry : artifacts.entrySet()) { 168 | Artifact artifact = entry.getValue(); 169 | storeJarToolParsedMetadata(jarTool, artifact); 170 | if (artifact.automatic) { 171 | storeJdepsToolParsedMetadata(jdepsTool, temporaryFolder, artifact, artifacts.values()); 172 | } 173 | } 174 | List modulesToRecommend = new ArrayList<>(); 175 | for (Map.Entry entry : artifacts.entrySet()) { 176 | Artifact artifact = entry.getValue(); 177 | if (artifact.automatic) { 178 | for (ModuleIdentifier dependency : artifact.allDependencies()) { 179 | Artifact dependencyArtifact = artifacts.get(dependency); 180 | // If the dependency modifier was not identified by jdeps, try to find it the "best" possible requires modifier 181 | // using the same heuristic that is utilized by "requireAllDefinedDependencies()". 182 | if (!artifact.containsAnyRequires(dependencyArtifact.moduleName)) { 183 | boolean hasCompileDependency = artifact.compileDependencies.contains(dependencyArtifact.coordinates); 184 | boolean hasRuntimeDependency = artifact.runtimeDependencies.contains(dependencyArtifact.coordinates); 185 | if (hasCompileDependency && hasRuntimeDependency) { 186 | artifact.requiresTransitive.add(dependencyArtifact.moduleName); 187 | } else if (hasRuntimeDependency) { 188 | artifact.requires.add(dependencyArtifact.moduleName); 189 | } else if (hasCompileDependency) { 190 | artifact.requiresStatic.add(dependencyArtifact.moduleName); 191 | } 192 | } 193 | } 194 | modulesToRecommend.add(artifact); 195 | } 196 | } 197 | 198 | modulesToRecommend.sort(Comparator.comparing(entry -> entry.coordinates.getGroup()).thenComparing(entry->entry.coordinates.getName())); 199 | 200 | for (Artifact artifact : modulesToRecommend) { 201 | System.out.println(artifact.dsl()); 202 | } 203 | 204 | if (modulesToRecommend.isEmpty()) { 205 | System.out.println("All good. Looks like all the dependencies have the proper module-info.class defined"); 206 | } 207 | 208 | getFileSystemOperations().delete(spec -> spec.delete(temporaryFolder)); 209 | } 210 | 211 | private static void extractArtifactsAndTheirDependencies(Map jarsToAnalyze, 212 | List artifacts, 213 | List resolvedComponentResults, 214 | Function> depsSink) { 215 | for (ResolvedComponentResult artifact : resolvedComponentResults) { 216 | ComponentIdentifier identifier = artifact.getId(); 217 | if (identifier instanceof ModuleComponentIdentifier) { 218 | ModuleIdentifier moduleIdentifier = ((ModuleComponentIdentifier) identifier).getModuleIdentifier(); 219 | int index = resolvedComponentResults.indexOf(artifact); 220 | jarsToAnalyze.computeIfAbsent(moduleIdentifier, (ignore) -> new Artifact(moduleIdentifier, artifacts.get(index))); 221 | } 222 | } 223 | for (ResolvedComponentResult resolvedComponent : resolvedComponentResults) { 224 | ModuleVersionIdentifier moduleVersion = resolvedComponent.getModuleVersion(); 225 | if (moduleVersion == null) { 226 | continue; 227 | } 228 | Artifact artifact = jarsToAnalyze.get(moduleVersion.getModule()); 229 | if (artifact == null) { 230 | continue; 231 | } 232 | for (DependencyResult dependency : resolvedComponent.getDependencies()) { 233 | if (dependency instanceof ResolvedDependencyResult) { 234 | ModuleVersionIdentifier dependantModuleVersion = ((ResolvedDependencyResult) dependency).getSelected().getModuleVersion(); 235 | if (dependantModuleVersion != null) { 236 | depsSink.apply(artifact).add(dependantModuleVersion.getModule()); 237 | } 238 | } 239 | } 240 | } 241 | } 242 | 243 | private static final Pattern REQUIRES_PATTERN = Pattern.compile("^ {4}requires (transitive )?(.*);$"); 244 | private static final Pattern EXPORTS_PATTERN = Pattern.compile("^ {4}exports (.*);$"); 245 | private static final Pattern PROVIDES_PATTERN = Pattern.compile("^ {4}provides (.*) with$"); 246 | 247 | @SuppressWarnings("Since15") 248 | private void storeJdepsToolParsedMetadata(Java8SafeToolProvider jdeps, Path outputPath, Artifact targetArtifact, Collection jars) throws IOException { 249 | List modulePath = new ArrayList<>(); 250 | for (Artifact artifact : jars) { 251 | if (!artifact.equals(targetArtifact)) { 252 | modulePath.add(artifact.jar.getAbsolutePath()); 253 | } 254 | } 255 | StringWriter out = new StringWriter(); 256 | StringWriter err = new StringWriter(); 257 | List args = new ArrayList<>(); 258 | if (!modulePath.isEmpty()) { 259 | args.addAll(List.of( "--module-path", String.join(File.pathSeparator, modulePath))); 260 | } 261 | args.addAll(List.of("--generate-module-info", outputPath.toString())); 262 | args.addAll(List.of("--multi-release", String.valueOf(getRelease().get()))); 263 | args.add("--ignore-missing-deps"); 264 | args.add(targetArtifact.jar.getAbsolutePath()); 265 | int retVal = jdeps.run(new PrintWriter(out, true), new PrintWriter(err, true), args.toArray(String[]::new)); 266 | if (retVal != 0) { 267 | throw new RuntimeException(String.format("jdeps returned error %d\n%s\n%s", retVal, out, err)); 268 | } 269 | String[] result = out.toString().split("\\R"); 270 | String writingToMessage = result.length == 2 271 | ? result[1] // Skipping "Warning: --ignore-missing-deps specified. Missing dependencies from xyz are ignored" 272 | : result[0]; 273 | String path = writingToMessage.replace("writing to ", ""); 274 | String moduleInfoJava = Files.readString(Path.of(path)); 275 | String[] parts = moduleInfoJava.split("\\R"); 276 | for (String part : parts) { 277 | Matcher requiresMatcher = REQUIRES_PATTERN.matcher(part); 278 | if (requiresMatcher.matches()) { 279 | if (requiresMatcher.group(1) == null) { 280 | targetArtifact.requires.add(requiresMatcher.group(2)); 281 | } else { 282 | targetArtifact.requiresTransitive.add(requiresMatcher.group(2)); 283 | } 284 | continue; 285 | } 286 | Matcher exportsMatcher = EXPORTS_PATTERN.matcher(part); 287 | if (exportsMatcher.matches()) { 288 | targetArtifact.exports.add(exportsMatcher.group(1)); 289 | continue; 290 | } 291 | Matcher providesMatcher = PROVIDES_PATTERN.matcher(part); 292 | if (providesMatcher.matches()) { 293 | targetArtifact.provides.add(providesMatcher.group(1)); 294 | } 295 | } 296 | } 297 | 298 | private static final Pattern AUTOMATIC_MODULE_NAME_PATTERN = Pattern.compile("(.*?)(@.*)? automatic"); 299 | private static final Pattern MODULE_INFO_CLASS_MODULE_NAME_PATTERN = Pattern.compile("(.*?)(@.*)? jar:(.*)"); 300 | 301 | private void storeJarToolParsedMetadata(Java8SafeToolProvider jar, Artifact artifact) { 302 | StringWriter out = new StringWriter(); 303 | StringWriter err = new StringWriter(); 304 | int retVal = jar.run( 305 | new PrintWriter(out, true), 306 | new PrintWriter(err, true), 307 | "--describe-module", 308 | "--file", 309 | artifact.jar.getAbsolutePath(), 310 | "--release", 311 | String.valueOf(getRelease().get()) 312 | ); 313 | if (retVal != 0) { 314 | throw new RuntimeException(String.format("jar returned error %d\n%s\n%s", retVal, out, err)); 315 | } 316 | String[] result = out.toString().split("\\R"); 317 | if (result[0].equals("No module descriptor found. Derived automatic module.")) { 318 | Matcher matcher = AUTOMATIC_MODULE_NAME_PATTERN.matcher(result[2]); 319 | if (!matcher.matches()) { 320 | throw new RuntimeException("Cannot extract module name from: " + out); 321 | } 322 | artifact.moduleName = matcher.group(1); 323 | artifact.automatic = true; 324 | } else { 325 | Matcher matcher = MODULE_INFO_CLASS_MODULE_NAME_PATTERN.matcher(result[0].startsWith("releases: ") ? result[2] : result[0]); 326 | if (!matcher.matches()) { 327 | throw new RuntimeException("Cannot extract module name from: " + out); 328 | } 329 | artifact.moduleName = matcher.group(1); 330 | artifact.automatic = false; 331 | } 332 | } 333 | 334 | } -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/FilePathToModuleCoordinatesTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo 2 | 3 | import spock.lang.Specification 4 | 5 | import java.nio.file.Path 6 | 7 | import static org.gradlex.javamodule.moduleinfo.FilePathToModuleCoordinates.gaCoordinatesFromFilePathMatch 8 | import static org.gradlex.javamodule.moduleinfo.FilePathToModuleCoordinates.versionFromFilePath 9 | 10 | class FilePathToModuleCoordinatesTest extends Specification { 11 | 12 | def "version from gradle cache file path"() { 13 | given: 14 | def path = path('/Users/someone/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.36/6c62681a2f655b49963a5983b8b0950a6120ae14/slf4j-api-1.7.36.jar') 15 | 16 | expect: 17 | versionFromFilePath(path) == "1.7.36" 18 | } 19 | 20 | def "version from gradle cache file path (version in file name does not match)"() { 21 | given: 22 | def path = path('/Users/jendrik/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/33.2.1-jre/818e780da2c66c63bbb6480fef1f3855eeafa3e4/guava-33.2.1-android.jar') 23 | 24 | expect: 25 | versionFromFilePath(path) == "33.2.1-jre" 26 | } 27 | 28 | def "ga coordinates from gradle cache file path"() { 29 | given: 30 | def path = path('/Users/someone/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.36/6c62681a2f655b49963a5983b8b0950a6120ae14/slf4j-api-1.7.36.jar') 31 | 32 | expect: 33 | gaCoordinatesFromFilePathMatch(path, "org.slf4j:slf4j-api") 34 | } 35 | 36 | def "ga coordinates from gradle cache file path (version in file name does not match)"() { 37 | given: 38 | def path = path('/Users/jendrik/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/33.2.1-jre/818e780da2c66c63bbb6480fef1f3855eeafa3e4/guava-33.2.1-android.jar') 39 | 40 | expect: 41 | gaCoordinatesFromFilePathMatch(path, "com.google.guava:guava") 42 | } 43 | 44 | def "version from m2 repo file path"() { 45 | given: 46 | def path = path('/Users/someone/.m2/repository/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar') 47 | 48 | expect: 49 | versionFromFilePath(path) == "3.0.2" 50 | } 51 | 52 | def "version from m2 repo file path (version in file name does not match)"() { 53 | given: 54 | def path = path('/Users/someone/.m2/repository/com/google/guava/guava/33.2.1-jre/guava-33.2.1-android.jar') 55 | 56 | expect: 57 | versionFromFilePath(path) == "33.2.1-jre" 58 | } 59 | 60 | def "ga coordinates from m2 repo file path"() { 61 | Path jarPath 62 | 63 | when: 64 | jarPath = path('/Users/someone/.m2/repository/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar') 65 | 66 | then: 67 | gaCoordinatesFromFilePathMatch(jarPath, "com.google.code.findbugs:jsr305") 68 | 69 | when: 70 | jarPath = path('/Users/someone/.m2/repository/de/odysseus/juel/juel-impl/2.2.7/juel-impl-2.2.7.jar') 71 | 72 | then: 73 | gaCoordinatesFromFilePathMatch(jarPath, "de.odysseus.juel:juel-impl") 74 | } 75 | 76 | def "ga coordinates from m2 repo file path (version in file name does not match)"() { 77 | given: 78 | def path = path('/Users/someone/.m2/repository/com/google/guava/guava/33.2.1-jre/guava-33.2.1-android.jar') 79 | 80 | expect: 81 | gaCoordinatesFromFilePathMatch(path, "com.google.guava:guava") 82 | } 83 | 84 | private Path path(String path) { 85 | new File(path).toPath() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/AddressCatalogEntriesFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import org.gradlex.javamodule.moduleinfo.test.fixture.LegacyLibraries 5 | import spock.lang.IgnoreIf 6 | 7 | @IgnoreIf({ GradleBuild.gradleVersionUnderTest?.startsWith("6.") }) 8 | class AddressCatalogEntriesFunctionalTest extends AbstractFunctionalTest { 9 | 10 | LegacyLibraries libs = new LegacyLibraries(false, true) 11 | 12 | def setup() { 13 | if (build.gradleVersionUnderTest?.startsWith("7.0")) { 14 | settingsFile << ''' 15 | enableFeaturePreview("VERSION_CATALOGS") 16 | ''' 17 | } 18 | file("gradle/libs.versions.toml") << LegacyLibraries.catalog() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/AddressCoordinatesFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.LegacyLibraries 4 | 5 | class AddressCoordinatesFunctionalTest extends AbstractFunctionalTest { 6 | 7 | LegacyLibraries libs = new LegacyLibraries(false) 8 | } 9 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/AddressJarFilesFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.LegacyLibraries 4 | 5 | class AddressJarFilesFunctionalTest extends AbstractFunctionalTest { 6 | 7 | LegacyLibraries libs = new LegacyLibraries(true) 8 | } 9 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/AllowAutomaticModulesFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import spock.lang.Specification 5 | 6 | class AllowAutomaticModulesFunctionalTest extends Specification { 7 | 8 | @Delegate 9 | GradleBuild build = new GradleBuild() 10 | 11 | def setup() { 12 | settingsFile << 'rootProject.name = "test-project"' 13 | buildFile << ''' 14 | plugins { 15 | id("application") 16 | id("org.gradlex.extra-java-module-info") 17 | } 18 | application { 19 | mainModule.set("org.gradle.sample.app") 20 | mainClass.set("org.gradle.sample.app.Main") 21 | } 22 | ''' 23 | file("src/main/java/module-info.java") << ''' 24 | module org.gradle.sample.app { 25 | requires org.yaml.snakeyaml; 26 | } 27 | ''' 28 | file("src/main/java/org/gradle/sample/app/Main.java") << ''' 29 | package org.gradle.sample.app; 30 | 31 | import org.yaml.snakeyaml.Yaml; 32 | 33 | public class Main { 34 | public static void main(String[] args) { 35 | System.out.println("Automatic: " + Yaml.class.getModule().getDescriptor().isAutomatic()); 36 | } 37 | } 38 | ''' 39 | } 40 | 41 | def "automatic modules are allowed by default"() { 42 | given: 43 | buildFile << ''' 44 | dependencies { 45 | implementation("org.yaml:snakeyaml:1.33") 46 | } 47 | extraJavaModuleInfo { 48 | 49 | } 50 | ''' 51 | 52 | expect: 53 | def out = run() 54 | out.output.contains("Automatic: true") 55 | } 56 | 57 | def "automatic modules are not allowed when failOnAutomaticModules set to true"() { 58 | given: 59 | buildFile << ''' 60 | dependencies { 61 | implementation("org.yaml:snakeyaml:1.33") 62 | } 63 | extraJavaModuleInfo { 64 | failOnAutomaticModules.set(true) 65 | } 66 | ''' 67 | 68 | expect: 69 | def out = failRun() 70 | out.output.contains("Found an automatic module: org.yaml.snakeyaml (snakeyaml-1.33.jar)") 71 | } 72 | 73 | def "automatic modules are allowed when failOnAutomaticModules set to true and there is a proper module override"() { 74 | given: 75 | buildFile << ''' 76 | dependencies { 77 | implementation("org.yaml:snakeyaml:1.33") 78 | } 79 | extraJavaModuleInfo { 80 | failOnAutomaticModules.set(true) 81 | module("org.yaml:snakeyaml", "org.yaml.snakeyaml") { 82 | closeModule() 83 | exports("org.yaml.snakeyaml") 84 | } 85 | } 86 | ''' 87 | 88 | expect: 89 | def out = run() 90 | out.output.contains("Automatic: false") 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/ClassifiedJarsFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradle.testkit.runner.TaskOutcome 4 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 5 | import spock.lang.Specification 6 | 7 | class ClassifiedJarsFunctionalTest extends Specification { 8 | 9 | @Delegate 10 | GradleBuild build = new GradleBuild() 11 | 12 | def setup() { 13 | settingsFile << 'rootProject.name = "test-project"' 14 | buildFile << ''' 15 | plugins { 16 | id("application") 17 | id("org.gradlex.extra-java-module-info") 18 | } 19 | application { 20 | mainModule.set("org.gradle.sample.app") 21 | mainClass.set("org.gradle.sample.app.Main") 22 | } 23 | ''' 24 | } 25 | 26 | def "can address classified Jars via coordinates"() { 27 | given: 28 | file("src/main/java/org/gradle/sample/app/Main.java") << """ 29 | package org.gradle.sample.app; 30 | public class Main { 31 | public static void main(String[] args) { } 32 | } 33 | """ 34 | file("src/main/java/module-info.java") << """ 35 | module org.gradle.sample.app { 36 | requires io.netty.transport.epoll.linux.x86_64; 37 | requires io.netty.transport.epoll.linux.aarch_64; 38 | } 39 | """ 40 | buildFile << """ 41 | dependencies { 42 | implementation(platform("io.netty:netty-bom:4.1.110.Final")) 43 | implementation("io.netty:netty-transport-native-epoll:0:linux-x86_64") 44 | implementation("io.netty:netty-transport-native-epoll:0:linux-aarch_64") 45 | } 46 | extraJavaModuleInfo { 47 | failOnAutomaticModules.set(true) 48 | module("io.netty:netty-transport-native-epoll|linux-x86_64", "io.netty.transport.epoll.linux.x86_64") 49 | module("io.netty:netty-transport-native-epoll|linux-aarch_64", "io.netty.transport.epoll.linux.aarch_64") 50 | 51 | module("io.netty:netty-transport-native-unix-common", "io.netty.transport.unix.common") 52 | module("io.netty:netty-buffer", "io.netty.buffer") 53 | module("io.netty:netty-codec", "io.netty.codec") 54 | module("io.netty:netty-common", "io.netty.common") 55 | module("io.netty:netty-handler", "io.netty.handler") 56 | module("io.netty:netty-resolver", "io.netty.resolver") 57 | module("io.netty:netty-transport", "io.netty.transport") 58 | module("io.netty:netty-transport-classes-epoll", "io.netty.transport.classes.epoll") 59 | } 60 | """ 61 | 62 | expect: 63 | build().task(':compileJava').outcome == TaskOutcome.SUCCESS 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/CombinationWithOtherPluginsFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import org.gradle.testkit.runner.TaskOutcome 5 | import spock.lang.IgnoreIf 6 | import spock.lang.Specification 7 | 8 | import java.util.jar.JarFile 9 | 10 | class CombinationWithOtherPluginsFunctionalTest extends Specification { 11 | 12 | @Delegate 13 | GradleBuild build = new GradleBuild() 14 | 15 | def setup() { 16 | settingsFile << 'rootProject.name = "test-project"' 17 | } 18 | 19 | def "mergeJar uses versions configured through jvm-dependency-conflict-resolution plugin"() { 20 | given: 21 | file("src/main/java/org/gradle/sample/app/Main.java") << """ 22 | package org.gradle.sample.app; 23 | public class Main { 24 | public static void main(String[] args) { 25 | Class loggerFromApi = org.slf4j.Logger.class; 26 | Class ndcFromExt = org.slf4j.NDC.class; 27 | } 28 | } 29 | """ 30 | file("src/main/java/module-info.java") << """ 31 | module org.gradle.sample.app { 32 | requires org.slf4j; 33 | } 34 | """ 35 | settingsFile << """ 36 | include("versions") 37 | """ 38 | file('versions/build.gradle.kts') << """ 39 | plugins { id("java-platform") } 40 | dependencies.constraints { 41 | api("org.slf4j:slf4j-api:1.7.32") 42 | api("org.slf4j:slf4j-ext:1.7.32") 43 | } 44 | """ 45 | buildFile << """ 46 | plugins { 47 | id("application") 48 | id("org.gradlex.extra-java-module-info") 49 | id("org.gradlex.jvm-dependency-conflict-resolution") version "2.1.2" 50 | } 51 | application.mainClass.set("org.gradle.sample.app.Main") 52 | dependencies { 53 | implementation("org.slf4j:slf4j-api") 54 | } 55 | jvmDependencyConflicts { 56 | consistentResolution { 57 | providesVersions(":") 58 | platform(":versions") 59 | } 60 | } 61 | extraJavaModuleInfo { 62 | automaticModule("org.slf4j:slf4j-api", "org.slf4j") { 63 | mergeJar("org.slf4j:slf4j-ext") 64 | } 65 | } 66 | tasks.named("run") { 67 | inputs.files(configurations.runtimeClasspath) 68 | doLast { println(inputs.files.map { it.name }) } 69 | } 70 | """ 71 | 72 | when: 73 | def result = run() 74 | 75 | then: 76 | result.task(":run").outcome == TaskOutcome.SUCCESS 77 | result.output.contains('slf4j-api-1.7.32-module.jar') 78 | !result.output.contains('slf4j-api-1.7.32.jar') 79 | !result.output.contains('slf4j-ext-1.7.32.jar') 80 | } 81 | 82 | @IgnoreIf({ !GradleBuild.gradleVersionUnderTest?.startsWith('7.') }) 83 | def "works in combination with shadow plugin"() { 84 | def shadowJar = file("app/build/libs/app-all.jar") 85 | 86 | given: 87 | settingsFile << """ 88 | include("app", "utilities") 89 | """ 90 | file("utilities/src/main/java/module-info.java") << """ 91 | module utilities { } 92 | """ 93 | file("utilities/src/main/java/Utility.java") << """ 94 | public class Utility { } 95 | """ 96 | file("utilities/build.gradle") << """ 97 | plugins { 98 | id 'java-library' 99 | } 100 | java.modularity.inferModulePath = true 101 | """ 102 | file("app/src/main/java/module-info.java") << """ 103 | module app { } 104 | """ 105 | file("app/src/main/java/App.java") << """ 106 | public class App { } 107 | """ 108 | file("app/build.gradle") << """ 109 | plugins { 110 | id 'application' 111 | id 'org.gradlex.extra-java-module-info' 112 | id 'com.github.johnrengelman.shadow' version '7.1.2' 113 | } 114 | dependencies { 115 | implementation project(':utilities') 116 | } 117 | java.modularity.inferModulePath = true 118 | application.mainClass = 'App' 119 | configurations { 120 | runtimeClasspath { 121 | attributes { attribute(Attribute.of("javaModule", Boolean), false) } 122 | } 123 | } 124 | """ 125 | 126 | expect: 127 | runner(':app:shadowJar').build().task(':app:shadowJar').outcome == TaskOutcome.SUCCESS 128 | shadowJar.exists() 129 | // 4 Entries = 2 * class file, 1 * META-INF folder, 1 * MANIFEST 130 | // module-info.class is excluded (see: https://github.com/johnrengelman/shadow/issues/352) 131 | new JarFile(shadowJar).entries().asIterator().size() == 4 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/ConfigurationDetailsFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import org.gradlex.javamodule.moduleinfo.test.fixture.LegacyLibraries 5 | import org.gradle.testkit.runner.TaskOutcome 6 | import spock.lang.Specification 7 | 8 | class ConfigurationDetailsFunctionalTest extends Specification { 9 | 10 | @Delegate 11 | GradleBuild build = new GradleBuild() 12 | 13 | def setup() { 14 | settingsFile << 'rootProject.name = "test-project"' 15 | buildFile << ''' 16 | plugins { 17 | id("application") 18 | id("org.gradlex.extra-java-module-info") 19 | } 20 | application { 21 | mainModule.set("org.gradle.sample.app") 22 | mainClass.set("org.gradle.sample.app.Main") 23 | } 24 | ''' 25 | } 26 | 27 | def "fails by default if no module info is defined for a legacy library"() { 28 | given: 29 | file("src/main/java/module-info.java") << """ 30 | module org.gradle.sample.app { } 31 | """ 32 | buildFile << """ 33 | dependencies { implementation("commons-cli:commons-cli:1.4") } 34 | """ 35 | 36 | expect: 37 | fail().task(':compileJava').outcome == TaskOutcome.FAILED 38 | } 39 | 40 | def "can opt-out of strict module requirement"() { 41 | given: 42 | file("src/main/java/module-info.java") << """ 43 | module org.gradle.sample.app { } 44 | """ 45 | buildFile << """ 46 | dependencies { implementation("commons-cli:commons-cli:1.4") } 47 | extraJavaModuleInfo { 48 | failOnMissingModuleInfo.set(false) 49 | } 50 | tasks.compileJava { 51 | doLast { println(classpath.map { it.name }) } 52 | } 53 | """ 54 | 55 | when: 56 | def result = build() 57 | 58 | then: 59 | result.task(':compileJava').outcome == TaskOutcome.SUCCESS 60 | result.output.contains('[commons-cli-1.4.jar]') 61 | } 62 | 63 | def "can opt-out for selected configurations by modifying the javaModule attribute"() { 64 | given: 65 | file("src/test/java/Test.java") << "" 66 | buildFile << """ 67 | configurations { 68 | testRuntimeClasspath { 69 | attributes { attribute(Attribute.of("javaModule", Boolean::class.javaObjectType), false) } 70 | } 71 | testCompileClasspath { 72 | attributes { attribute(Attribute.of("javaModule", Boolean::class.javaObjectType), false) } 73 | } 74 | } 75 | dependencies { testImplementation("commons-cli:commons-cli:1.4") } 76 | """ 77 | 78 | expect: 79 | build().task(':compileTestJava').outcome == TaskOutcome.SUCCESS 80 | } 81 | 82 | // See: https://github.com/jjohannes/extra-java-module-info/issues/23 83 | def "ignores MANIFEST.MF files that are not correctly positioned in Jar"() { 84 | given: 85 | file("src/main/java/org/gradle/sample/app/Main.java") << """ 86 | package org.gradle.sample.app; 87 | 88 | public class Main { 89 | public static void main(String[] args) throws Exception { 90 | } 91 | } 92 | """ 93 | 94 | file("src/main/java/module-info.java") << """ 95 | module org.gradle.sample.app { 96 | requires com.google.javascript.closure.compiler; 97 | } 98 | """ 99 | buildFile << """ 100 | dependencies { 101 | implementation("com.google.javascript:closure-compiler:v20211201") 102 | } 103 | 104 | extraJavaModuleInfo { 105 | automaticModule(${new LegacyLibraries().closureCompiler}, "com.google.javascript.closure.compiler") 106 | } 107 | 108 | application { 109 | mainModule.set("org.gradle.sample.app") 110 | mainClass.set("org.gradle.sample.app.Main") 111 | } 112 | """ 113 | 114 | expect: 115 | build().task(':compileJava').outcome == TaskOutcome.SUCCESS 116 | } 117 | 118 | // See: https://github.com/jjohannes/extra-java-module-info/issues/27 119 | def "correctly handles copying JAR contents on JDKs < 16"() { 120 | given: 121 | file("src/main/java/org/gradle/sample/app/Main.java") << """ 122 | package org.gradle.sample.app; 123 | 124 | public class Main { 125 | public static void main(String[] args) { 126 | System.out.println(org.w3c.css.sac.Parser.class); 127 | } 128 | } 129 | """ 130 | file("src/main/java/module-info.java") << """ 131 | module org.gradle.sample.app { 132 | requires sac; 133 | } 134 | """ 135 | buildFile << """ 136 | dependencies { 137 | implementation("org.w3c.css:sac:1.3") 138 | } 139 | 140 | extraJavaModuleInfo { 141 | automaticModule(${new LegacyLibraries().sac}, "sac") 142 | } 143 | 144 | application { 145 | mainModule.set("org.gradle.sample.app") 146 | mainClass.set("org.gradle.sample.app.Main") 147 | } 148 | """ 149 | 150 | expect: 151 | build().task(':compileJava').outcome == TaskOutcome.SUCCESS 152 | } 153 | 154 | def "can define version of module explicitly"() { 155 | given: 156 | def libs = new LegacyLibraries() 157 | file("src/main/java/org/gradle/sample/app/Main.java") << """ 158 | package org.gradle.sample.app; 159 | 160 | import org.apache.commons.cli.CommandLineParser; 161 | 162 | public class Main { 163 | public static void main(String[] args) throws Exception { 164 | System.out.println(CommandLineParser.class.getModule().getName() + "=" + CommandLineParser.class.getModule().getDescriptor().version().get()); 165 | System.out.println(ModuleLayer.boot().findModule("org.apache.commons.collections").get().getName() + "=" + ModuleLayer.boot().findModule("org.apache.commons.collections").get().getDescriptor().version().get()); 166 | } 167 | } 168 | """ 169 | file("src/main/java/module-info.java") << """ 170 | module org.gradle.sample.app { 171 | requires org.apache.commons.cli; 172 | requires org.apache.commons.collections; 173 | } 174 | """ 175 | buildFile << """ 176 | dependencies { 177 | implementation("commons-cli:commons-cli:1.4") 178 | implementation("commons-collections:commons-collections:3.2.2") 179 | } 180 | 181 | extraJavaModuleInfo { 182 | module(${libs.commonsCli}, "org.apache.commons.cli", "8.1") { 183 | exports("org.apache.commons.cli") 184 | } 185 | module(${libs.commonsCollections}, "org.apache.commons.collections", "9.2") 186 | } 187 | """ 188 | 189 | when: 190 | def result = run() 191 | 192 | then: 193 | result.task(':run').outcome == TaskOutcome.SUCCESS 194 | result.output.contains('org.apache.commons.cli=8.1') 195 | result.output.contains('org.apache.commons.collections=9.2') 196 | } 197 | 198 | def "automatically uses resolved version for module version"() { 199 | given: 200 | def libs = new LegacyLibraries() 201 | file("src/main/java/org/gradle/sample/app/Main.java") << """ 202 | package org.gradle.sample.app; 203 | 204 | import org.apache.commons.cli.CommandLineParser; 205 | 206 | public class Main { 207 | public static void main(String[] args) throws Exception { 208 | System.out.println(CommandLineParser.class.getModule().getName() + "=" + CommandLineParser.class.getModule().getDescriptor().version().get()); 209 | System.out.println(ModuleLayer.boot().findModule("org.apache.commons.collections").get().getName() + "=" + ModuleLayer.boot().findModule("org.apache.commons.collections").get().getDescriptor().version().get()); 210 | } 211 | } 212 | """ 213 | file("src/main/java/module-info.java") << """ 214 | module org.gradle.sample.app { 215 | requires org.apache.commons.cli; 216 | requires org.apache.commons.collections; 217 | 218 | requires static jsr305; 219 | } 220 | """ 221 | buildFile << """ 222 | dependencies { 223 | implementation("commons-cli:commons-cli:1.4") 224 | implementation("commons-collections:commons-collections:3.2.2") 225 | 226 | compileOnly("com.google.code.findbugs:jsr305:3.0.2") 227 | } 228 | 229 | extraJavaModuleInfo { 230 | module(${libs.commonsCli}, "org.apache.commons.cli") { 231 | exports("org.apache.commons.cli") 232 | } 233 | module(${libs.commonsCollections}, "org.apache.commons.collections") 234 | 235 | automaticModule(${libs.jsr305}, "jsr305") 236 | } 237 | """ 238 | 239 | when: 240 | def result = run() 241 | 242 | then: 243 | result.task(':run').outcome == TaskOutcome.SUCCESS 244 | result.output.contains('org.apache.commons.cli=1.4') 245 | result.output.contains('org.apache.commons.collections=3.2.2') 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/EdgeCasesFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradle.testkit.runner.TaskOutcome 4 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 5 | import org.gradlex.javamodule.moduleinfo.test.fixture.LegacyLibraries 6 | import spock.lang.Specification 7 | 8 | class EdgeCasesFunctionalTest extends Specification { 9 | 10 | @Delegate 11 | GradleBuild build = new GradleBuild() 12 | 13 | LegacyLibraries libs = new LegacyLibraries(false) 14 | 15 | def setup() { 16 | settingsFile << 'rootProject.name = "test-project"' 17 | buildFile << ''' 18 | plugins { 19 | id("application") 20 | id("org.gradlex.extra-java-module-info") 21 | } 22 | application { 23 | mainModule.set("org.gradle.sample.app") 24 | mainClass.set("org.gradle.sample.app.Main") 25 | } 26 | ''' 27 | } 28 | 29 | def "does not fail if an unused Jar on the merge path cannot be resolved"() { 30 | given: 31 | file("src/main/java/org/gradle/sample/app/Main.java") << """ 32 | package org.gradle.sample.app; 33 | 34 | public class Main { 35 | public static void main(String[] args) { 36 | } 37 | } 38 | """ 39 | file("src/main/java/module-info.java") << """ 40 | module org.gradle.sample.app { 41 | requires org.slf4j; 42 | } 43 | """ 44 | buildFile << """ 45 | dependencies { 46 | implementation("org.slf4j:slf4j-api:2.0.3") 47 | } 48 | 49 | extraJavaModuleInfo { 50 | failOnMissingModuleInfo.set(false) 51 | module(${libs.zookeeper}, "org.apache.zookeeper") { 52 | mergeJar(${libs.zookeeperJute}) 53 | 54 | exports("org.apache.jute") 55 | exports("org.apache.zookeeper") 56 | exports("org.apache.zookeeper.server.persistence") 57 | } 58 | } 59 | """ 60 | 61 | expect: 62 | run() 63 | } 64 | 65 | def "does fully merge zip files on the classpath"() { 66 | given: 67 | buildFile << """ 68 | dependencies { 69 | implementation("org.apache.qpid:qpid-broker-core:9.0.0") 70 | implementation("org.apache.qpid:qpid-broker-plugins-management-http:9.0.0") 71 | } 72 | 73 | extraJavaModuleInfo { 74 | failOnMissingModuleInfo.set(false) 75 | automaticModule("org.apache.qpid:qpid-broker-core", "org.apache.qpid.broker") { 76 | mergeJar("org.apache.qpid:qpid-broker-plugins-management-http") 77 | mergeJar("org.dojotoolkit:dojo") // This is a Zip, selected by 'distribution' classifier in dependencies of 'qpid-broker-plugins-management-http' 78 | mergeJar("org.webjars.bower:dgrid") 79 | mergeJar("org.webjars.bower:dstore") 80 | } 81 | } 82 | 83 | tasks.named("build") { 84 | inputs.files(configurations.runtimeClasspath) 85 | doLast { println(inputs.files.map { it.name }) } 86 | } 87 | """ 88 | 89 | when: 90 | def result = build() 91 | 92 | then: 93 | result.output.contains('qpid-broker-core-9.0.0-module.jar') 94 | !result.output.contains('dgrid-1.3.3.jar') 95 | !result.output.contains('dojo-1.17.2-distribution.zip') 96 | !result.output.contains('dstore-1.1.2.jar') 97 | !result.output.contains('qpid-broker-plugins-management-http-9.0.0.jar') 98 | } 99 | 100 | def "can merge jars that are already modules"() { 101 | given: 102 | file("src/main/java/org/gradle/sample/app/Main.java") << """ 103 | package org.gradle.sample.app; 104 | 105 | public class Main { 106 | public static void main(String[] args) throws Exception { 107 | } 108 | } 109 | """ 110 | file("src/main/java/module-info.java") << """ 111 | module org.gradle.sample.app { 112 | requires java.annotation; 113 | } 114 | """ 115 | buildFile << """ 116 | dependencies { 117 | implementation("com.google.code.findbugs:jsr305:3.0.2") 118 | implementation("javax.annotation:javax.annotation-api:1.3.2") 119 | } 120 | 121 | extraJavaModuleInfo { 122 | module("com.google.code.findbugs:jsr305", "java.annotation") { 123 | mergeJar("javax.annotation:javax.annotation-api") 124 | exports("javax.annotation") 125 | exports("javax.annotation.concurrent") 126 | exports("javax.annotation.meta") 127 | } 128 | } 129 | 130 | tasks.named("run") { 131 | inputs.files(configurations.runtimeClasspath) 132 | doLast { println(inputs.files.map { it.name }) } 133 | } 134 | """ 135 | 136 | when: 137 | def result = run() 138 | 139 | then: 140 | result.output.contains('jsr305-3.0.2-module.jar') 141 | } 142 | 143 | def "can automatically export all packages of a multi-release legacy Jar"() { 144 | given: 145 | file("src/main/java/org/gradle/sample/app/Main.java") << """ 146 | package org.gradle.sample.app; 147 | 148 | import org.kohsuke.github.GHApp; 149 | 150 | public class Main { 151 | public static void main(String[] args) { 152 | GHApp app = new GHApp(); 153 | } 154 | } 155 | """ 156 | file("src/main/java/module-info.java") << """ 157 | module org.gradle.sample.app { 158 | exports org.gradle.sample.app; 159 | 160 | requires org.kohsuke.github.api; 161 | } 162 | """ 163 | buildFile << """ 164 | dependencies { 165 | implementation("org.kohsuke:github-api:1.317") 166 | } 167 | 168 | extraJavaModuleInfo { 169 | module("org.kohsuke:github-api", "org.kohsuke.github.api") { 170 | exportAllPackages() 171 | requires("org.apache.commons.lang3") 172 | } 173 | } 174 | """ 175 | 176 | expect: 177 | run().task(':run').outcome == TaskOutcome.SUCCESS 178 | } 179 | 180 | def "deriveAutomaticModuleNamesFromFileNames produces a build time error for invalid module names"() { 181 | given: 182 | buildFile << """ 183 | dependencies { 184 | implementation("org.nd4j:nd4j-native-api:0.9.1") 185 | } 186 | 187 | extraJavaModuleInfo { 188 | deriveAutomaticModuleNamesFromFileNames.set(true) 189 | } 190 | """ 191 | 192 | expect: 193 | def result = failRun() 194 | result.output.contains "nd4j.native.api: Invalid module name: 'native' is not a Java identifier" 195 | } 196 | 197 | def "fail if module name does not correspond to Automatic-Module-Name - module"() { 198 | given: 199 | buildFile << """ 200 | dependencies { 201 | implementation("org.apache.commons:commons-lang3:3.10") 202 | } 203 | 204 | extraJavaModuleInfo { 205 | module("org.apache.commons:commons-lang3", "org.apache.commons.lang") { 206 | exportAllPackages() 207 | } 208 | } 209 | """ 210 | 211 | expect: 212 | def result = failRun() 213 | result.output.contains "The name 'org.apache.commons.lang' is different than the Automatic-Module-Name 'org.apache.commons.lang3'; explicitly allow override via 'overrideModuleName()'" 214 | } 215 | 216 | def "fail if module name does not correspond to Automatic-Module-Name - automaticModule"() { 217 | given: 218 | buildFile << """ 219 | dependencies { 220 | implementation("org.apache.commons:commons-lang3:3.10") 221 | } 222 | 223 | extraJavaModuleInfo { 224 | automaticModule("org.apache.commons:commons-lang3", "org.apache.commons.lang") 225 | } 226 | """ 227 | 228 | expect: 229 | def result = failRun() 230 | result.output.contains "'org.apache.commons.lang' already has the Automatic-Module-Name 'org.apache.commons.lang3'; explicitly allow override via 'overrideModuleName()'" 231 | } 232 | 233 | def "do not fail if overrideModuleName is set - module"() { 234 | given: 235 | buildFile << """ 236 | dependencies { 237 | implementation("org.apache.commons:commons-lang3:3.10") 238 | } 239 | 240 | extraJavaModuleInfo { 241 | module("org.apache.commons:commons-lang3", "org.apache.commons.lang") { 242 | overrideModuleName() 243 | exportAllPackages() 244 | } 245 | } 246 | """ 247 | 248 | expect: 249 | build() 250 | } 251 | 252 | def "do not fail if overrideModuleName is set - automaticModule"() { 253 | given: 254 | buildFile << """ 255 | dependencies { 256 | implementation("org.apache.commons:commons-lang3:3.10") 257 | } 258 | 259 | extraJavaModuleInfo { 260 | automaticModule("org.apache.commons:commons-lang3", "org.apache.commons.lang") { 261 | overrideModuleName() 262 | } 263 | } 264 | """ 265 | 266 | expect: 267 | build() 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/ExportsFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import org.gradlex.javamodule.moduleinfo.test.fixture.LegacyLibraries 5 | import spock.lang.Specification 6 | 7 | class ExportsFunctionalTest extends Specification { 8 | 9 | @Delegate 10 | GradleBuild build = new GradleBuild() 11 | 12 | LegacyLibraries libs = new LegacyLibraries(false) 13 | 14 | def setup() { 15 | settingsFile << 'rootProject.name = "test-project"' 16 | buildFile << ''' 17 | plugins { 18 | id("application") 19 | id("org.gradlex.extra-java-module-info") 20 | } 21 | application { 22 | mainModule.set("org.gradle.sample.app") 23 | mainClass.set("org.gradle.sample.app.Main") 24 | } 25 | ''' 26 | file("src/main/java/module-info.java") << """ 27 | module org.gradle.sample.app { 28 | requires apache.commons.collections; 29 | } 30 | """ 31 | file("src/main/java/org/gradle/sample/app/Main.java") << """ 32 | package org.gradle.sample.app; 33 | 34 | public class Main { 35 | public static void main(String[] args) { 36 | new org.apache.commons.collections.bag.HashBag(); 37 | } 38 | } 39 | """ 40 | } 41 | 42 | def "a package can be exported globally"() { 43 | given: 44 | buildFile << """ 45 | dependencies { 46 | implementation("commons-collections:commons-collections:3.2.2") 47 | } 48 | extraJavaModuleInfo { 49 | module(${libs.commonsCollections}, "apache.commons.collections") { 50 | exports("org.apache.commons.collections.bag") 51 | } 52 | } 53 | """ 54 | 55 | expect: 56 | run() 57 | } 58 | 59 | def "a package can be exported to a specific module and only to this module"() { 60 | given: 61 | 62 | buildFile << """ 63 | dependencies { 64 | implementation("commons-collections:commons-collections:3.2.2") 65 | } 66 | extraJavaModuleInfo { 67 | module(${libs.commonsCollections}, "apache.commons.collections") { 68 | exports("org.apache.commons.collections.bag", "org.gradle.sample.lib") 69 | } 70 | } 71 | """ 72 | 73 | expect: 74 | def out = failRun() 75 | out.output.contains('package org.apache.commons.collections.bag is declared in module apache.commons.collections, which does not export it to module org.gradle.sample.app') 76 | } 77 | 78 | def "a package can be exported to a specific module"() { 79 | given: 80 | 81 | buildFile << """ 82 | dependencies { 83 | implementation("commons-collections:commons-collections:3.2.2") 84 | } 85 | extraJavaModuleInfo { 86 | module(${libs.commonsCollections}, "apache.commons.collections") { 87 | exports("org.apache.commons.collections.bag", "org.gradle.sample.app") 88 | } 89 | } 90 | """ 91 | 92 | expect: 93 | run() 94 | } 95 | 96 | def "a package can be exported to multiple modules"() { 97 | given: 98 | 99 | buildFile << """ 100 | dependencies { 101 | implementation("commons-collections:commons-collections:3.2.2") 102 | } 103 | extraJavaModuleInfo { 104 | module(${libs.commonsCollections}, "apache.commons.collections") { 105 | exports("org.apache.commons.collections.bag", "org.gradle.sample.lib", "org.gradle.sample.app") 106 | } 107 | } 108 | """ 109 | 110 | expect: 111 | run() 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/IdValidationFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import spock.lang.Specification 5 | 6 | class IdValidationFunctionalTest extends Specification { 7 | 8 | @Delegate 9 | GradleBuild build = new GradleBuild() 10 | 11 | def setup() { 12 | settingsFile << 'rootProject.name = "test-project"' 13 | buildFile << ''' 14 | plugins { 15 | id("application") 16 | id("org.gradlex.extra-java-module-info") 17 | } 18 | ''' 19 | } 20 | 21 | def "fails for wrong coordinates"() { 22 | given: 23 | buildFile << """ 24 | extraJavaModuleInfo { 25 | module("commons-logging:commons-logging:2.0", "apache.commons.logging") { 26 | exportAllPackages() 27 | } 28 | } 29 | """ 30 | 31 | expect: 32 | def out = fail() 33 | out.output.contains("'commons-logging:commons-logging:2.0' are not valid coordinates (group:name) / is not a valid file name (name-1.0.jar)") 34 | } 35 | 36 | def "fails for wrong file name"() { 37 | given: 38 | buildFile << """ 39 | extraJavaModuleInfo { 40 | module("/dummy/some/my.jar", "apache.commons.logging") 41 | } 42 | """ 43 | 44 | expect: 45 | def out = fail() 46 | out.output.contains("'/dummy/some/my.jar' are not valid coordinates (group:name) / is not a valid file name (name-1.0.jar)") 47 | } 48 | 49 | def "fails for wrong module name"() { 50 | given: 51 | buildFile << """ 52 | extraJavaModuleInfo { 53 | module("commons-logging:commons-logging", "apache.commons:logging") 54 | } 55 | """ 56 | 57 | expect: 58 | def out = fail() 59 | out.output.contains("apache.commons:logging: Invalid module name: 'commons:logging' is not a Java identifier") 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/IgnoreServiceProviderFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import spock.lang.Specification 5 | 6 | class IgnoreServiceProviderFunctionalTest extends Specification { 7 | 8 | @Delegate 9 | GradleBuild build = new GradleBuild() 10 | 11 | def "specific implementations can be ignored"() { 12 | settingsFile << 'rootProject.name = "test-project"' 13 | buildFile << ''' 14 | plugins { 15 | id("application") 16 | id("org.gradlex.extra-java-module-info") 17 | } 18 | 19 | application { 20 | mainModule.set("org.gradle.sample.app") 21 | mainClass.set("org.gradle.sample.app.Main") 22 | } 23 | 24 | repositories { 25 | mavenCentral() 26 | } 27 | 28 | dependencies { 29 | implementation("org.hsqldb:hsqldb:2.7.4") 30 | implementation("org.liquibase:liquibase-core:4.31.1") 31 | implementation("com.mattbertolini:liquibase-slf4j:5.1.0") 32 | implementation("org.slf4j:slf4j-simple:2.0.16") 33 | components { 34 | withModule("org.liquibase:liquibase-core") { 35 | allVariants { 36 | withDependencies { 37 | removeIf { it.name in setOf("opencsv", "jaxb-api", "commons-collections4", "commons-text") } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | extraJavaModuleInfo { 45 | failOnAutomaticModules.set(true) 46 | module("org.liquibase:liquibase-core", "liquibase.core") { 47 | closeModule() 48 | requiresTransitive("java.sql") 49 | requires("java.desktop") 50 | requires("java.logging") 51 | requires("java.naming") 52 | requires("java.xml") 53 | requires("org.apache.commons.io") 54 | requires("org.apache.commons.lang3") 55 | requires("org.yaml.snakeyaml") 56 | exports("liquibase") 57 | exports("liquibase.analytics") 58 | exports("liquibase.analytics.configuration") 59 | exports("liquibase.configuration") 60 | exports("liquibase.database.jvm") 61 | exports("liquibase.exception") 62 | exports("liquibase.logging") 63 | exports("liquibase.logging.core") 64 | exports("liquibase.resource") 65 | exports("liquibase.ui") 66 | 67 | opens("www.liquibase.org.xml.ns.dbchangelog") 68 | 69 | uses("liquibase.change.Change") 70 | uses("liquibase.changelog.ChangeLogHistoryService") 71 | uses("liquibase.changelog.visitor.ValidatingVisitorGenerator") 72 | uses("liquibase.changeset.ChangeSetService") 73 | uses("liquibase.command.CommandStep") 74 | uses("liquibase.command.LiquibaseCommand") 75 | uses("liquibase.configuration.AutoloadedConfigurations") 76 | uses("liquibase.configuration.ConfigurationValueProvider") 77 | uses("liquibase.configuration.ConfiguredValueModifier") 78 | uses("liquibase.database.Database") 79 | uses("liquibase.database.DatabaseConnection") 80 | uses("liquibase.database.LiquibaseTableNames") 81 | uses("liquibase.database.jvm.ConnectionPatterns") 82 | uses("liquibase.datatype.LiquibaseDataType") 83 | uses("liquibase.diff.DiffGenerator") 84 | uses("liquibase.diff.compare.DatabaseObjectComparator") 85 | uses("liquibase.diff.output.changelog.ChangeGenerator") 86 | uses("liquibase.executor.Executor") 87 | uses("liquibase.io.OutputFileHandler") 88 | uses("liquibase.lockservice.LockService") 89 | uses("liquibase.logging.LogService") 90 | uses("liquibase.logging.mdc.CustomMdcObject") 91 | uses("liquibase.logging.mdc.MdcManager") 92 | uses("liquibase.parser.ChangeLogParser") 93 | uses("liquibase.parser.LiquibaseSqlParser") 94 | uses("liquibase.parser.NamespaceDetails") 95 | uses("liquibase.parser.SnapshotParser") 96 | uses("liquibase.precondition.Precondition") 97 | uses("liquibase.report.ShowSummaryGenerator") 98 | uses("liquibase.resource.PathHandler") 99 | uses("liquibase.serializer.ChangeLogSerializer") 100 | uses("liquibase.serializer.SnapshotSerializer") 101 | uses("liquibase.servicelocator.ServiceLocator") 102 | uses("liquibase.snapshot.SnapshotGenerator") 103 | uses("liquibase.sqlgenerator.SqlGenerator") 104 | uses("liquibase.structure.DatabaseObject") 105 | ignoreServiceProvider("liquibase.change.Change", "liquibase.change.core.LoadDataChange", "liquibase.change.core.LoadUpdateDataChange") 106 | } 107 | } 108 | ''' 109 | file("src/main/java/module-info.java") << ''' 110 | @SuppressWarnings("opens") // the db package contains a resource file 111 | module org.gradle.sample.app { 112 | requires liquibase.core; 113 | requires org.hsqldb; 114 | //opens org.example.db to liquibase.core; -- this is too strict, the package needs to be "opened" globally so that liquibase's resource scan mechanism can detect resource files there 115 | opens org.gradle.sample.db; 116 | } 117 | ''' 118 | file("src/main/resources/simplelogger.properties") << ''' 119 | org.slf4j.simpleLogger.logFile=System.out 120 | org.slf4j.simpleLogger.cacheOutputStream=false 121 | '''.stripIndent() 122 | file("src/main/resources/org/gradle/sample/db/db.changelog-master.xml") << ''' 123 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | '''.stripIndent() 137 | file("src/main/java/org/gradle/sample/app/Main.java") << ''' 138 | package org.gradle.sample.app; 139 | 140 | import liquibase.Liquibase; 141 | import liquibase.Scope; 142 | import liquibase.UpdateSummaryEnum; 143 | import liquibase.analytics.configuration.AnalyticsArgs; 144 | import liquibase.database.jvm.JdbcConnection; 145 | import liquibase.resource.ClassLoaderResourceAccessor; 146 | import liquibase.ui.UIServiceEnum; 147 | import org.hsqldb.jdbc.JDBCDataSource; 148 | 149 | import java.sql.Connection; 150 | import java.util.Map; 151 | 152 | public class Main { 153 | public static void main(String[] args) throws Exception { 154 | JDBCDataSource ds = new JDBCDataSource(); 155 | ds.setURL("jdbc:hsqldb:mem:test"); 156 | try (Connection connection = ds.getConnection()) { 157 | Map attrs = Map.of( 158 | // use logging instead of printing directly to stdout 159 | Scope.Attr.ui.name(), UIServiceEnum.LOGGER.getUiServiceClass().getConstructor().newInstance(), 160 | // do not send analytics 161 | AnalyticsArgs.ENABLED.getKey(), false 162 | ); 163 | Scope.child(attrs, () -> { 164 | Liquibase liquibase = new Liquibase( 165 | "org/gradle/sample/db/db.changelog-master.xml", 166 | new ClassLoaderResourceAccessor(), 167 | new JdbcConnection(connection) 168 | ); 169 | 170 | liquibase.setShowSummary(UpdateSummaryEnum.OFF); 171 | liquibase.update(); 172 | }); 173 | } 174 | 175 | } 176 | } 177 | ''' 178 | expect: 179 | def out = run() 180 | out.output.contains('[main] INFO liquibase.lockservice.StandardLockService - Successfully acquired change log lock') 181 | out.output.contains('[main] INFO liquibase.ui.LoggerUIService - Liquibase: Update has been successful. Rows affected: 1') 182 | out.output.contains('[main] INFO liquibase.lockservice.StandardLockService - Successfully released change log lock') 183 | !out.output.contains("Caused by: java.lang.NoClassDefFoundError: com/opencsv/exceptions/CsvMalformedLineException") 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/LocalJarTransformFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import spock.lang.Specification 5 | 6 | class LocalJarTransformFunctionalTest extends Specification { 7 | 8 | @Delegate 9 | GradleBuild build = new GradleBuild() 10 | 11 | def setup() { 12 | settingsFile << ''' 13 | rootProject.name = "test-project" 14 | include(":sub") 15 | ''' 16 | file("sub/build.gradle.kts") << ''' 17 | plugins { 18 | id("java-library") 19 | id("org.gradlex.extra-java-module-info") 20 | id("maven-publish") 21 | } 22 | ''' 23 | buildFile << ''' 24 | plugins { 25 | id("java-library") 26 | id("org.gradlex.extra-java-module-info") 27 | } 28 | dependencies { 29 | implementation(project(":sub")) 30 | } 31 | ''' 32 | } 33 | 34 | def "a locally produced Jar is transformed"() { 35 | given: 36 | buildFile << ''' 37 | extraJavaModuleInfo { 38 | // transform local Jar to assert that it has gone through transformation 39 | module("sub.jar", "org.example.sub") 40 | } 41 | tasks.register("printCP") { 42 | inputs.files(configurations.runtimeClasspath) 43 | doLast { println(inputs.files.files.map { it.name }) } 44 | } 45 | ''' 46 | 47 | when: 48 | def result = task('printCP', '-q') 49 | 50 | then: 51 | result.output.trim() == "[sub-module.jar]" 52 | } 53 | 54 | def "transformation of locally produced Jars can be deactivates"() { 55 | given: 56 | buildFile << ''' 57 | tasks.register("printCP") { 58 | inputs.files(configurations.runtimeClasspath) 59 | doLast { println(inputs.files.files.map { it.name }) } 60 | } 61 | ''' 62 | file("sub/build.gradle.kts") << """ 63 | extraJavaModuleInfo { skipLocalJars.set(true) } 64 | """ 65 | 66 | when: 67 | def result = task('printCP', '-q') 68 | 69 | then: 70 | result.output.trim() == "[sub.jar]" 71 | } 72 | 73 | 74 | def "deactivation of locally produced Jars does not cause additional attributes to be published"() { 75 | given: 76 | def repo = file("repo") 77 | file("sub/build.gradle.kts") << """ 78 | group = "foo" 79 | version = "1" 80 | publishing { 81 | publications.create("lib").from(components["java"]) 82 | repositories.maven("${repo.absolutePath}") 83 | } 84 | extraJavaModuleInfo { skipLocalJars.set(true) } 85 | """ 86 | 87 | when: 88 | task('publish') 89 | 90 | then: 91 | !new File(repo, 'foo/sub/1/sub-1.module').text.contains('"javaModule":') 92 | } 93 | 94 | def "if transform fails due to missing local Jar, an actionable error message is given"() { 95 | given: 96 | buildFile << ''' 97 | tasks.register("printCP") { 98 | inputs.files(configurations.runtimeClasspath.get().files) // provoke error: access at configuration time 99 | doLast { println(inputs.files.files.map { it.name }) } 100 | } 101 | ''' 102 | 103 | when: 104 | def result = failTask('printCP', '-q') 105 | 106 | then: 107 | result.output.contains("File does not exist:") 108 | result.output.contains("You can prevent this error by setting 'skipLocalJars = true'") 109 | } 110 | 111 | def "resolving early does not fail if transformation is disabled for locally produced Jars"() { 112 | given: 113 | buildFile << ''' 114 | tasks.register("printCP") { 115 | inputs.files(configurations.runtimeClasspath.get().files) // provoke resolution at configuration time 116 | doLast { println(inputs.files.files.map { it.name }) } 117 | } 118 | ''' 119 | file("sub/build.gradle.kts") << ''' 120 | extraJavaModuleInfo { skipLocalJars.set(true) } 121 | ''' 122 | 123 | when: 124 | def result = task('printCP', '-q') 125 | 126 | then: 127 | result.output.trim() == "[sub.jar]" 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/OpensFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import org.gradlex.javamodule.moduleinfo.test.fixture.LegacyLibraries 5 | import spock.lang.Specification 6 | 7 | class OpensFunctionalTest extends Specification { 8 | 9 | @Delegate 10 | GradleBuild build = new GradleBuild() 11 | 12 | LegacyLibraries libs = new LegacyLibraries(false) 13 | 14 | def setup() { 15 | settingsFile << 'rootProject.name = "test-project"' 16 | buildFile << ''' 17 | plugins { 18 | id("application") 19 | id("org.gradlex.extra-java-module-info") 20 | } 21 | application { 22 | mainModule.set("org.gradle.sample.app") 23 | mainClass.set("org.gradle.sample.app.Main") 24 | } 25 | ''' 26 | file("src/main/java/module-info.java") << """ 27 | module org.gradle.sample.app { 28 | requires apache.commons.collections; 29 | } 30 | """ 31 | file("src/main/java/org/gradle/sample/app/Main.java") << """ 32 | package org.gradle.sample.app; 33 | 34 | public class Main { 35 | public static void main(String[] args) throws ClassNotFoundException { 36 | Class.forName("org.apache.commons.collections.buffer.BlockingBuffer").getDeclaredMethods()[0].setAccessible(true); 37 | Class.forName("org.apache.commons.collections.bag.HashBag").getDeclaredMethods()[0].setAccessible(true); 38 | } 39 | } 40 | """ 41 | } 42 | 43 | def "a module is open by default"() { 44 | given: 45 | buildFile << """ 46 | dependencies { 47 | implementation("commons-collections:commons-collections:3.2.2") 48 | } 49 | extraJavaModuleInfo { 50 | module(${libs.commonsCollections}, "apache.commons.collections") 51 | } 52 | """ 53 | 54 | expect: 55 | run() 56 | } 57 | 58 | def "a module can be closed"() { 59 | given: 60 | buildFile << """ 61 | dependencies { 62 | implementation("commons-collections:commons-collections:3.2.2") 63 | } 64 | extraJavaModuleInfo { 65 | module(${libs.commonsCollections}, "apache.commons.collections") { 66 | closeModule() 67 | } 68 | } 69 | """ 70 | 71 | expect: 72 | def out = failRun() 73 | out.output.contains('module apache.commons.collections does not "exports org.apache.commons.collections.buffer" to module org.gradle.sample.app') 74 | } 75 | 76 | def "a module is closed once it has an open package"() { 77 | given: 78 | 79 | buildFile << """ 80 | dependencies { 81 | implementation("commons-collections:commons-collections:3.2.2") 82 | } 83 | extraJavaModuleInfo { 84 | module(${libs.commonsCollections}, "apache.commons.collections") { 85 | opens("org.apache.commons.collections.buffer") 86 | } 87 | } 88 | """ 89 | 90 | expect: 91 | def out = failRun() 92 | out.output.contains('module apache.commons.collections does not "opens org.apache.commons.collections.bag" to module org.gradle.sample.app') 93 | } 94 | 95 | def "a package can be opened to a specific module"() { 96 | given: 97 | 98 | buildFile << """ 99 | dependencies { 100 | implementation("commons-collections:commons-collections:3.2.2") 101 | } 102 | extraJavaModuleInfo { 103 | module(${libs.commonsCollections}, "apache.commons.collections") { 104 | opens("org.apache.commons.collections.buffer", "org.gradle.sample.app") 105 | opens("org.apache.commons.collections.bag", "org.gradle.sample.app") 106 | } 107 | } 108 | """ 109 | 110 | expect: 111 | run() 112 | } 113 | 114 | def "a package can be opened to a specific module and only to this module"() { 115 | given: 116 | 117 | buildFile << """ 118 | dependencies { 119 | implementation("commons-collections:commons-collections:3.2.2") 120 | } 121 | extraJavaModuleInfo { 122 | module(${libs.commonsCollections}, "apache.commons.collections") { 123 | opens("org.apache.commons.collections.buffer", "org.gradle.sample.lib") 124 | opens("org.apache.commons.collections.bag", "org.gradle.sample.lib") 125 | } 126 | } 127 | """ 128 | 129 | expect: 130 | def out = failRun() 131 | out.output.contains('module apache.commons.collections does not "exports org.apache.commons.collections.buffer" to module org.gradle.sample.app') 132 | } 133 | 134 | def "a package can be opened to multiple modules"() { 135 | given: 136 | 137 | buildFile << """ 138 | dependencies { 139 | implementation("commons-collections:commons-collections:3.2.2") 140 | } 141 | extraJavaModuleInfo { 142 | module(${libs.commonsCollections}, "apache.commons.collections") { 143 | opens("org.apache.commons.collections.buffer", "org.gradle.sample.lib", "org.gradle.sample.app") 144 | opens("org.apache.commons.collections.bag", "org.gradle.sample.lib", "org.gradle.sample.app") 145 | } 146 | } 147 | """ 148 | 149 | expect: 150 | run() 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/PluginActivationFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import spock.lang.Specification 5 | 6 | class PluginActivationFunctionalTest extends Specification { 7 | 8 | @Delegate 9 | GradleBuild build = new GradleBuild() 10 | 11 | def setup() { 12 | settingsFile << 'rootProject.name = "test-project"' 13 | buildFile << ''' 14 | plugins { 15 | id("java-library") 16 | id("org.gradlex.extra-java-module-info") 17 | } 18 | val customPath = configurations.create("customPath") 19 | dependencies { 20 | implementation("commons-cli:commons-cli:1.4") 21 | annotationProcessor("commons-cli:commons-cli:1.4") 22 | customPath("commons-cli:commons-cli:1.4") 23 | } 24 | extraJavaModuleInfo { module("commons-cli:commons-cli", "org.apache.commons.cli") } 25 | tasks.register("printRuntimeCP") { 26 | inputs.files(configurations.runtimeClasspath) 27 | doLast { print("RCP: "); println(inputs.files.files.map { it.name }) } 28 | } 29 | tasks.register("printCompileCP") { 30 | inputs.files(configurations.compileClasspath) 31 | doLast { print("CCP: "); println(inputs.files.files.map { it.name }) } 32 | } 33 | tasks.register("printAP") { 34 | inputs.files(configurations.annotationProcessor) 35 | doLast { print("AP: "); println(inputs.files.files.map { it.name }) } 36 | } 37 | tasks.register("printCustom") { 38 | inputs.files(configurations["customPath"]) 39 | doLast { print("CUS: "); println(inputs.files.files.map { it.name }) } 40 | } 41 | ''' 42 | } 43 | 44 | def "plugin is a activated by default for all configurations of a source set"() { 45 | when: 46 | def result = task('printRuntimeCP', 'printCompileCP', 'printAP', '-q') 47 | 48 | then: 49 | result.output.contains('RCP: [commons-cli-1.4-module.jar]') 50 | result.output.contains('CCP: [commons-cli-1.4-module.jar]') 51 | result.output.contains('AP: [commons-cli-1.4-module.jar]') 52 | } 53 | 54 | def "plugin can be deactivated for a source set"() { 55 | given: 56 | buildFile << 'extraJavaModuleInfo { deactivate(sourceSets.main) }' 57 | 58 | when: 59 | def result = task('printRuntimeCP', 'printCompileCP', 'printAP', '-q') 60 | 61 | then: 62 | result.output.contains('RCP: [commons-cli-1.4.jar]') 63 | result.output.contains('CCP: [commons-cli-1.4.jar]') 64 | result.output.contains('AP: [commons-cli-1.4.jar]') 65 | } 66 | 67 | def "plugin can be deactivated and later re-activated for a source set"() { 68 | given: 69 | buildFile << 'extraJavaModuleInfo { deactivate(sourceSets.main.get()) }\n' 70 | buildFile << 'extraJavaModuleInfo { activate(sourceSets.main) }\n' 71 | 72 | when: 73 | def result = task('printRuntimeCP', 'printCompileCP', 'printAP', '-q') 74 | 75 | then: 76 | result.output.contains('RCP: [commons-cli-1.4-module.jar]') 77 | result.output.contains('CCP: [commons-cli-1.4-module.jar]') 78 | result.output.contains('AP: [commons-cli-1.4-module.jar]') 79 | } 80 | 81 | def "plugin can be deactivated for a single configuration"() { 82 | given: 83 | buildFile << 'extraJavaModuleInfo { deactivate(configurations.annotationProcessor) }' 84 | 85 | when: 86 | def result = task('printRuntimeCP', 'printCompileCP', 'printAP', '-q') 87 | 88 | then: 89 | result.output.contains('RCP: [commons-cli-1.4-module.jar]') 90 | result.output.contains('CCP: [commons-cli-1.4-module.jar]') 91 | result.output.contains('AP: [commons-cli-1.4.jar]') 92 | } 93 | 94 | def "plugin is not active for custom configurations by default"() { 95 | when: 96 | def result = task('printCustom', '-q') 97 | 98 | then: 99 | result.output.contains('CUS: [commons-cli-1.4.jar]') 100 | } 101 | 102 | def "plugin can be activated for a single custom configuration"() { 103 | given: 104 | buildFile << 'extraJavaModuleInfo { activate(customPath) }' 105 | 106 | when: 107 | def result = task('printCustom', '-q') 108 | 109 | then: 110 | result.output.contains('CUS: [commons-cli-1.4-module.jar]') 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/RealModuleJarPatchingFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import spock.lang.Specification 5 | 6 | class RealModuleJarPatchingFunctionalTest extends Specification { 7 | 8 | @Delegate 9 | GradleBuild build = new GradleBuild() 10 | 11 | def setup() { 12 | settingsFile << 'rootProject.name = "test-project"' 13 | buildFile << ''' 14 | plugins { 15 | id("java") 16 | id("org.gradlex.extra-java-module-info") 17 | } 18 | 19 | tasks.register("run") { 20 | mainClass.set("jdk.tools.jlink.internal.Main") 21 | mainModule.set("jdk.jlink") 22 | args = listOf( 23 | "--module-path", 24 | configurations.runtimeClasspath.get().asPath, 25 | "--output", 26 | "jlink-image-test", 27 | "--add-modules", 28 | "org.apache.tomcat.embed.core" 29 | ) 30 | } 31 | 32 | ''' 33 | } 34 | 35 | def "jlink fails because of a broken module descriptor"() { 36 | given: 37 | buildFile << ''' 38 | dependencies { 39 | implementation("org.apache.tomcat.embed:tomcat-embed-core:10.1.13") 40 | } 41 | 42 | extraJavaModuleInfo { 43 | 44 | } 45 | ''' 46 | 47 | expect: 48 | def out = failRun() 49 | out.output.contains("Packages that are exported or open in org.apache.tomcat.embed.core are not present: [org.apache.catalina.ssi]") 50 | } 51 | 52 | def "jlink succeeds with the patched module descriptor"() { 53 | given: 54 | buildFile << ''' 55 | dependencies { 56 | implementation("org.apache.tomcat.embed:tomcat-embed-core:10.1.13") 57 | } 58 | 59 | extraJavaModuleInfo { 60 | module("org.apache.tomcat.embed:tomcat-embed-core", "org.apache.tomcat.embed.core") { 61 | patchRealModule() 62 | requires("java.desktop") 63 | requires("java.instrument") 64 | requires("java.logging") 65 | requires("java.rmi") 66 | 67 | requiresStatic("jakarta.ejb") 68 | requiresStatic("jakarta.mail") 69 | requiresStatic("jakarta.persistence") 70 | requiresStatic("jakarta.xml.ws") 71 | requiresStatic("java.xml.ws") 72 | 73 | requiresTransitive("jakarta.annotation") 74 | requiresTransitive("java.management") 75 | requiresTransitive("java.naming") 76 | requiresTransitive("java.security.jgss") 77 | requiresTransitive("java.sql") 78 | requiresTransitive("java.xml") 79 | 80 | exports("jakarta.security.auth.message") 81 | exports("jakarta.security.auth.message.callback") 82 | exports("jakarta.security.auth.message.config") 83 | exports("jakarta.security.auth.message.module") 84 | exports("jakarta.servlet") 85 | exports("jakarta.servlet.annotation") 86 | exports("jakarta.servlet.descriptor") 87 | exports("jakarta.servlet.http") 88 | exports("jakarta.servlet.resources") 89 | exports("org.apache.catalina") 90 | exports("org.apache.catalina.authenticator") 91 | exports("org.apache.catalina.authenticator.jaspic") 92 | exports("org.apache.catalina.connector") 93 | exports("org.apache.catalina.core") 94 | exports("org.apache.catalina.deploy") 95 | exports("org.apache.catalina.filters") 96 | exports("org.apache.catalina.loader") 97 | exports("org.apache.catalina.manager") 98 | exports("org.apache.catalina.manager.host") 99 | exports("org.apache.catalina.manager.util") 100 | exports("org.apache.catalina.mapper") 101 | exports("org.apache.catalina.mbeans") 102 | exports("org.apache.catalina.realm") 103 | exports("org.apache.catalina.security") 104 | exports("org.apache.catalina.servlets") 105 | exports("org.apache.catalina.session") 106 | // The only difference with the bundled module-info.class 107 | // exports("org.apache.catalina.ssi") 108 | exports("org.apache.catalina.startup") 109 | exports("org.apache.catalina.users") 110 | exports("org.apache.catalina.util") 111 | exports("org.apache.catalina.valves") 112 | exports("org.apache.catalina.valves.rewrite") 113 | exports("org.apache.catalina.webresources") 114 | exports("org.apache.catalina.webresources.war") 115 | exports("org.apache.coyote") 116 | exports("org.apache.coyote.ajp") 117 | exports("org.apache.coyote.http11") 118 | exports("org.apache.coyote.http11.filters") 119 | exports("org.apache.coyote.http11.upgrade") 120 | exports("org.apache.coyote.http2") 121 | exports("org.apache.juli") 122 | exports("org.apache.juli.logging") 123 | exports("org.apache.naming") 124 | exports("org.apache.naming.factory") 125 | exports("org.apache.naming.java") 126 | exports("org.apache.tomcat") 127 | exports("org.apache.tomcat.jni") 128 | exports("org.apache.tomcat.util") 129 | exports("org.apache.tomcat.util.bcel.classfile") 130 | exports("org.apache.tomcat.util.buf") 131 | exports("org.apache.tomcat.util.codec.binary") 132 | exports("org.apache.tomcat.util.collections") 133 | exports("org.apache.tomcat.util.compat") 134 | exports("org.apache.tomcat.util.descriptor") 135 | exports("org.apache.tomcat.util.descriptor.tagplugin") 136 | exports("org.apache.tomcat.util.descriptor.web") 137 | exports("org.apache.tomcat.util.digester") 138 | exports("org.apache.tomcat.util.file") 139 | exports("org.apache.tomcat.util.http") 140 | exports("org.apache.tomcat.util.http.fileupload") 141 | exports("org.apache.tomcat.util.http.fileupload.disk") 142 | exports("org.apache.tomcat.util.http.fileupload.impl") 143 | exports("org.apache.tomcat.util.http.fileupload.servlet") 144 | exports("org.apache.tomcat.util.http.fileupload.util") 145 | exports("org.apache.tomcat.util.http.parser") 146 | exports("org.apache.tomcat.util.log") 147 | exports("org.apache.tomcat.util.modeler") 148 | exports("org.apache.tomcat.util.modeler.modules") 149 | exports("org.apache.tomcat.util.net") 150 | exports("org.apache.tomcat.util.net.openssl") 151 | exports("org.apache.tomcat.util.net.openssl.ciphers") 152 | exports("org.apache.tomcat.util.res") 153 | exports("org.apache.tomcat.util.scan") 154 | exports("org.apache.tomcat.util.security") 155 | exports("org.apache.tomcat.util.threads") 156 | 157 | uses("org.apache.juli.logging.Log") 158 | } 159 | } 160 | ''' 161 | 162 | expect: 163 | run() 164 | } 165 | 166 | def "patching of real modules must be explicitly enabled"() { 167 | given: 168 | buildFile << ''' 169 | dependencies { 170 | implementation("org.apache.tomcat.embed:tomcat-embed-core:10.1.13") 171 | } 172 | 173 | extraJavaModuleInfo { 174 | module("org.apache.tomcat.embed:tomcat-embed-core", "org.apache.tomcat.embed.core") { 175 | requires("java.desktop") 176 | } 177 | } 178 | ''' 179 | 180 | expect: 181 | def out = failRun() 182 | out.output.contains("Patching of real modules must be explicitly enabled with 'patchRealModule()'") 183 | } 184 | 185 | def "a real module cannot be demoted to an automatic module"() { 186 | given: 187 | buildFile << ''' 188 | dependencies { 189 | implementation("org.apache.tomcat.embed:tomcat-embed-core:10.1.13") 190 | } 191 | 192 | extraJavaModuleInfo { 193 | automaticModule("org.apache.tomcat.embed:tomcat-embed-core", "org.apache.tomcat.embed.core") 194 | } 195 | ''' 196 | 197 | expect: 198 | def out = failRun() 199 | out.output.contains("Patching of real modules must be explicitly enabled with 'patchRealModule()' and can only be done with 'module()'") 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/RealModuleJarPreservePatchingFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import spock.lang.IgnoreIf 5 | import spock.lang.Specification 6 | 7 | class RealModuleJarPreservePatchingFunctionalTest extends Specification { 8 | 9 | @Delegate 10 | GradleBuild build = new GradleBuild() 11 | 12 | def setup() { 13 | settingsFile << 'rootProject.name = "test-project"' 14 | buildFile << ''' 15 | plugins { 16 | id("application") 17 | id("org.gradlex.extra-java-module-info") 18 | } 19 | application { 20 | mainModule.set("org.example") 21 | mainClass.set("org.example.Main") 22 | } 23 | ''' 24 | } 25 | 26 | @IgnoreIf({ GradleBuild.gradleVersionUnderTest?.matches("[67]\\..*") }) // requires Gradle to support Java 17 27 | def "a real module cannot be extended via preserveExisting"() { 28 | given: 29 | buildFile << ''' 30 | tasks.withType().configureEach { 31 | options.compilerArgs.add("-Xlint:all") 32 | options.compilerArgs.add("-Werror") 33 | } 34 | dependencies { 35 | implementation("org.apache.logging.log4j:log4j-api:2.24.3") 36 | 37 | // required because not declared in LOG4J metadata 38 | compileOnly("com.google.errorprone:error_prone_annotations:2.36.0") 39 | compileOnly("com.github.spotbugs:spotbugs-annotations:4.9.0") 40 | compileOnly("biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0") 41 | compileOnly("org.osgi:osgi.annotation:8.1.0") // this includes 'org.osgi.annotation.bundle' 42 | } 43 | extraJavaModuleInfo { 44 | failOnMissingModuleInfo.set(false) // transitive dependencies of annotation libs 45 | 46 | module("org.apache.logging.log4j:log4j-api", "org.apache.logging.log4j") { 47 | preserveExisting() 48 | requiresStatic("com.google.errorprone.annotations") 49 | requiresStatic("com.github.spotbugs.annotations") 50 | requiresStatic("biz.aQute.bnd.annotation") 51 | requiresStatic("org.osgi.annotation") 52 | } 53 | module("biz.aQute.bnd:biz.aQute.bnd.annotation", "biz.aQute.bnd.annotation") { 54 | requiresStatic("org.osgi.annotation") 55 | exportAllPackages() 56 | } 57 | module("org.osgi:osgi.annotation", "org.osgi.annotation") 58 | } 59 | ''' 60 | file("src/main/java/module-info.java") << """ 61 | module org.example { 62 | requires org.apache.logging.log4j; 63 | } 64 | """ 65 | file("src/main/java/org/example/Main.java") << """ 66 | package org.example; 67 | public class Main { 68 | org.apache.logging.log4j.message.ParameterizedMessage m; // needs errorprone 69 | org.apache.logging.log4j.status.StatusData d; // needs spotbugs 70 | org.apache.logging.log4j.util.SystemPropertiesPropertySource s; // needs aQute.bnd 71 | org.apache.logging.log4j.util.Activator a; // needs osgi 72 | 73 | public static void main(String[] args) {} 74 | } 75 | """ 76 | 77 | expect: 78 | run() 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/RemovePackageFunctionalTest.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test 2 | 3 | import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild 4 | import org.gradlex.javamodule.moduleinfo.test.fixture.LegacyLibraries 5 | import spock.lang.Specification 6 | 7 | class RemovePackageFunctionalTest extends Specification { 8 | 9 | @Delegate 10 | GradleBuild build = new GradleBuild() 11 | 12 | LegacyLibraries libs = new LegacyLibraries(false) 13 | 14 | def setup() { 15 | settingsFile << 'rootProject.name = "test-project"' 16 | buildFile << ''' 17 | plugins { 18 | id("application") 19 | id("org.gradlex.extra-java-module-info") 20 | } 21 | application { 22 | mainModule.set("org.example.app") 23 | mainClass.set("org.example.app.Main") 24 | } 25 | ''' 26 | file("src/main/java/module-info.java") << """ 27 | module org.example.app { 28 | requires jdk.xml.dom; 29 | requires xerces; 30 | } 31 | """ 32 | file("src/main/java/org/gradle/sample/app/Main.java") << """ 33 | package org.example.app; 34 | public class Main { 35 | public static void main(String[] args) { 36 | org.apache.xerces.util.DOMUtil util; 37 | } 38 | } 39 | """ 40 | } 41 | 42 | def "can remove duplicated packages"() { 43 | given: 44 | buildFile << """ 45 | dependencies { 46 | implementation("xerces:xercesImpl:2.12.2") { isTransitive = false } 47 | } 48 | extraJavaModuleInfo { 49 | module("xerces:xercesImpl", "xerces") { 50 | removePackage("org.w3c.dom.html") 51 | exportAllPackages() 52 | requires("java.xml") 53 | } 54 | } 55 | """ 56 | 57 | expect: 58 | run() 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/fixture/GradleBuild.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test.fixture 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.GradleRunner 5 | 6 | import java.lang.management.ManagementFactory 7 | import java.nio.file.Files 8 | 9 | class GradleBuild { 10 | 11 | final File projectDir 12 | final File buildFile 13 | final File settingsFile 14 | 15 | final static String gradleVersionUnderTest = System.getProperty("gradleVersionUnderTest") 16 | 17 | GradleBuild(File projectDir = Files.createTempDirectory("gradle-build").toFile()) { 18 | this.projectDir = projectDir 19 | this.buildFile = new File(projectDir, "build.gradle.kts") 20 | this.settingsFile = new File(projectDir, "settings.gradle.kts") 21 | } 22 | 23 | File file(String path) { 24 | new File(projectDir, path).tap { 25 | it.getParentFile().mkdirs() 26 | } 27 | } 28 | 29 | BuildResult build() { 30 | runner('build').build() 31 | } 32 | 33 | BuildResult run() { 34 | runner('run').build() 35 | } 36 | 37 | BuildResult failRun() { 38 | runner('run').buildAndFail() 39 | } 40 | 41 | BuildResult test() { 42 | runner('test').build() 43 | } 44 | 45 | BuildResult fail() { 46 | runner('build').buildAndFail() 47 | } 48 | 49 | BuildResult task(String... taskNames) { 50 | runner(taskNames).build() 51 | } 52 | 53 | BuildResult failTask(String... taskNames) { 54 | runner(taskNames).buildAndFail() 55 | } 56 | 57 | GradleRunner runner(String... args) { 58 | if (buildFile.exists()) { 59 | buildFile << '\nrepositories.mavenCentral()' 60 | if (gradleVersionUnderTest && gradleVersionUnderTest.startsWith("6.")) { 61 | buildFile << '\njava.modularity.inferModulePath.set(true)' 62 | } 63 | } 64 | List latestFeaturesArgs = gradleVersionUnderTest ? [] : [ 65 | '--configuration-cache', 66 | '-Dorg.gradle.unsafe.isolated-projects=true', 67 | ] 68 | GradleRunner.create() 69 | .forwardOutput() 70 | .withPluginClasspath() 71 | .withProjectDir(projectDir) 72 | .withArguments(Arrays.asList(args) + latestFeaturesArgs + '-s') 73 | .withDebug(ManagementFactory.getRuntimeMXBean().getInputArguments().toString().contains("-agentlib:jdwp")).with { 74 | gradleVersionUnderTest ? it.withGradleVersion(gradleVersionUnderTest) : it 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/groovy/org/gradlex/javamodule/moduleinfo/test/fixture/LegacyLibraries.groovy: -------------------------------------------------------------------------------- 1 | package org.gradlex.javamodule.moduleinfo.test.fixture 2 | 3 | class LegacyLibraries { 4 | 5 | final boolean jarNameOnly 6 | final boolean aliases 7 | 8 | LegacyLibraries(boolean jarNameOnly = false, boolean aliases = false) { 9 | this.jarNameOnly = jarNameOnly 10 | this.aliases = aliases 11 | } 12 | 13 | def closureCompiler = jarNameOnly ? '"closure-compiler-v20211201.jar"' : aliases ? 'libs.closure.compiler' : '"com.google.javascript:closure-compiler"' 14 | def commonsBeanutils = jarNameOnly ? '"commons-beanutils-1.9.4.jar"' : aliases ? 'libs.commons.beanutils' : '"commons-beanutils:commons-beanutils"' 15 | def commonsCli = jarNameOnly ? '"commons-cli-1.4.jar"' : aliases ? 'libs.commons.cli' : '"commons-cli:commons-cli"' 16 | def commonsCollections = jarNameOnly ? '"commons-collections-3.2.2.jar"' : aliases ? 'libs.commons.collections' : '"commons-collections:commons-collections"' 17 | def commonsHttpClient = jarNameOnly ? '"httpclient-4.5.14.jar"' : aliases ? 'libs.httpclient' : '"org.apache.httpcomponents:httpclient"' 18 | def commonsLogging = jarNameOnly ? '"commons-logging-1.2.jar"' : aliases ? 'libs.commons.logging' : '"commons-logging:commons-logging"' 19 | def groovyAll = jarNameOnly ? '"groovy-all-2.4.15.jar"' : aliases ? 'libs.groovy.all' : '"org.codehaus.groovy:groovy-all"' 20 | def javaxInject = jarNameOnly ? '"javax.inject-1.jar"' : aliases ? 'libs.javax.inject' : '"javax.inject:javax.inject"' 21 | def jsr305 = jarNameOnly ? '"jsr305-3.0.2.jar"' : aliases ? 'libs.jsr305' : '"com.google.code.findbugs:jsr305"' 22 | def log4jCore = jarNameOnly ? '"log4j-core-2.14.0.jar"' : aliases ? 'libs.log4j.core' : '"org.apache.logging.log4j:log4j-core"' 23 | def qpidJmsClient = jarNameOnly ? '"qpid-jms-client-2.2.0.jar"' : aliases ? 'libs.qpid.jms.client' : '"org.apache.qpid:qpid-jms-client"' 24 | def qpidJmsDiscovery = jarNameOnly ? '"qpid-jms-discovery-2.2.0.jar"' : aliases ? 'libs.qpid.jms.discovery' : '"org.apache.qpid:qpid-jms-discovery"' 25 | def sac = jarNameOnly ? '"sac-1.3.jar"' : aliases ? 'libs.sac' : '"org.w3c.css:sac"' 26 | def slf4jApi = jarNameOnly ? '"slf4j-api-1.7.32.jar"' : aliases ? 'libs.slf4j.api' : '"org.slf4j:slf4j-api"' 27 | def slf4jExt = jarNameOnly ? '"slf4j-ext-1.7.32.jar"' : aliases ? 'libs.slf4j.ext' : '"org.slf4j:slf4j-ext"' 28 | def springBootAutoconfigure = jarNameOnly ? '"spring-boot-autoconfigure-2.4.2.jar"' : aliases ? 'libs.spring.boot.autoconfigure' : '"org.springframework.boot:spring-boot-autoconfigure"' 29 | def zookeeper = jarNameOnly ? '"zookeeper-3.8.0.jar"' : aliases ? 'libs.zookeeper.core' : '"org.apache.zookeeper:zookeeper"' 30 | def zookeeperJute = jarNameOnly ? '"zookeeper-jute-3.8.0.jar"' : aliases ? 'libs.zookeeper.jute' : '"org.apache.zookeeper:zookeeper-jute"' 31 | 32 | static String catalog() { 33 | LegacyLibraries aliases = new LegacyLibraries(false, true) 34 | LegacyLibraries coordinates = new LegacyLibraries(false, false) 35 | 36 | """ 37 | [libraries] 38 | ${alias(aliases.closureCompiler)} = '${unquote(coordinates.closureCompiler)}:1' 39 | ${alias(aliases.commonsBeanutils)} = '${unquote(coordinates.commonsBeanutils)}:1' 40 | ${alias(aliases.commonsCli)} = '${unquote(coordinates.commonsCli)}:1' 41 | ${alias(aliases.commonsCollections)} = '${unquote(coordinates.commonsCollections)}:1' 42 | ${alias(aliases.commonsHttpClient)} = '${unquote(coordinates.commonsHttpClient)}:1' 43 | ${alias(aliases.commonsLogging)} = '${unquote(coordinates.commonsLogging)}:1' 44 | ${alias(aliases.groovyAll)} = '${unquote(coordinates.groovyAll)}:1' 45 | ${alias(aliases.javaxInject)} = '${unquote(coordinates.javaxInject)}:1' 46 | ${alias(aliases.jsr305)} = '${unquote(coordinates.jsr305)}:1' 47 | ${alias(aliases.log4jCore)} = '${unquote(coordinates.log4jCore)}:1' 48 | ${alias(aliases.qpidJmsClient)} = '${unquote(coordinates.qpidJmsClient)}:1' 49 | ${alias(aliases.qpidJmsDiscovery)} = '${unquote(coordinates.qpidJmsDiscovery)}:1' 50 | ${alias(aliases.sac)} = '${unquote(coordinates.sac)}:1' 51 | ${alias(aliases.slf4jApi)} = '${unquote(coordinates.slf4jApi)}:1' 52 | ${alias(aliases.slf4jExt)} = '${unquote(coordinates.slf4jExt)}:1' 53 | ${alias(aliases.springBootAutoconfigure)} = '${unquote(coordinates.springBootAutoconfigure)}:1' 54 | ${alias(aliases.zookeeper)} = '${unquote(coordinates.zookeeper)}:1' 55 | ${alias(aliases.zookeeperJute)} = '${unquote(coordinates.zookeeperJute)}:1' 56 | """.stripIndent() 57 | } 58 | 59 | private static String alias(String alias) { 60 | alias.substring('libs.'.length()).replace('.', '-') 61 | } 62 | 63 | private static String unquote(String coordinates) { 64 | coordinates.replace('"', '') 65 | } 66 | } --------------------------------------------------------------------------------