├── .gitattributes ├── .github └── workflows │ ├── changelog-print.yml │ ├── ci.yml │ ├── deploy.yml │ └── gradle-wrapper-validation.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── logo-square.png ├── logo.license ├── logo.png ├── logo.svg ├── settings.gradle └── src ├── main └── java │ └── com │ └── diffplug │ └── blowdryer │ ├── Blowdryer.java │ ├── BlowdryerPlugin.java │ ├── BlowdryerPluginConfigAvoidance.java │ ├── BlowdryerPluginLegacy.java │ ├── BlowdryerSetup.java │ ├── BlowdryerSetupPlugin.java │ ├── PluginsBlockParsed.java │ ├── RateLimitInterceptor.java │ └── 干.java └── test ├── java └── com │ └── diffplug │ └── blowdryer │ ├── BlowdryerPluginAuthTest.java │ ├── BlowdryerPluginConfigurationCacheTest.java │ ├── BlowdryerPluginTest.java │ ├── BlowdryerRetryTest.java │ ├── BlowdryerTest.java │ ├── GradleHarness.java │ ├── PluginsBlockParsedTest.java │ └── ResourceHarness.java └── resources └── com └── diffplug └── blowdryer └── test.jar /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.jar binary 3 | *.png binary 4 | -------------------------------------------------------------------------------- /.github/workflows/changelog-print.yml: -------------------------------------------------------------------------------- 1 | name: changelogPrint 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | name: changelogPrint 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: jdk 11 14 | uses: actions/setup-java@v3 15 | with: 16 | java-version: 11 17 | distribution: 'temurin' 18 | - name: gradle caching 19 | uses: gradle/gradle-build-action@v2 20 | with: 21 | gradle-home-cache-cleanup: true 22 | - run: ./gradlew changelogPrint 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | push: 4 | branches: [main] 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | jobs: 9 | build: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | jre: [11] 14 | os: [ubuntu-latest, windows-latest] 15 | include: 16 | - jre: 17 17 | os: ubuntu-latest 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | - name: Install JDK ${{ matrix.jre }} 23 | uses: actions/setup-java@v3 24 | with: 25 | distribution: "temurin" 26 | java-version: ${{ matrix.jre }} 27 | - name: gradle caching 28 | uses: gradle/gradle-build-action@v2 29 | with: 30 | gradle-home-cache-cleanup: true 31 | - name: git fetch origin main 32 | run: git fetch origin main 33 | - name: gradlew build 34 | run: ./gradlew build 35 | - name: junit result 36 | uses: mikepenz/action-junit-report@v3 37 | if: always() # always run even if the previous step fails 38 | with: 39 | check_name: JUnit ${{ matrix.jre }} ${{ matrix.os }} 40 | report_paths: '*/build/test-results/*/TEST-*.xml' 41 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # GH_TOKEN 2 | # NEXUS_USER 3 | # NEXUS_PASS64 (base64 NOTE: `base64` and `openssl base64` failed, had to use Java 4 | # byte[] data = "{{password}}".getBytes(StandardCharsets.UTF_8); 5 | # String encoded = new String(Base64.getEncoder().encode(data), StandardCharsets.UTF_8); 6 | # System.out.println(encoded); 7 | # GPG_PASSPHRASE 8 | # GPG_KEY64 (base64) 9 | # gpg --export-secret-keys --armor KEY_ID | openssl base64 | pbcopy 10 | # GRADLE_KEY 11 | # GRADLE_SECRET 12 | 13 | name: deploy 14 | on: 15 | workflow_dispatch: 16 | inputs: 17 | to_publish: 18 | description: 'What to publish' 19 | required: true 20 | default: 'all' 21 | type: choice 22 | options: 23 | - all 24 | 25 | jobs: 26 | build: 27 | runs-on: ubuntu-latest 28 | name: deploy 29 | env: 30 | gh_token: ${{ secrets.GH_TOKEN }} 31 | ORG_GRADLE_PROJECT_nexus_user: ${{ secrets.NEXUS_USER }} 32 | ORG_GRADLE_PROJECT_nexus_pass64: ${{ secrets.NEXUS_PASS64 }} 33 | ORG_GRADLE_PROJECT_gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} 34 | ORG_GRADLE_PROJECT_gpg_key64: ${{ secrets.GPG_KEY64 }} 35 | steps: 36 | - uses: actions/checkout@v3 37 | - name: jdk 11 38 | uses: actions/setup-java@v3 39 | with: 40 | java-version: 11 41 | distribution: 'temurin' 42 | - name: gradle caching 43 | uses: gradle/gradle-build-action@v2 44 | with: 45 | gradle-home-cache-cleanup: true 46 | - name: git fetch origin main 47 | run: git fetch origin main 48 | - name: publish all 49 | if: "${{ github.event.inputs.to_publish == 'all' }}" 50 | run: | 51 | ./gradlew :changelogPush -Prelease=true -Penable_publishing=true -Pgradle.publish.key=${{ secrets.GRADLE_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_SECRET }} --stacktrace --warning-mode all 52 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.yml: -------------------------------------------------------------------------------- 1 | name: "Validate Gradle Wrapper" 2 | on: 3 | push: 4 | paths: 5 | - 'gradlew' 6 | - 'gradlew.bat' 7 | - 'gradle/wrapper/' 8 | pull_request: 9 | paths: 10 | - 'gradlew' 11 | - 'gradlew.bat' 12 | - 'gradle/wrapper/' 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | validation: 19 | name: "Validation" 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: gradle/wrapper-validation-action@v1 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # mac stuff 3 | *.DS_Store 4 | 5 | # gradle stuff 6 | .gradle/ 7 | build/ 8 | 9 | # Eclipse stuff 10 | .project 11 | .classpath 12 | .settings/ 13 | bin/ 14 | 15 | # IntelliJ 16 | .idea/ 17 | out/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.7.1] - 2023-12-08 10 | ### Fixed 11 | - Remove `Provider.forUseAtConfigurationTime method has been deprecated` warnings in recent versions of Gradle. ([#38](https://github.com/diffplug/blowdryer/pull/38)) 12 | 13 | ## [1.7.0] - 2023-01-28 14 | ### Added 15 | - New `BlowdryerSetup.setPluginsBlockTo` for setting plugin versions ([docs](https://github.com/diffplug/blowdryer#plugin-versions)). ([#32](https://github.com/diffplug/blowdryer/pull/32) implements [#10](https://github.com/diffplug/blowdryer/issues/10)) 16 | ### Fixed 17 | - Fix `BlowdryerSetup.localJar` on Windows. ([#31](https://github.com/diffplug/blowdryer/pull/31)) 18 | 19 | ## [1.6.0] - 2022-05-20 20 | ### Added 21 | - Add retry on (gitlab) rateLimit ([#30](https://github.com/diffplug/blowdryer/pull/30)) 22 | 23 | ## [1.5.1] - 2022-01-31 24 | ### Fixed 25 | - Fix `StackOverflowError` on `干.projOptional` 26 | 27 | ## [1.5.0] - 2022-01-31 28 | ### Added 29 | - `干.projOptional('JRE_TARGET', 'Sets the target JRE')` which returns null if it's not present. ([#28](https://github.com/diffplug/blowdryer/pull/28)). 30 | 31 | ## [1.4.1] - 2021-09-03 32 | ### Fixed 33 | - No longer breaks the Gradle configuration cache ([#26](https://github.com/diffplug/blowdryer/pull/26)). 34 | 35 | ## [1.4.0] - 2021-06-23 36 | ### Added 37 | - `干.file('blah.foo')` now preserves `.foo` extension in the returned file ([#23](https://github.com/diffplug/blowdryer/pull/23)). 38 | - also, `干.immutableUrl(String url)` can take an optional second argument for specifying the file extension, e.g. 39 | - `干.immutableUrl('https://foo.org/?file=blah.foo&rev=7')` returns a file which ends in `.foo-rev-7` 40 | - `干.immutableUrl('https://foo.org/?file=blah.foo&rev=7', '.foo')` returns a file which ends in `.foo` 41 | 42 | ## [1.3.0] - 2021-06-23 43 | ### Added 44 | - Support for Bitbucket Cloud and Server ([#23](https://github.com/diffplug/blowdryer/pull/23)). 45 | 46 | ## [1.2.1] - 2021-06-01 47 | ### Fixed 48 | - `repoSubfolder` doesn't do anything in `localJar` mode, so setting `repoSubfolder` ought to be an error, and now it is ([#22](https://github.com/diffplug/blowdryer/pull/22)). 49 | 50 | ## [1.2.0] - 2021-05-30 51 | ### Added 52 | - Support for local JAR file ([#20](https://github.com/diffplug/blowdryer/pull/20)). 53 | 54 | ## [1.1.1] - 2021-02-12 55 | ### Fixed 56 | - Occasionally a file would be deleted from temp storage while a long-lived gradle daemon kept it in cache ([#11](https://github.com/diffplug/blowdryer/pull/18)). 57 | 58 | ## [1.1.0] - 2021-02-12 59 | ### Added 60 | - Support for GitLab, self-hosted and `gitlab.com` ([#18](https://github.com/diffplug/blowdryer/pull/18)). 61 | - Support for private GitHub and GitLab script repositories via auth tokens ([#18](https://github.com/diffplug/blowdryer/pull/18)). 62 | 63 | ## [1.0.0] - 2020-01-09 64 | Same as `0.2.0`, just committing to API back-compat from here. 65 | 66 | ## [0.2.0] - 2020-01-09 67 | ### Added 68 | - Support for Gradle pre-4.9 (task configuration avoidance). 69 | 70 | ## [0.1.1] - 2019-12-10 71 | ### Fixed 72 | - Minor javadoc improvements. 73 | 74 | ## [0.1.0] - 2019-12-08 75 | First release! 76 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at community@diffplug.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Blowdryer 2 | 3 | Pull requests are welcome! In order to get merged, you'll need to: 4 | 5 | - [ ] have tests 6 | - [ ] update the changelog 7 | 8 | ## Build instructions 9 | 10 | It's a bog-standard gradle build. 11 | 12 | - `gradlew eclipse` creates an Eclipse project file for you. 13 | - `gradlew build` builds the jar and runs the tests 14 | 15 | If you're getting style warnings, `gradlew spotlessApply` will apply anything necessary to fix formatting. For more info on the formatter, check out [spotless](https://github.com/diffplug/spotless). 16 | 17 | ## Test locally 18 | 19 | To make changes to Blowdryer and test those changes on a local project, add the following to the top of your local project's `build.gradle` (the project you want to use Blowdryer on, not Blowdryer itself): 20 | 21 | ```groovy 22 | buildscript { 23 | repositories { 24 | mavenLocal() 25 | jcenter() 26 | configurations.all { 27 | resolutionStrategy.cacheChangingModulesFor 0, 'seconds' 28 | } 29 | } 30 | 31 | dependencies { 32 | classpath 'com.diffplug:blowdryer:+' 33 | } 34 | } 35 | ``` 36 | 37 | To test your changes, run `gradlew publishToMavenLocal` on your Blowdryer project. Now you can make changes and test them on your project. 38 | 39 | ## Use unreleased versions from JitPack 40 | 41 | We publish tagged releases to mavenCentral, jcenter, and the gradle plugin portal. But you can also grab any intermediate release using [JitPack](https://jitpack.io/#com.diffplug/blowdryer). 42 | 43 | ## License 44 | 45 | By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/diffplug/blowdryer/blob/master/LICENSE 46 | 47 | All files are released with the Apache 2.0 license as such: 48 | 49 | ``` 50 | Copyright 2019 DiffPlug 51 | 52 | Licensed under the Apache License, Version 2.0 (the "License"); 53 | you may not use this file except in compliance with the License. 54 | You may obtain a copy of the License at 55 | 56 | http://www.apache.org/licenses/LICENSE-2.0 57 | 58 | Unless required by applicable law or agreed to in writing, software 59 | distributed under the License is distributed on an "AS IS" BASIS, 60 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 61 | See the License for the specific language governing permissions and 62 | limitations under the License. 63 | ``` 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **If you only changed documentation or typos, you can ignore the following.** 2 | 3 | After creating the PR, please update the [CHANGELOG.md](https://github.com/diffplug/blowdryer/blob/master/CHANGELOG.md). We follow [keep a changelog conventions](https://keepachangelog.com/en/1.0.0/), everything you need to know is below: 4 | 5 | ``` 6 | ## [Unreleased] 7 | ### Added (or Changed, Deprecated, Removed, Fixed, Security) 8 | - Bullet point for your change ([#1](https://link-to-your-pr)) 9 | 10 | ### Changed (add a header for your entry as necessary) 11 | ``` 12 | 13 | **We won't look at your PR until you update the changelog**. We can always the change the changelog before merging, but it's easier for us to review if you have already condensed the scope of your changes into the changelog. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blowdryer: keep your gradle builds dry 2 | 3 | 11 | [![Gradle plugin](https://img.shields.io/badge/gradle_plugin-com.diffplug.blowdryer-blue.svg)](https://plugins.gradle.org/plugin/com.diffplug.blowdryer) 12 | [![Changelog](https://img.shields.io/badge/changelog-1.7.1-blue.svg)](CHANGELOG.md) 13 | [![Maven central](https://img.shields.io/badge/mavencentral-here-blue.svg)](https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22com.diffplug%22%20AND%20a%3A%22blowdryer%22) 14 | [![Javadoc](https://img.shields.io/badge/javadoc-here-blue.svg)](https://javadoc.io/doc/com.diffplug/blowdryer/1.7.1/index.html) 15 | 16 | 17 | If you have multiple loosely-related gradle projects in separate repositories, then you probably have these problems: 18 | 19 | - challenging to keep build files consistent (copy-paste doesn't scale) 20 | - frustrating to fix the same build upgrade problems over and over in multiple repositories 21 | - a single "master plugin" which applies plugins for you is too restrictive 22 | - hard to debug 23 | - hard to experiment and innovate 24 | 25 | Blowdryer lets you centralize your build scripts, config files, and properties into a single repository, with an easy workflow for pulling those resources into various projects that use them, improving them in-place, then cycling those improvements back across the other projects. 26 | 27 | 33 | 34 | ## How to use it 35 | 36 | First, make a public github repository ([`diffplug/blowdryer-diffplug`](https://github.com/diffplug/blowdryer-diffplug) is a good example), and push the stuff that you want to centralize into the `src/main/resources` subdirectory of that repo. 37 | 38 | Then, in the `settings.gradle` for the project that you want to suck these into, do this: 39 | 40 | ```gradle 41 | plugins { 42 | id 'com.diffplug.blowdryerSetup' version '1.7.1' 43 | } 44 | 45 | blowdryerSetup { 46 | github('acme/blowdryer-acme', 'tag', 'v1.4.5') 47 | // or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14' 48 | // or 'tree', 'a5df7233a98a1407f588e52eb0f31e596eab0228' 49 | 50 | // or gitlab('acme/blowdryer-acme', 'tag', 'v1.4.5').authToken('abc123').customDomainHttp('acme.org') 51 | // or bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').authToken('abc123').customDomainHttps('acme.org') 52 | } 53 | ``` 54 | * Reference on how to create [application password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/) 55 | for Bitbucket Cloud private repo access.
56 | * Reference on how to create [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html) 57 | for Bitbucket Server private repo access. 58 | 59 | Now, in *only* your root `build.gradle`, do this: `apply plugin: 'com.diffplug.blowdryer'`. Now, in any project throughout your gradle build (including subprojects), you can do this: 60 | 61 | ```gradle 62 | apply from: Blowdryer.file('someScript.gradle') 63 | somePlugin { 64 | configFile Blowdryer.file('somePluginConfig.xml') 65 | configProp Blowdryer.prop('propfile', 'key') // key from propfile.properties 66 | } 67 | ``` 68 | 69 | `Blowdryer.file()` returns a `File` which was downloaded to your system temp directory, from the `src/main/resources` folder of `acme/blowdryer-acme`, at the `v1.4.5` tag. Only one download will ever happen for the entire machine, and it will cache it until your system temp directory is cleaned. To force a clean, you can run `gradlew blowdryerWipeEntireCache`. 70 | 71 | `Blowdryer.prop()` parses a java `.properties` file which was downloaded using `Blowdryer.file()`, and then returns the value associated with the given key. 72 | 73 | ### Chinese for "dry" (干) 74 | 75 | If you like brevity and unicode, you can replace `Blowdryer` with `干`. We'll use `干` throughout the rest of the readme, but you can find-replace `干` with `Blowdryer` and get the same results. 76 | 77 | ```gradle 78 | apply from: 干.file('someScript.gradle') 79 | somePlugin { 80 | configFile 干.file('somePluginConfig.xml') 81 | configProp 干.prop('propfile', 'key') 82 | } 83 | ``` 84 | 85 | ### Script plugins 86 | 87 | When you call into a script plugin, you might want to set some configuration values first. You can read them inside the script using `干.proj('propertyName', 'property description for error message')`: 88 | 89 | ```gradle 90 | // build.gradle 91 | ext.pluginPass = 'supersecret' 92 | ext.keyFile = new File('keyFile') 93 | apply from: 干.file('someScript.gradle') 94 | 95 | // someScript.gradle 96 | somePlugin { 97 | pass 干.proj('pluginPass', 'password for the keyFile') 98 | // if the property isn't a String, you have to specify the class you expect 99 | keyFile 干.proj(File.class, 'keyFile', 'location of the keyFile') 100 | } 101 | ``` 102 | 103 | If the property isn't set, you'll get a nice error message describing what was missing, along with links to gradle's documentation on how to set properties (`gradle.properties`, env variables, `ext`, etc). 104 | 105 | #### Script plugin gotchas 106 | 107 | Script plugins can't `import` any classes that were loaded from a third-party plugin on the `build.gradle` classpath. There is an easy workaround, which is to declare all plugins and their versions in the `settings.gradle` file. Blowdryer includes a mechanism for centralizing plugins and their versions, see [plugin versions](#plugin-versions) below. 108 | 109 | ## Dev workflow 110 | 111 | To change and test scripts before you push them up to GitHub, you can do this: 112 | 113 | ```gradle 114 | // settings.gradle 115 | blowdryerSetup { 116 | //github 'acme/blowdryer-acme', 'tag', 'v1.4.5' 117 | devLocal '../path-to-local-blowdryer-acme' 118 | } 119 | ``` 120 | 121 | The call to `devLocal` means that all calls to `Blowdryer.file` will skip caching and get served from that local folder's `src/main/resources` subfolder. This sets up the following virtuous cycle: 122 | 123 | - easily create/improve a plugin in one project using `devLocal '../blowdryer-acme'` 124 | - commit the script, then tag and push to `acme/blowdryer-acme` 125 | - because the `blowdryer-acme` version is immutably pinned **per-project**, you'll never break existing builds as you make changes 126 | - when a project opts-in to update their blowdryer tag, they get all script improvements from that timespan, and an opportunity to test that none of the changes broke their usage. If something broke, you can fix it or just go back to an older tag. 127 | 128 | ### `repoSubfolder` 129 | 130 | If you want your scripts to come from a different subfolder, you can change it: 131 | 132 | ```gradle 133 | // settings.gradle 134 | blowdryerSetup { 135 | repoSubfolder 'some/other/dir/but/why' 136 | github 'acme/blowdryer-acme', 'tag', 'v1.4.5' 137 | } 138 | ``` 139 | 140 | The nice thing about the default `src/main/resources` is that if you ever want to, you can package the files into a plain-old jar and pull the resources from that jar rather than from a github repository. 141 | 142 | ### Packaging as jar 143 | 144 | ```gradle 145 | // settings.gradle 146 | blowdryerSetup { 147 | localJar(file('/absolute/path/to/dependency.jar')) 148 | } 149 | ``` 150 | 151 | To pull this jar from a maven repository, see [#21](https://github.com/diffplug/blowdryer/issues/21). 152 | 153 | ## Plugin versions 154 | 155 | We recommend that your `settings.gradle` should look like this: 156 | 157 | ```gradle 158 | plugins { 159 | id 'com.diffplug.blowdryerSetup' version '1.7.1' 160 | id 'acme.java' version '1.0.0' apply false 161 | id 'acme.kotlin' version '2.0.0' apply false 162 | } 163 | blowdryerSetup { 164 | github('acme/blowdryer-acme', 'tag', 'v1.4.5') 165 | setPluginsBlockTo { 166 | it.file('plugin.versions') 167 | } 168 | } 169 | ``` 170 | 171 | First note that every plugin has `apply false` except for `com.diffplug.blowdryerSetup`. That is on purpose. We need to apply `blowdryerSetup` so that we can use the `blowdryerSetup {}` block, and we need to do `apply false` on the other plugins because we're just putting them on the classpath, not actually using them (yet). 172 | 173 | The second thing to note is `setPluginsBlockTo { it.file('plugin.versions') }`. That means that if you go to `github.com/acme/blowdryer-acme` and then open the `v1.4.5` tab and then go into the `src/main/resources` folder, you will find a file called `plugin.versions`. And the content of that file will be 174 | 175 | ```gradle 176 | id 'com.diffplug.blowdryerSetup' version '1.7.1' 177 | id 'acme.java' version '1.0.0' apply false 178 | id 'acme.kotlin' version '2.0.0' apply false 179 | ``` 180 | 181 | Blowdryer is using the same immutable file mechanism described earlier, but this time it's using it to set just that one section of your `settings.gradle` using a workflow very similar to the [`spotlessCheck` / `spotlessApply` idea](https://github.com/diffplug/spotless/blob/main/plugin-gradle/README.md). 182 | 183 | ### Updating plugin versions 184 | 185 | The workflow goes like this: 186 | 187 | 1. Enter `devLocal` mode (demonstrated [above](#dev-workflow)) 188 | 2. Update the `plugin.versions` file 189 | 3. When you try to run your build, you will get an error 190 | - > settings.gradle plugins block has the wrong content. Add -DsetPluginVersions to overwrite 191 | 4. Add `-DsetPluginVersions` to your command line 192 | 5. You'll get another error 193 | - > settings.gradle plugins block was written successfully. Plugin versions have been updated, try again. 194 | 6. Now the plugins block will be up-to-date and your next build will succeed 195 | 196 | ### Tweaking the `plugin.versions` 197 | 198 | It doesn't *have* to be called `plugin.versions`, it's just using the `干.file` mechanism and sticking that file in. So you could have `plugin-java.versions` and `plugin-kotlin.versions`. Also, you have other methods you can call: 199 | 200 | ```gradle 201 | setPluginsBlockTo { 202 | it.file('plugin.versions') 203 | it.file('kotlin-extras.versions') 204 | it.add(" id 'special-plugin-for-just-this-project' version '1.0.0'") 205 | it.remove(" id 'acme.java' version '1.0.0' apply false") 206 | it.replace('1.7.20', '1.8.0') // update Kotlin version but only for this build 207 | } 208 | ``` 209 | 210 | ### Compared to version catalogs 211 | 212 | Recent versions of Gradle shipped a flexible [version catalog](https://docs.gradle.org/current/userguide/platforms.html) feature. You can use that in combination with blowdryer's `setPluginsBlockTo`. The problem is that every plugin you use throughout the build still has to be declared in the `settings.gradle` with `apply false`. Just having the version in the catalog isn't enough. See [script plugin gotchas](#script-plugin-gotchas) above for the gory classloader details. 213 | 214 | Disappointingly, you can't use `libs.versions.toml` inside the `settings.gradle` file, which is exactly the place that we need it. 215 | 216 | ## API Reference 217 | 218 | You have to apply the `com.diffplug.blowdryerSetup` plugin in your `settings.gradle`. But you don't actually have to `apply plugin: 'com.diffplug.blowdryer'` in your `build.gradle`, you can also just use these static methods (even in `settings.gradle` or inside the code of other plugins). 219 | 220 | ```gradle 221 | // com.diffplug.blowdryer.干 is alias of com.diffplug.blowdryer.Blowdryer 222 | static File 干.file(String resource) 223 | static String 干.prop(String propFile, String key) 224 | static String 干.proj(Project proj, String String key, String description) 225 | static T 干.proj(Project proj, Class clazz, String String key, String description) 226 | static File 干.immutableUrl(String guaranteedImmutableUrl) 227 | static File 干.immutableUrl(String guaranteedImmutableUrl, String fileSuffix) 228 | // 干.immutableUrl('https://foo.org/?file=blah.foo&rev=7') returns a file which ends in `.foo-rev-7` 229 | // 干.immutableUrl('https://foo.org/?file=blah.foo&rev=7', '.foo') returns a file which ends in `.foo` 230 | ``` 231 | 232 | - [javadoc `Blowdryer`](https://javadoc.io/doc/com.diffplug/blowdryer/1.7.0/com/diffplug/blowdryer/Blowdryer.html) 233 | - [javadoc `BlowdryerSetup`](https://javadoc.io/doc/com.diffplug/blowdryer/1.7.0/com/diffplug/blowdryer/BlowdryerSetup.html) 234 | - [javadoc `BlowdryerSetup.PluginsBlock`](https://javadoc.io/doc/com.diffplug/blowdryer/latest/com/diffplug/blowdryer/BlowdryerSetup.PluginsBlock.html) 235 | 236 | If you do `apply plugin: 'com.diffplug.blowdryer'` then every project gets an extension object ([code](https://github.com/diffplug/blowdryer/blob/master/src/main/java/com/diffplug/blowdryer/BlowdryerPlugin.java)) where the project field has been filled in for you, which is why we don't pass it explicitly in the examples before this section. If you don't apply the plugin, you can still call these static methods and pass `project` explicitly for the `proj()` methods. 237 | 238 | ### Using with Kotlin 239 | 240 | The Gradle Kotlin DSL doesn't play well with the name-based extension object that we use in Groovy, but you can just call the static methods above. 241 | 242 | ```kotlin 243 | // settings.gradle.kts 244 | plugins { 245 | id("com.diffplug.blowdryerSetup") version "1.7.1" 246 | } 247 | import com.diffplug.blowdryer.BlowdryerSetup 248 | import com.diffplug.blowdryer.BlowdryerSetup.GitAnchorType 249 | configure { 250 | github("acme/blowdryer-acme", GitAnchorType.TAG, "v1.4.5") 251 | //devLocal("../path-to-local-blowdryer-acme") 252 | } 253 | 254 | // inside settings.gradle.kts, build.gradle.kts, or any-script.gradle.kts 255 | import com.diffplug.blowdryer.干 // or .Blowdryer 256 | 257 | apply(from = 干.file("script.gradle.kts")) 258 | somePlugin { 259 | configFile 干.file("somePluginConfig.xml") 260 | configProp 干.prop("propfile", "key") 261 | pass 干.proj(project, "pluginPass", "password for the keyFile") 262 | keyFile 干.proj(project, File.class, "keyFile", "location of the keyFile") 263 | } 264 | ``` 265 | 266 | 267 | 268 | ### Other packaging options 269 | 270 | [`Blowdryer.immutableUrl`](https://javadoc.io/static/com.diffplug/blowdryer/1.7.1/com/diffplug/blowdryer/Blowdryer.html#immutableUrl-java.lang.String-) returns a `File` containing the downloaded content of the given URL. It's on you to guarantee that the content of that URL is immutable. 271 | 272 | When you setup the Blowdryer plugin in your `settings.gradle`, you're telling Blowdryer what URL scheme to use when resolving a call to [`Blowdryer.file`](https://javadoc.io/static/com.diffplug/blowdryer/1.7.1/com/diffplug/blowdryer/Blowdryer.html#file-java.lang.String-), for example: 273 | 274 | ```java 275 | //blowdryer { 276 | // github 'acme/blowdryer-acme', 'tag', 'v1.4.5' 277 | public GitHub github(String repoOrg, GitAnchorType anchorType, String anchor) { 278 | String root = "https://raw.githubusercontent.com/" + repoOrg + "/" + anchor + "/" + repoSubfolder + "/"; 279 | Blowdryer.setResourcePlugin(resource -> root + resource); 280 | return ; 281 | } 282 | ``` 283 | 284 | If you develop support for other git hosts, please open a PR! You can test prototypes with the code below, and clean up your mistakes with `gradlew blowdryerWipeEntireCache`. 285 | 286 | ```gradle 287 | blowdryerSetup { 288 | experimental { source -> 'https://someImmutableUrlScheme/' + source } 289 | } 290 | ``` 291 | 292 | ## In the wild 293 | 294 | Here are resource repositories in the wild (PRs welcome for others!) 295 | 296 | - https://github.com/diffplug/blowdryer-diffplug 297 | - https://github.com/mytakedotorg/blowdryer-mtdo 298 | 299 | ## Blowdryer for [gulp](https://gulpjs.com/), etc. 300 | 301 | It would be handy to have something like this for other script-based build systems. It would be great to standardize on `干`, feel free to name your project `blowdryer-foo`. If you find or build one, whatever names it chooses, let us know with an issue, and we'll link to it here! 302 | 303 | ## Requirements 304 | 305 | Requires Java 8+ and Graadle 6.8+. 306 | 307 | ## Acknowledgements 308 | 309 | 310 | 311 | - Thanks to [Volker Gropp](https://github.com/vgropp) for implementing [GitLab support](https://github.com/diffplug/blowdryer/pull/15) and [authToken support for GitHub and GitLab](https://github.com/diffplug/blowdryer/pull/18). 312 | - Thanks to [Sergey Sklarov](https://github.com/bHacklv) for implementing [Bitbucket support](https://github.com/diffplug/blowdryer/pull/23). 313 | - Thanks to [Chris Serra](https://github.com/chris-serra) for implementing [local jar support](https://github.com/diffplug/blowdryer/pull/20). 314 | - Thanks to [Zac Sweers](https://github.com/ZacSweers) for [sparking](https://github.com/diffplug/spotless/pull/488) the idea for lightweight publishing of immutable scripts. 315 | - [Gradle](https://gradle.com/) is *so* good. 316 | - Maintained by [DiffPlug](https://www.diffplug.com/). 317 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.diffplug.blowdryer' 3 | id 'com.diffplug.spotless' 4 | id 'com.diffplug.spotless-changelog' 5 | id 'com.gradle.plugin-publish' 6 | id 'io.github.gradle-nexus.publish-plugin' 7 | id "com.palantir.idea-test-fix" version "0.1.0" // Added to run tests successfully in IntelliJ 8 | } 9 | 10 | apply from: 干.file('base/changelog.gradle') 11 | apply from: 干.file('base/java.gradle') 12 | apply from: 干.file('spotless/freshmark.gradle') 13 | apply from: 干.file('spotless/java.gradle') 14 | apply from: 干.file('base/gradle-plugin.gradle') 15 | apply from: 干.file('base/maven.gradle') 16 | apply from: 干.file('base/sonatype.gradle') 17 | 18 | tasks.named('spotlessFreshmark') { 19 | if (JavaVersion.current().isJava12Compatible()) { 20 | enabled = false 21 | } 22 | } 23 | if (System.properties['os.name'].toLowerCase().contains('windows')) { 24 | tasks.named('javadoc') { 25 | enabled = false 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation 'com.squareup.okhttp3:okhttp:4.10.0' 31 | implementation 'com.squareup.okio:okio:3.3.0' 32 | implementation 'com.google.code.gson:gson:2.10.1' 33 | implementation 'com.diffplug.durian:durian-core:1.2.0' 34 | implementation 'com.diffplug.durian:durian-io:1.2.0' 35 | testImplementation 'junit:junit:4.13.2' 36 | testImplementation 'org.assertj:assertj-core:3.24.2' 37 | testImplementation 'org.mockito:mockito-core:4.11.0' 38 | testImplementation 'com.github.tomakehurst:wiremock-jre8:2.35.0' 39 | } 40 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | maven_name=Blowdryer 2 | maven_desc=Keep your gradle builds dry \u5E72 3 | maven_group=com.diffplug 4 | license=apache 5 | git_url=github.com/diffplug/blowdryer 6 | 7 | ver_java=8 8 | javadoc_links=\ 9 | https://docs.oracle.com/javase/8/docs/api/ \ 10 | https://docs.gradle.org/6.0/javadoc/ 11 | 12 | plugin_list=blowdryerSetup blowdryer 13 | plugin_tags=defaults standards dry multi-project \u5E72 14 | 15 | plugin_blowdryerSetup_id =com.diffplug.blowdryerSetup 16 | plugin_blowdryerSetup_impl=com.diffplug.blowdryer.BlowdryerSetupPlugin 17 | plugin_blowdryerSetup_name=Blowdryer Setup 18 | plugin_blowdryerSetup_desc=Keep your gradle builds dry \u5E72 19 | 20 | plugin_blowdryer_id =com.diffplug.blowdryer 21 | plugin_blowdryer_impl=com.diffplug.blowdryer.BlowdryerPlugin 22 | plugin_blowdryer_name=Blowdryer 23 | plugin_blowdryer_desc=Keep your gradle builds dry \u5E72 24 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diffplug/blowdryer/9ec93ca95c11d1d41230401d38d94617b5843313/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /logo-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diffplug/blowdryer/9ec93ca95c11d1d41230401d38d94617b5843313/logo-square.png -------------------------------------------------------------------------------- /logo.license: -------------------------------------------------------------------------------- 1 | Woman holding hairdryer image was obtained from: https://pixabay.com/illustrations/women-hair-dryer-hair-lady-people-4133566/ 2 | 3 | Author: itsilatak2560 https://pixabay.com/users/itsilatak2560-4359449/ 4 | 5 | Used under the pixabay license: https://pixabay.com/service/license/ 6 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diffplug/blowdryer/9ec93ca95c11d1d41230401d38d94617b5843313/logo.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | plugins { 8 | // https://github.com/diffplug/blowdryer/blob/main/CHANGELOG.md 9 | id 'com.diffplug.blowdryerSetup' version '1.7.1' 10 | // https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md 11 | id 'com.diffplug.spotless' version '7.0.3' apply false 12 | // https://github.com/diffplug/spotless-changelog/blob/main/CHANGELOG.md 13 | id 'com.diffplug.spotless-changelog' version '3.1.2' apply false 14 | // https://plugins.gradle.org/plugin/com.gradle.plugin-publish 15 | id 'com.gradle.plugin-publish' version '1.3.1' apply false 16 | // https://github.com/equodev/equo-ide/blob/main/plugin-gradle/CHANGELOG.md 17 | id 'dev.equo.ide' version '1.7.8' apply false 18 | // https://github.com/gradle-nexus/publish-plugin/releases 19 | id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' apply false 20 | } 21 | rootProject.name = 'blowdryer' 22 | blowdryerSetup { 23 | github 'diffplug/blowdryer-diffplug', 'tag', '9.0.0' 24 | //devLocal '../blowdryer-diffplug' 25 | setPluginsBlockTo { 26 | it.file 'plugin.versions' 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/java/com/diffplug/blowdryer/Blowdryer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019-2022 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | 19 | import com.diffplug.common.base.Errors; 20 | import com.diffplug.common.base.Preconditions; 21 | import com.diffplug.common.hash.Hashing; 22 | import com.diffplug.common.io.Files; 23 | import java.io.File; 24 | import java.io.FileNotFoundException; 25 | import java.io.IOException; 26 | import java.io.InputStream; 27 | import java.io.OutputStream; 28 | import java.net.MalformedURLException; 29 | import java.net.URI; 30 | import java.net.URISyntaxException; 31 | import java.nio.charset.StandardCharsets; 32 | import java.nio.file.Path; 33 | import java.util.Base64; 34 | import java.util.Comparator; 35 | import java.util.Date; 36 | import java.util.HashMap; 37 | import java.util.Map; 38 | import java.util.Objects; 39 | import java.util.Properties; 40 | import java.util.zip.ZipEntry; 41 | import java.util.zip.ZipFile; 42 | import javax.annotation.Nullable; 43 | import okhttp3.OkHttpClient; 44 | import okhttp3.Request; 45 | import okhttp3.Response; 46 | import okhttp3.ResponseBody; 47 | import okio.BufferedSink; 48 | import okio.Okio; 49 | import org.gradle.api.Project; 50 | 51 | /** 52 | * Public static methods which retrieve resources as 53 | * determined by {@link BlowdryerSetup}. 54 | */ 55 | public class Blowdryer { 56 | 57 | private static final String FILE_PROTOCOL = "file:///"; 58 | private static final String JAR_FILE_RESOURCE_SEPARATOR = "!/"; 59 | 60 | private Blowdryer() {} 61 | 62 | static void initTempDir(String tempDirPath) { 63 | File tempDir = new File(tempDirPath); 64 | if (cacheTempDir == null) { 65 | cacheTempDir = tempDir; 66 | } else { 67 | Preconditions.checkArgument(cacheTempDir.equals(tempDir)); 68 | } 69 | } 70 | 71 | static File cacheDir() { 72 | Preconditions.checkArgument(cacheTempDir != null, "Call initTempDir first"); 73 | return new File(cacheTempDir, "blowdryer-cache"); 74 | } 75 | 76 | private static File cacheTempDir; 77 | private static final Map urlToContent = new HashMap<>(); 78 | private static final Map> fileToProps = new HashMap<>(); 79 | 80 | static void wipeEntireCache() { 81 | synchronized (Blowdryer.class) { 82 | try { 83 | urlToContent.clear(); 84 | fileToProps.clear(); 85 | java.nio.file.Files.walk(cacheDir().toPath()) 86 | .sorted(Comparator.reverseOrder()) 87 | .forEach(Errors.rethrow().wrap((Path path) -> java.nio.file.Files.delete(path))); 88 | } catch (IOException e) { 89 | throw Errors.asRuntime(e); 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Downloads the given url to a local file in the system temporary directory. 96 | * It will only be downloaded once, system-wide, and it will not be checked for updates. 97 | * This is appropriate only for immutable URLs, such as specific hashes from Git. 98 | */ 99 | public static File immutableUrl(String url) { 100 | return immutableUrl(url, null); 101 | } 102 | 103 | /** 104 | * Downloads the given url to a local file in the system temporary directory. 105 | * It will only be downloaded once, system-wide, and it will not be checked for updates. 106 | * This is appropriate only for immutable URLs, such as specific hashes from Git. 107 | * 108 | * If requiredSuffix is non-null, it is guaranteed that the returned filename will end 109 | * with that string. 110 | */ 111 | public static File immutableUrl(String url, @Nullable String requiredSuffix) { 112 | synchronized (Blowdryer.class) { 113 | String cacheKey = requiredSuffix == null ? url : url + "|" + requiredSuffix; // | is illegal in URLs 114 | File result = urlToContent.get(cacheKey); 115 | if (result != null && result.isFile()) { 116 | return result; 117 | } 118 | 119 | String safe = filenameSafe(url); 120 | if (requiredSuffix != null && !safe.endsWith(requiredSuffix)) { 121 | safe = safe + requiredSuffix; 122 | } 123 | File metaFile = new File(cacheDir(), "meta_" + safe + ".properties"); 124 | File dataFile = new File(cacheDir(), safe); 125 | 126 | try { 127 | if (metaFile.exists() && dataFile.exists()) { 128 | Map props = loadPropertyFile(metaFile); 129 | String propUrl = props.get(PROP_URL); 130 | if (propUrl == null) { 131 | throw new IllegalArgumentException("Unexpected content, recommend deleting file at " + metaFile); 132 | } 133 | if (propUrl.equals(url)) { 134 | urlToContent.put(cacheKey, dataFile); 135 | return dataFile; 136 | } else { 137 | throw new IllegalStateException("Expected url " + url + " but was " + propUrl + ", recommend deleting file at " + metaFile.getAbsolutePath()); 138 | } 139 | } else { 140 | if (metaFile.exists()) { 141 | metaFile.delete(); 142 | } 143 | if (dataFile.exists()) { 144 | dataFile.delete(); 145 | } 146 | Files.createParentDirs(dataFile); 147 | download(url, dataFile); 148 | Properties props = new Properties(); 149 | props.setProperty("version", "1"); 150 | props.setProperty(PROP_URL, url); 151 | props.setProperty("downloadedAt", new Date().toString()); 152 | try (OutputStream output = Files.asByteSink(metaFile).openBufferedStream()) { 153 | props.store(output, ""); 154 | } 155 | urlToContent.put(cacheKey, dataFile); 156 | return dataFile; 157 | } 158 | } catch (IOException | URISyntaxException e) { 159 | throw Errors.asRuntime(e); 160 | } 161 | } 162 | } 163 | 164 | private static Map loadPropertyFile(File file) throws IOException { 165 | Properties props = new Properties(); 166 | try (InputStream input = Files.asByteSource(file).openBufferedStream()) { 167 | props.load(input); 168 | } 169 | Map asMap = new HashMap<>(props.size()); 170 | for (Map.Entry entry : props.entrySet()) { 171 | asMap.put(entry.getKey().toString(), entry.getValue().toString()); 172 | } 173 | return asMap; 174 | } 175 | 176 | private static final String PROP_URL = "url"; 177 | 178 | private static void download(String url, File dst) throws IOException, URISyntaxException { 179 | if (url.startsWith(FILE_PROTOCOL)) { 180 | downloadLocal(url, dst); 181 | } else { 182 | downloadRemote(url, dst); 183 | } 184 | } 185 | 186 | private static void downloadLocal(String url, File dst) throws IOException, URISyntaxException { 187 | 188 | String[] splitUrl = url.split(JAR_FILE_RESOURCE_SEPARATOR); 189 | if (splitUrl.length != 2) { 190 | throw new IllegalArgumentException("Expected a file URL in the format: file:///path-to-dependency.jar!/path-to-file.ext"); 191 | } 192 | 193 | String jarPath = splitUrl[0]; 194 | String filename = splitUrl[1]; 195 | 196 | URI jarPathUri = new URI(jarPath); 197 | try (ZipFile jar = new ZipFile(new File(jarPathUri))) { 198 | ZipEntry foundEntry = jar.stream() 199 | .filter(s -> s.getName().equals(filename)).findAny() 200 | .orElseThrow(() -> new FileNotFoundException("Could not find '" + filename + "' in '" + jarPath + "'")); 201 | 202 | java.nio.file.Files.copy(jar.getInputStream(foundEntry), dst.toPath()); 203 | } 204 | } 205 | 206 | private static void downloadRemote(String url, File dst) throws IOException { 207 | OkHttpClient client = new OkHttpClient.Builder() 208 | .addInterceptor(new RateLimitInterceptor()) 209 | .build(); 210 | Request.Builder req = new Request.Builder().url(url); 211 | authPlugin.addAuthToken(url, req); 212 | try (Response response = client.newCall(req.build()).execute()) { 213 | if (!response.isSuccessful()) { 214 | throw new IllegalArgumentException(url + "\nreceived http code " + response.code() + "\n" + response.body().string()); 215 | } 216 | try (ResponseBody body = response.body(); 217 | BufferedSink sink = Okio.buffer(Okio.sink(dst))) { 218 | if (body == null) { 219 | throw new IllegalArgumentException("Body was expected to be non-null"); 220 | } 221 | sink.writeAll(body.source()); 222 | } 223 | } 224 | } 225 | 226 | private static final int MAX_FILE_LENGTH = 92; 227 | private static final int ABBREVIATED = 40; 228 | 229 | /** Returns either the filename safe URL, or (first40)--(Base64 filenamesafe)(last40). */ 230 | static String filenameSafe(String url) { 231 | String allSafeCharacters = url.replaceAll("[^a-zA-Z0-9-+_.]", "-"); 232 | String noDuplicateDash = allSafeCharacters.replaceAll("-+", "-"); 233 | if (noDuplicateDash.length() <= MAX_FILE_LENGTH) { 234 | return noDuplicateDash; 235 | } else { 236 | int secondPoint = noDuplicateDash.length() - ABBREVIATED; 237 | String first = noDuplicateDash.substring(0, ABBREVIATED); 238 | String middle = noDuplicateDash.substring(ABBREVIATED, secondPoint); 239 | String end = noDuplicateDash.substring(secondPoint); 240 | byte[] hash = Hashing.murmur3_32() 241 | .hashString(middle, StandardCharsets.UTF_8) 242 | .asBytes(); 243 | String hashed = Base64.getEncoder().encodeToString(hash) 244 | .replace('/', '-').replace('=', '-'); 245 | return first + "--" + hashed + end; 246 | } 247 | } 248 | 249 | ////////////////////// 250 | // plugin interface // 251 | ////////////////////// 252 | static interface ResourcePlugin { 253 | String toImmutableUrl(String resourcePath); 254 | } 255 | 256 | private static ResourcePlugin plugin; 257 | 258 | static void assertPluginNotSet(String errorMessage) { 259 | if (Blowdryer.plugin != null) { 260 | throw new IllegalStateException(errorMessage); 261 | } 262 | } 263 | 264 | static void assertPluginNotSet() { 265 | assertPluginNotSet("You already initialized the `blowdryer` plugin, you can't do this twice."); 266 | } 267 | 268 | static void setResourcePluginNull() { 269 | synchronized (Blowdryer.class) { 270 | Blowdryer.plugin = null; 271 | Blowdryer.authPlugin = authPluginNone; 272 | } 273 | } 274 | 275 | static void setResourcePlugin(ResourcePlugin plugin) { 276 | setResourcePlugin(plugin, null); 277 | } 278 | 279 | static void setResourcePlugin(ResourcePlugin plugin, AuthPlugin authPlugin) { 280 | synchronized (Blowdryer.class) { 281 | assertPluginNotSet(); 282 | Blowdryer.plugin = plugin; 283 | Blowdryer.authPlugin = authPlugin == null ? authPluginNone : authPlugin; 284 | } 285 | } 286 | 287 | private static void assertInitialized() { 288 | if (plugin == null) { 289 | throw new IllegalStateException("You needed to initialize the `blowdryer` plugin in the root build.gradle first."); 290 | } 291 | } 292 | 293 | @FunctionalInterface 294 | interface AuthPlugin { 295 | void addAuthToken(String url, Request.Builder builder) throws MalformedURLException; 296 | } 297 | 298 | private static final AuthPlugin authPluginNone = (url, builder) -> {}; 299 | private static AuthPlugin authPlugin = authPluginNone; 300 | 301 | /** Returns the given resource as a File (as configured by {@link BlowdryerSetup}. */ 302 | public static File file(String resourcePath) { 303 | synchronized (Blowdryer.class) { 304 | assertInitialized(); 305 | if (plugin instanceof DevPlugin) { 306 | return new File(((DevPlugin) plugin).root, resourcePath); 307 | } else { 308 | int lastDot = resourcePath.lastIndexOf('.'); 309 | String preserveExtension = lastDot == -1 ? null : resourcePath.substring(lastDot); 310 | return immutableUrl(plugin.toImmutableUrl(resourcePath), preserveExtension); 311 | } 312 | } 313 | } 314 | 315 | static final class DevPlugin implements ResourcePlugin { 316 | File root; 317 | 318 | public DevPlugin(File root) { 319 | this.root = Objects.requireNonNull(root); 320 | } 321 | 322 | @Override 323 | @Deprecated 324 | public final String toImmutableUrl(String resourcePath) { 325 | throw new UnsupportedOperationException(); 326 | } 327 | } 328 | 329 | //////////////// 330 | // Properties // 331 | //////////////// 332 | /** Returns all of the properties from the given url. */ 333 | private static Map props(String resourcePath) { 334 | synchronized (Blowdryer.class) { 335 | try { 336 | assertInitialized(); 337 | if (plugin instanceof DevPlugin) { 338 | return loadPropertyFile(file(resourcePath)); 339 | } else { 340 | File file = file(resourcePath); 341 | Map props = fileToProps.get(file); 342 | if (props != null) { 343 | return props; 344 | } 345 | props = loadPropertyFile(file); 346 | fileToProps.put(file, props); 347 | return props; 348 | } 349 | } catch (IOException e) { 350 | throw Errors.asRuntime(e); 351 | } 352 | } 353 | } 354 | 355 | /** Returns the key from the given propFile (adds .properties extension automatically). */ 356 | public static String prop(String propFile, String key) throws IOException { 357 | Map map = props(propFile + ".properties"); 358 | String value = map.get(key); 359 | if (value == null) { 360 | throw new IllegalArgumentException(propFile + ".properties does not have key '" + key + "', does have " + map.keySet()); 361 | } 362 | return value; 363 | } 364 | 365 | /** 366 | * Reads a property from the project, and throws "Undefined 'key': descForError" if it is missing. 367 | * Requires the property value to be a String. 368 | */ 369 | public static String proj(Project project, String key, String descForError) { 370 | return proj(project, String.class, key, descForError); 371 | } 372 | 373 | /** 374 | * Reads a property from the project, and throws "Undefined 'key': descForError" if it is missing, 375 | * or "Wrong type 'key': descForError - expected java.io.File but was java.io.String" 376 | */ 377 | @SuppressWarnings("unchecked") 378 | public static T proj(Project project, Class clazz, String key, String descForError) { 379 | T value = projOptional(project, clazz, key, descForError); 380 | if (value == null) { 381 | throw new IllegalArgumentException("Undefined '" + key + "': " + descForError + 382 | "\nset in gradle.properties: https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties" + 383 | "\nset in buildscript: https://docs.gradle.org/current/userguide/writing_build_scripts.html#sec:extra_properties" + 384 | "\nset in environment: https://docs.gradle.org/current/userguide/build_environment.html#sec:project_properties" + 385 | "\nexact search order: https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#findProperty-java.lang.String-"); 386 | } else { 387 | return value; 388 | } 389 | } 390 | 391 | /** 392 | * Reads a property from the project, returns null if it is not present. 393 | */ 394 | @SuppressWarnings("unchecked") 395 | public static @Nullable T projOptional(Project project, Class clazz, String key, String descForError) { 396 | Object value = project.findProperty(key); 397 | if (value == null) { 398 | return null; 399 | } else if (!(clazz.isInstance(value))) { 400 | throw new IllegalArgumentException("Wrong type '" + key + "': " + descForError + " - expected " + clazz + " but was " + value.getClass()); 401 | } else { 402 | return (T) value; 403 | } 404 | } 405 | 406 | /** Alias for {@link #projOptional(Project, String, String)} with {@code String.class}. */ 407 | public static @Nullable String projOptional(Project project, String key, String descForError) { 408 | return projOptional(project, String.class, key, descForError); 409 | } 410 | 411 | /** Alias for {@link Blowdryer} which fills in the `project` field of the `proj()` methods automatically. */ 412 | public static class WithProject { 413 | private final Project project; 414 | 415 | public WithProject(Project project) { 416 | this.project = project; 417 | } 418 | 419 | /** Alias for {@link Blowdryer#immutableUrl(String)}. */ 420 | public File immutableUrl(String url) { 421 | return Blowdryer.immutableUrl(url); 422 | } 423 | 424 | /** Alias for {@link Blowdryer#immutableUrl(String, String)}. */ 425 | public File immutableUrl(String url, @Nullable String requiredSuffix) { 426 | return Blowdryer.immutableUrl(url, requiredSuffix); 427 | } 428 | 429 | /** Alias for {@link Blowdryer#file(String)}. */ 430 | public File file(String resource) { 431 | return Blowdryer.file(resource); 432 | } 433 | 434 | /** Alias for {@link Blowdryer#prop(String, String)}. */ 435 | public String prop(String propFile, String key) throws IOException { 436 | return Blowdryer.prop(propFile, key); 437 | } 438 | 439 | /** Alias for {@link Blowdryer#proj(Project, String, String)}. */ 440 | public String proj(String key, String descForError) { 441 | return Blowdryer.proj(project, key, descForError); 442 | } 443 | 444 | /** Alias for {@link Blowdryer#proj(Project, Class, String, String)}. */ 445 | public T proj(Class clazz, String key, String descForError) { 446 | return Blowdryer.proj(project, clazz, key, descForError); 447 | } 448 | 449 | /** Alias for {@link Blowdryer#proj(Project, String, String)}. */ 450 | public @Nullable String projOptional(String key, String descForError) { 451 | return Blowdryer.projOptional(project, key, descForError); 452 | } 453 | 454 | /** Alias for {@link Blowdryer#proj(Project, Class, String, String)}. */ 455 | public @Nullable T projOptional(Class clazz, String key, String descForError) { 456 | return Blowdryer.projOptional(project, clazz, key, descForError); 457 | } 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/blowdryer/BlowdryerPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019-2020 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | 19 | import org.gradle.api.Plugin; 20 | import org.gradle.api.Project; 21 | import org.gradle.util.GradleVersion; 22 | 23 | /** Optional gradle plugin which can only be applied to the root project, and will create the 干 extension on every project. */ 24 | public class BlowdryerPlugin implements Plugin { 25 | static final String PLUGIN_ID = "com.diffplug.blowdryer"; 26 | static final String WIPE_CACHE_TASK = "blowdryerWipeEntireCache"; 27 | 28 | @Override 29 | public void apply(Project root) { 30 | if (root != root.getRootProject()) { 31 | throw new IllegalArgumentException("You must apply this plugin only to the root project."); 32 | } 33 | root.allprojects(p -> { 34 | Blowdryer.WithProject withProject = new Blowdryer.WithProject(p); 35 | p.getExtensions().add("干", withProject); 36 | p.getExtensions().add("Blowdryer", withProject); 37 | }); 38 | 39 | if (GradleVersion.current().compareTo(BlowdryerPluginLegacy.CONFIG_AVOIDANCE_INTRODUCED) >= 0) { 40 | BlowdryerPluginConfigAvoidance.wipeCacheTask(root); 41 | } else { 42 | BlowdryerPluginLegacy.wipeCacheTask(root); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/blowdryer/BlowdryerPluginConfigAvoidance.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2020 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | 19 | import org.gradle.api.Project; 20 | 21 | class BlowdryerPluginConfigAvoidance { 22 | static void wipeCacheTask(Project root) { 23 | root.getTasks().register(BlowdryerPlugin.WIPE_CACHE_TASK, task -> { 24 | task.doFirst(unused -> Blowdryer.wipeEntireCache()); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/blowdryer/BlowdryerPluginLegacy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2020 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | 19 | import org.gradle.api.Project; 20 | import org.gradle.util.GradleVersion; 21 | 22 | class BlowdryerPluginLegacy { 23 | static final GradleVersion CONFIG_AVOIDANCE_INTRODUCED = GradleVersion.version("4.9"); 24 | 25 | static void wipeCacheTask(Project root) { 26 | root.getTasks().create(BlowdryerPlugin.WIPE_CACHE_TASK).doFirst(unused -> { 27 | Blowdryer.wipeEntireCache(); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019-2023 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | import com.diffplug.common.annotations.VisibleForTesting; 19 | import com.diffplug.common.base.Errors; 20 | import com.diffplug.common.base.Unhandled; 21 | import com.google.gson.Gson; 22 | import groovy.lang.Closure; 23 | import java.io.File; 24 | import java.io.IOException; 25 | import java.io.UnsupportedEncodingException; 26 | import java.net.URLEncoder; 27 | import java.nio.charset.StandardCharsets; 28 | import java.nio.file.Files; 29 | import java.util.Arrays; 30 | import java.util.Base64; 31 | import java.util.Objects; 32 | import java.util.function.Function; 33 | import java.util.stream.Collectors; 34 | import javax.annotation.Nullable; 35 | import okhttp3.OkHttpClient; 36 | import okhttp3.Request; 37 | import okhttp3.Request.Builder; 38 | import okhttp3.Response; 39 | import okhttp3.ResponseBody; 40 | import org.gradle.api.Action; 41 | import org.gradle.api.GradleException; 42 | import org.jetbrains.annotations.NotNull; 43 | 44 | /** Configures where {@link Blowdryer#file(String)} downloads files from. */ 45 | public class BlowdryerSetup { 46 | static final String NAME = "blowdryerSetup"; 47 | 48 | private static final String GITHUB_HOST = "raw.githubusercontent.com"; 49 | private static final String GITLAB_HOST = "gitlab.com"; 50 | private static final String BITBUCKET_HOST = "api.bitbucket.org/2.0/repositories"; 51 | 52 | private static final String HTTP_PROTOCOL = "http://"; 53 | private static final String HTTPS_PROTOCOL = "https://"; 54 | 55 | private final File rootDir; 56 | 57 | /** Pass in the directory that will be used to resolve string arguments to devLocal. */ 58 | public BlowdryerSetup(File rootDir) { 59 | Blowdryer.setResourcePluginNull(); // because of gradle daemon 60 | this.rootDir = rootDir; 61 | } 62 | 63 | private static final String REPO_SUBFOLDER_DEFAULT = "src/main/resources"; 64 | private String repoSubfolder = REPO_SUBFOLDER_DEFAULT; 65 | 66 | /** 67 | * Default value is `src/main/resources`. If you change, you must change as the *first* call. 68 | * 69 | * The nice thing about the default `src/main/resources` is that if you ever want to, you could 70 | * copy the blowdryer code into your blowdryer repo, and deploy your own plugin that pulls resources 71 | * from the local jar rather than from github. Keeping the default lets you switch to that approach 72 | * in the future without moving your scripts. 73 | */ 74 | public void repoSubfolder(String repoSubfolder) { 75 | Blowdryer.assertPluginNotSet("You have to call `repoSubfolder` first."); 76 | this.repoSubfolder = assertNoLeadingOrTrailingSlash(repoSubfolder); 77 | } 78 | 79 | public enum GitAnchorType { 80 | TAG, COMMIT, TREE 81 | } 82 | 83 | /** Sets the source where we will grab these scripts. */ 84 | public GitHub github(String repoOrg, GitAnchorType anchorType, String anchor) { 85 | // anchorType isn't used right now, but makes it easier to read what "anchor" is 86 | return new GitHub(repoOrg, anchor); 87 | } 88 | 89 | public class GitHub { 90 | private String repoOrg; 91 | private String anchor; 92 | private @Nullable String authToken; 93 | 94 | private GitHub(String repoOrg, String anchor) { 95 | Blowdryer.assertPluginNotSet(); 96 | this.repoOrg = assertNoLeadingOrTrailingSlash(repoOrg); 97 | this.anchor = assertNoLeadingOrTrailingSlash(anchor); 98 | setGlobals(); 99 | } 100 | 101 | public GitHub authToken(String authToken) { 102 | this.authToken = authToken; 103 | return setGlobals(); 104 | } 105 | 106 | private GitHub setGlobals() { 107 | Blowdryer.setResourcePluginNull(); 108 | String root = HTTPS_PROTOCOL + GITHUB_HOST + "/" + repoOrg + "/" + anchor + "/"; 109 | Blowdryer.setResourcePlugin(resource -> root + getFullResourcePath(resource), authToken == null ? null : (url, builder) -> { 110 | if (url.startsWith(root)) { 111 | builder.addHeader("Authorization", "Bearer " + authToken); 112 | } 113 | }); 114 | return this; 115 | } 116 | } 117 | 118 | /** Sets the source where we will grab these scripts. */ 119 | public GitLab gitlab(String repoOrg, GitAnchorType anchorType, String anchor) { 120 | // anchorType isn't used right now, but makes it easier to read what "anchor" is 121 | return new GitLab(repoOrg, anchor); 122 | } 123 | 124 | public class GitLab { 125 | private String repoOrg; 126 | private String anchor; 127 | private @Nullable String authToken; 128 | private String protocol, host; 129 | 130 | private GitLab(String repoOrg, String anchor) { 131 | Blowdryer.assertPluginNotSet(); 132 | this.repoOrg = assertNoLeadingOrTrailingSlash(repoOrg); 133 | this.anchor = assertNoLeadingOrTrailingSlash(anchor); 134 | customDomainHttps(GITLAB_HOST); 135 | } 136 | 137 | public GitLab authToken(String authToken) { 138 | this.authToken = authToken; 139 | return setGlobals(); 140 | } 141 | 142 | public GitLab customDomainHttp(String domain) { 143 | return customProtocolAndDomain(HTTP_PROTOCOL, domain); 144 | } 145 | 146 | public GitLab customDomainHttps(String domain) { 147 | return customProtocolAndDomain(HTTPS_PROTOCOL, domain); 148 | } 149 | 150 | private GitLab customProtocolAndDomain(String protocol, String domain) { 151 | this.protocol = protocol; 152 | this.host = domain; 153 | return setGlobals(); 154 | } 155 | 156 | private GitLab setGlobals() { 157 | Blowdryer.setResourcePluginNull(); 158 | String urlStart = protocol + host + "/api/v4/projects/" + encodeUrlPart(repoOrg) + "/repository/files/"; 159 | String urlEnd = "/raw?ref=" + encodeUrlPart(anchor); 160 | Blowdryer.setResourcePlugin(resource -> urlStart + encodeUrlPart(getFullResourcePath(resource)) + urlEnd, authToken == null ? null : (url, builder) -> { 161 | if (url.startsWith(urlStart)) { 162 | builder.addHeader("Authorization", "Bearer " + authToken); 163 | } 164 | }); 165 | return this; 166 | } 167 | } 168 | 169 | public enum BitbucketType { 170 | CLOUD, SERVER 171 | } 172 | 173 | /** Sets the source where we will grab these scripts. */ 174 | public Bitbucket bitbucket(String repoOrg, GitAnchorType anchorType, String anchor) { 175 | return new Bitbucket(repoOrg, anchorType, anchor, BitbucketType.CLOUD); 176 | } 177 | 178 | public class Bitbucket { 179 | 180 | private String repoOrg; 181 | private String repoName; 182 | private String anchor; 183 | private GitAnchorType anchorType; 184 | private BitbucketType bitbucketType; 185 | private @Nullable String auth; 186 | private @Nullable String authToken; 187 | private String protocol, host; 188 | 189 | private Bitbucket(String repoOrg, GitAnchorType anchorType, String anchor, BitbucketType bitbucketType) { 190 | Blowdryer.assertPluginNotSet(); 191 | final String[] repoOrgAndName = assertNoLeadingOrTrailingSlash(repoOrg).split("/"); 192 | if (repoOrgAndName.length != 2) { 193 | throw new IllegalArgumentException("repoOrg must be in format 'repoOrg/repoName'"); 194 | } 195 | this.repoOrg = repoOrgAndName[0]; 196 | this.repoName = repoOrgAndName[1]; 197 | this.anchorType = anchorType; 198 | this.bitbucketType = bitbucketType; 199 | this.anchor = assertNoLeadingOrTrailingSlash(anchor); 200 | customProtocolAndDomain(BitbucketType.CLOUD, HTTPS_PROTOCOL, BITBUCKET_HOST); 201 | } 202 | 203 | public Bitbucket authToken(String auth) { 204 | this.auth = auth; 205 | return setGlobals(); 206 | } 207 | 208 | public Bitbucket customDomainHttp(String domain) { 209 | return customProtocolAndDomain(BitbucketType.SERVER, HTTP_PROTOCOL, domain); 210 | } 211 | 212 | public Bitbucket customDomainHttps(String domain) { 213 | return customProtocolAndDomain(BitbucketType.SERVER, HTTPS_PROTOCOL, domain); 214 | } 215 | 216 | private Bitbucket customProtocolAndDomain(BitbucketType type, String protocol, String domain) { 217 | this.bitbucketType = type; 218 | this.protocol = protocol; 219 | this.host = domain; 220 | return setGlobals(); 221 | } 222 | 223 | private Bitbucket setGlobals() { 224 | if (auth == null) { 225 | authToken = null; 226 | } else { 227 | switch (bitbucketType) { 228 | case SERVER: 229 | authToken = String.format("Bearer %s", auth); 230 | break; 231 | case CLOUD: 232 | String base64 = Base64.getEncoder().encodeToString((auth).getBytes(StandardCharsets.UTF_8)); 233 | authToken = String.format("Basic %s", base64); 234 | break; 235 | default: 236 | throw Unhandled.enumException(bitbucketType); 237 | } 238 | } 239 | Blowdryer.setResourcePluginNull(); 240 | String urlStart = getUrlStart(); 241 | Blowdryer.setResourcePlugin(resource -> getFullUrl(urlStart, encodeUrlParts(getFullResourcePath(resource))), (url, builder) -> { 242 | if (authToken != null) { 243 | builder.addHeader("Authorization", authToken); 244 | } 245 | }); 246 | return this; 247 | } 248 | 249 | private String getUrlStart() { 250 | // Bitbucket Cloud and Bitbucket Server (premium, company hosted) has different url structures. 251 | // Bitbucket Cloud uses "org/repo" in URLs, where org is your (or someone else's) account name. 252 | // Bitbucket Server uses "projects/PROJECT_KEY/repos/REPO_NAME" in urls. 253 | if (isServer()) { 254 | return String.format("%s%s/projects/%s/repos/%s", protocol, host, repoOrg, repoName); 255 | } else { 256 | return String.format("%s%s/%s/%s", protocol, host, repoOrg, repoName); 257 | } 258 | } 259 | 260 | private String getFullUrl(String urlStart, String filePath) { 261 | if (isServer()) { 262 | return String.format("%s/raw/%s?at=%s", urlStart, filePath, encodeUrlPart(getAnchorForServer())); 263 | } else { 264 | return String.format("%s/src/%s/%s", urlStart, encodeUrlParts(getAnchorForCloud()), filePath); 265 | } 266 | } 267 | 268 | private boolean isServer() { 269 | return BitbucketType.SERVER.equals(this.bitbucketType); 270 | } 271 | 272 | private String getAnchorForServer() { 273 | switch (anchorType) { 274 | case COMMIT: 275 | return anchor; 276 | case TAG: 277 | return "refs/tags/" + anchor; 278 | default: 279 | throw new UnsupportedOperationException(anchorType + " not supported for Bitbucket"); 280 | } 281 | } 282 | 283 | private String getAnchorForCloud() { 284 | switch (anchorType) { 285 | case COMMIT: 286 | return anchor; 287 | case TAG: 288 | // rewrite the tag into the commit it points to 289 | anchor = getCommitHash("refs/tags/"); 290 | anchorType = GitAnchorType.COMMIT; 291 | return anchor; 292 | default: 293 | throw new UnsupportedOperationException(anchorType + " not supported for Bitbucket"); 294 | } 295 | } 296 | 297 | // Bitbucket API: https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Bworkspace%7D/%7Brepo_slug%7D/src/%7Bcommit%7D/%7Bpath%7D 298 | private String getCommitHash(String baseRefs) { 299 | String requestUrl = String.format("%s/%s%s", getUrlStart(), baseRefs, encodeUrlParts(anchor)); 300 | 301 | return getCommitHashFromBitbucket(requestUrl); 302 | } 303 | 304 | @VisibleForTesting 305 | String getCommitHashFromBitbucket(String requestUrl) { 306 | OkHttpClient client = new OkHttpClient.Builder().build(); 307 | Builder requestBuilder = new Builder().url(requestUrl); 308 | if (authToken != null) { 309 | requestBuilder.addHeader("Authorization", authToken); 310 | } 311 | Request request = requestBuilder.build(); 312 | 313 | try (Response response = client.newCall(request).execute()) { 314 | if (!response.isSuccessful()) { 315 | throw new IllegalArgumentException(String.format("%s\nreceived http code %s \n %s", request.url(), response.code(), 316 | Objects.requireNonNull(response.body()).string())); 317 | } 318 | try (ResponseBody body = response.body()) { 319 | RefsTarget refsTarget = new Gson().fromJson(Objects.requireNonNull(body).string(), RefsTarget.class); 320 | return refsTarget.target.hash; 321 | } 322 | } catch (Exception e) { 323 | throw new IllegalArgumentException("Body was expected to be non-null", e); 324 | } 325 | } 326 | 327 | // Do not encode '/'. 328 | private String encodeUrlParts(String part) { 329 | return Arrays.stream(part.split("/")) 330 | .map(BlowdryerSetup::encodeUrlPart) 331 | .collect(Collectors.joining("/")); 332 | } 333 | 334 | private class RefsTarget { 335 | private final Target target; 336 | 337 | private RefsTarget(Target target) { 338 | this.target = target; 339 | } 340 | 341 | private class Target { 342 | private final String hash; 343 | 344 | private Target(String hash) { 345 | this.hash = hash; 346 | } 347 | } 348 | } 349 | } 350 | 351 | /** 352 | * Uses the provided {@code jarFile} to extract a file resource. 353 | * @param jarFile Absolute path to JAR on the file system. 354 | */ 355 | public void localJar(File jarFile) { 356 | Objects.requireNonNull(jarFile, "jarFile must not be null."); 357 | Blowdryer.setResourcePluginNull(); 358 | if (!repoSubfolder.equals(REPO_SUBFOLDER_DEFAULT)) { 359 | throw new IllegalArgumentException("repoSubfolder has no effect when reading from a jar, delete the call to repoSubfolder."); 360 | } 361 | 362 | String rootUrl = "file:///" + jarFile.getAbsolutePath().replace('\\', '/') + "!/"; 363 | Blowdryer.setResourcePlugin(resource -> rootUrl + resource); 364 | } 365 | 366 | @NotNull 367 | private String getFullResourcePath(String resource) { 368 | return (repoSubfolder.isEmpty() ? "" : repoSubfolder + "/") + resource; 369 | } 370 | 371 | /** Sets the mapping from `file(String)` to `immutableUrl(String)`. */ 372 | public void experimental(Closure function) { 373 | experimental(function::call); 374 | } 375 | 376 | /** Sets the mapping from `file(String)` to `immutableUrl(String)`. */ 377 | public void experimental(Function function) { 378 | Blowdryer.setResourcePlugin(function::apply); 379 | } 380 | 381 | /** Sets the source to be the given local folder, usually for developing changes before they are pushed to git. */ 382 | public void devLocal(Object devPath) { 383 | Objects.requireNonNull(devPath); 384 | File devPathFile; 385 | if (devPath instanceof File) { 386 | devPathFile = (File) devPath; 387 | } else if (devPath instanceof String) { 388 | devPathFile = new File(rootDir, (String) devPath); 389 | } else { 390 | throw new IllegalArgumentException("Expected a String or File, was a " + devPath.getClass()); 391 | } 392 | File projectRoot = Errors.rethrow().get(devPathFile::getCanonicalFile); 393 | File resourceRoot = new File(projectRoot, repoSubfolder); 394 | Blowdryer.setResourcePlugin(new Blowdryer.DevPlugin(resourceRoot)); 395 | } 396 | 397 | private static String assertNoLeadingOrTrailingSlash(String input) { 398 | Objects.requireNonNull(input); 399 | if (input.isEmpty()) { 400 | return input; 401 | } 402 | if (input.charAt(0) == '/') { 403 | throw new IllegalArgumentException("Remove the leading slash"); 404 | } 405 | if (input.charAt(input.length() - 1) == '/') { 406 | throw new IllegalArgumentException("Remove the trailing slash"); 407 | } 408 | return input; 409 | } 410 | 411 | private static String encodeUrlPart(String part) { 412 | try { 413 | return URLEncoder.encode(part, "UTF-8"); 414 | } catch (UnsupportedEncodingException e) { 415 | throw new IllegalArgumentException("error encoding part", e); 416 | } 417 | } 418 | 419 | ////////////////////////////////////////////////// 420 | // set the plugins block inside settings.gradle // 421 | ////////////////////////////////////////////////// 422 | public static class PluginsBlock { 423 | private StringBuilder totalContent = new StringBuilder(); 424 | 425 | public void file(String file) throws IOException { 426 | add(readFile(Blowdryer.file(file))); 427 | } 428 | 429 | public void add(String line) { 430 | totalContent.append(line.replace("\r", "")); 431 | if (!line.endsWith("\n")) { 432 | totalContent.append('\n'); 433 | } 434 | } 435 | 436 | public void remove(String line) { 437 | replace(line + "\n", ""); 438 | } 439 | 440 | public void replace(String in, String out) { 441 | String current = totalContent.toString(); 442 | String replaced = current.replace(in.replace("\r", ""), out); 443 | if (current.equals(replaced)) { 444 | throw new IllegalArgumentException("Doesn't contain " + in + "\n\n" + current); 445 | } 446 | totalContent.setLength(0); 447 | totalContent.append(replaced); 448 | } 449 | 450 | String desiredContent() { 451 | String content = totalContent.toString(); 452 | while (content.endsWith("\n")) { 453 | content = content.substring(0, content.length() - 1); 454 | } 455 | return content; 456 | } 457 | } 458 | 459 | public void setPluginsBlockTo(Action versionSetter) throws IOException { 460 | File settingsDotGradle = new File(rootDir, "settings.gradle"); 461 | PluginsBlockParsed parsed = new PluginsBlockParsed(readFile(settingsDotGradle)); 462 | PluginsBlock versions = new PluginsBlock(); 463 | versionSetter.execute(versions); 464 | if (parsed.inPlugins.equals(versions.desiredContent())) { 465 | return; 466 | } 467 | if (System.getProperty("setPluginVersions") != null) { 468 | parsed.setPluginContent(versions.desiredContent()); 469 | Files.write(settingsDotGradle.toPath(), parsed.contentCorrectEndings().getBytes()); 470 | throw new GradleException("settings.gradle plugins block was written successfully. Plugin versions have been updated, try again."); 471 | } else if (System.getProperty("ignorePluginVersions") != null) { 472 | System.err.println("wrong plugins in settings.gradle, ignoring because of -DignorePluginVersions"); 473 | } else { 474 | throw new GradleException("settings.gradle plugins block has the wrong content.\n" + 475 | " Add -DsetPluginVersions to overwrite\n" + 476 | " Add -DignorePluginVersions to ignore\n" + 477 | " https://github.com/diffplug/blowdryer#plugin-versions for more info.\n\n" + "" + 478 | "DESIRED:\n" + versions.desiredContent() + "\n\nACTUAL:\n" + parsed.inPlugins); 479 | } 480 | } 481 | 482 | private static String readFile(File file) throws IOException { 483 | return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); 484 | } 485 | } 486 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/blowdryer/BlowdryerSetupPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2023 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | import java.util.regex.Matcher; 19 | import java.util.regex.Pattern; 20 | import org.gradle.api.GradleException; 21 | import org.gradle.api.Plugin; 22 | import org.gradle.api.initialization.Settings; 23 | import org.gradle.api.provider.Provider; 24 | 25 | /** Gradle settings plugin which configures the source URL and version. */ 26 | public class BlowdryerSetupPlugin implements Plugin { 27 | static final String MINIMUM_GRADLE = "6.8"; 28 | static final String STOP_FORUSE_AT_CONFIGURATION_TIME = "7.4"; 29 | 30 | private static final Pattern BAD_SEMVER = Pattern.compile("(\\d+)\\.(\\d+)"); 31 | 32 | @Override 33 | public void apply(Settings settings) { 34 | if (badSemver(settings.getGradle().getGradleVersion()) < badSemver(MINIMUM_GRADLE)) { 35 | throw new GradleException("Blowdryer requires Gradle " + MINIMUM_GRADLE + " or newer, this was " + settings.getGradle().getGradleVersion()); 36 | } 37 | Provider tmpDir = settings.getProviders().systemProperty("java.io.tmpdir"); 38 | String tmpDirPath = badSemver(settings.getGradle().getGradleVersion()) >= badSemver(STOP_FORUSE_AT_CONFIGURATION_TIME) ? // depends on Gradle version 39 | tmpDir.get() : // Gradle 7.4 and later 40 | tmpDir.forUseAtConfigurationTime().get(); // before Gradle 7.4 41 | Blowdryer.initTempDir(tmpDirPath); 42 | settings.getExtensions().create(BlowdryerSetup.NAME, BlowdryerSetup.class, settings.getRootDir()); 43 | } 44 | 45 | private static int badSemver(String input) { 46 | Matcher matcher = BAD_SEMVER.matcher(input); 47 | if (!matcher.find() || matcher.start() != 0) { 48 | throw new IllegalArgumentException("Version must start with " + BAD_SEMVER.pattern()); 49 | } 50 | String major = matcher.group(1); 51 | String minor = matcher.group(2); 52 | return badSemver(Integer.parseInt(major), Integer.parseInt(minor)); 53 | } 54 | 55 | /** Ambiguous after 2147.483647.blah-blah */ 56 | private static int badSemver(int major, int minor) { 57 | return major * 1_000_000 + minor; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/blowdryer/PluginsBlockParsed.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | public class PluginsBlockParsed { 19 | boolean isWindowsNewline; 20 | String beforePlugins; 21 | String inPlugins; 22 | String afterPlugins; 23 | 24 | private static final String PLUGINS_OPEN = "\nplugins {\n"; 25 | private static final String PLUGINS_CLOSE = "\n}\n"; 26 | 27 | private static String escape(String input) { 28 | return input.replace("\n", "⏎"); 29 | } 30 | 31 | PluginsBlockParsed(String dirty) { 32 | isWindowsNewline = dirty.indexOf("\r\n") != -1; 33 | String unix = isWindowsNewline ? dirty.replace("\r\n", "\n") : dirty; 34 | 35 | int pluginsStart = unix.indexOf(PLUGINS_OPEN); 36 | if (pluginsStart == -1) { 37 | throw new IllegalArgumentException("Couldn't find " + escape(PLUGINS_OPEN)); 38 | } 39 | beforePlugins = unix.substring(0, pluginsStart); 40 | int pluginsEnd = unix.indexOf("\n}\n", pluginsStart + PLUGINS_OPEN.length()); 41 | if (pluginsEnd == -1) { 42 | throw new IllegalArgumentException("Couldn't find " + escape(PLUGINS_CLOSE) + " after " + escape(PLUGINS_OPEN)); 43 | } 44 | inPlugins = unix.substring(pluginsStart + PLUGINS_OPEN.length(), pluginsEnd); 45 | afterPlugins = unix.substring(pluginsEnd + PLUGINS_CLOSE.length()); 46 | } 47 | 48 | public String inPluginsUnix() { 49 | return inPlugins; 50 | } 51 | 52 | public String contentUnix() { 53 | return beforePlugins + PLUGINS_OPEN + inPlugins + PLUGINS_CLOSE + afterPlugins; 54 | } 55 | 56 | public String contentCorrectEndings() { 57 | return isWindowsNewline ? contentUnix().replace("\n", "\r\n") : contentUnix(); 58 | } 59 | 60 | public void setPluginContent(String desiredContent) { 61 | inPlugins = desiredContent; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/blowdryer/RateLimitInterceptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | 19 | import java.io.IOException; 20 | import okhttp3.Interceptor; 21 | import okhttp3.Response; 22 | 23 | class RateLimitInterceptor implements Interceptor { 24 | private static int RETRY_MAX_ATTEMPTS = 100; 25 | private static long RETRY_MS = 100; 26 | private static long RETRX_MAX_MS = 90_000; 27 | 28 | private int retryAttempts = 0; 29 | 30 | @Override 31 | public Response intercept(Chain chain) throws IOException { 32 | Response response = chain.proceed(chain.request()); 33 | // The retry system depends on the backend being used 34 | // - GitLab -> 429 https://github.com/diffplug/blowdryer/pull/30 35 | // - GitHub, etc. -> PR welcome 36 | if (response.code() == 429 && retryAttempts < RETRY_MAX_ATTEMPTS) { 37 | long retryAfter = RETRY_MS; 38 | try { 39 | retryAfter = Long.parseLong(response.header("Retry-After", "0")) * 1000; 40 | } catch (NumberFormatException e) { 41 | // ignore a non parsable header, it might be a date (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) 42 | // currently only gitlab is tested wich should send seconds in the header 43 | } 44 | if (retryAfter <= 0) { 45 | retryAfter = RETRY_MS; 46 | } 47 | if (retryAfter > RETRX_MAX_MS) { 48 | retryAfter = RETRX_MAX_MS; 49 | } 50 | 51 | response.close(); 52 | try { 53 | System.out.println("Blowdryer request to " + chain.request().url() + " has been rate-limited, retrying in " + retryAfter + " milliseconds"); 54 | Thread.sleep(retryAfter); 55 | } catch (InterruptedException e) { 56 | throw new IllegalStateException("interrupted while waiting due to rate limiting", e); 57 | } 58 | retryAttempts++; 59 | response = chain.proceed(chain.request()); 60 | } 61 | 62 | return response; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/diffplug/blowdryer/干.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019-2022 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | 19 | import java.io.File; 20 | import java.io.IOException; 21 | import javax.annotation.Nullable; 22 | import org.gradle.api.Project; 23 | 24 | /** Alias for {@link Blowdryer}. */ 25 | public class 干 { 26 | private 干() {} 27 | 28 | /** Alias for {@link Blowdryer#immutableUrl(String)}. */ 29 | public static File immutableUrl(String url) { 30 | return Blowdryer.immutableUrl(url); 31 | } 32 | 33 | /** Alias for {@link Blowdryer#immutableUrl(String, String)}. */ 34 | public File immutableUrl(String url, @Nullable String requiredSuffix) { 35 | return Blowdryer.immutableUrl(url, requiredSuffix); 36 | } 37 | 38 | /** Alias for {@link Blowdryer#file(String)}. */ 39 | public static File file(String resource) { 40 | return Blowdryer.file(resource); 41 | } 42 | 43 | /** Alias for {@link Blowdryer#prop(String, String)}. */ 44 | public static String prop(String propFile, String key) throws IOException { 45 | return Blowdryer.prop(propFile, key); 46 | } 47 | 48 | /** Alias for {@link Blowdryer#proj(Project, String, String)}. */ 49 | public static String proj(Project project, String key, String descForError) { 50 | return Blowdryer.proj(project, key, descForError); 51 | } 52 | 53 | /** Alias for {@link Blowdryer#proj(Project, Class, String, String)}. */ 54 | public static T proj(Project project, Class clazz, String key, String descForError) { 55 | return Blowdryer.proj(project, clazz, key, descForError); 56 | } 57 | 58 | /** Alias for {@link Blowdryer#projOptional(Project, Class, String, String)} with {@code String.class}. */ 59 | public static @Nullable String projOptional(Project project, String key, String descForError) { 60 | return Blowdryer.projOptional(project, key, descForError); 61 | } 62 | 63 | /** Alias for {@link Blowdryer#projOptional(Project, Class, String, String)}. */ 64 | public static @Nullable T projOptional(Project project, Class clazz, String key, String descForError) { 65 | return Blowdryer.projOptional(project, clazz, key, descForError); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | 19 | import java.io.File; 20 | import java.io.IOException; 21 | import java.nio.charset.StandardCharsets; 22 | import java.nio.file.Files; 23 | import java.util.Arrays; 24 | import java.util.stream.Collectors; 25 | import org.junit.Ignore; 26 | import org.junit.Test; 27 | 28 | @Ignore("has to be filled with private tokens and repos") 29 | public class BlowdryerPluginAuthTest extends GradleHarness { 30 | 31 | private static final String BITBUCKET_REPO_ORG = "bHack/blowdryer-private"; 32 | private static final String BITBUCKET_REPO_USER = "bHack"; 33 | private static final String BITBUCKET_REPO_APP_PW = "replace-with-app-pw"; 34 | 35 | private static final String BITBUCKET_REPO_PAT_REPO_ORG = "MNT/mnt-centralised-cfg"; 36 | private static final String BITBUCKET_REPO_PAT = "replace-with-pat"; 37 | private static final String BITBUCKET_PRIVATE_SERVER_HOST = "replace.with.private.host"; 38 | 39 | private void settingsGitlabAuth(String tag, String... extra) throws IOException { 40 | write("settings.gradle", 41 | "plugins { id 'com.diffplug.blowdryerSetup' }", 42 | "blowdryerSetup { repoSubfolder(''); " 43 | + "gitlab('private/repo', 'tag', '" + tag + "').authToken('foobar');" 44 | + " }", 45 | Arrays.stream(extra).collect(Collectors.joining("\n"))); 46 | } 47 | 48 | private void settingsGithubAuth(String tag, String... extra) throws IOException { 49 | write("settings.gradle", 50 | "plugins { id 'com.diffplug.blowdryerSetup' }", 51 | "blowdryerSetup { github('private/repo', 'tag', '" + tag + "').authToken('foobar');" 52 | + " }", 53 | Arrays.stream(extra).collect(Collectors.joining("\n"))); 54 | } 55 | 56 | private void settingsBitbucketBasicAuth(String tag, String... extra) throws IOException { 57 | write("settings.gradle", 58 | "plugins { id 'com.diffplug.blowdryerSetup' }", 59 | String.format("blowdryerSetup { bitbucket('%s', 'commit', '%s').cloudAuth('%s:%s');", 60 | BITBUCKET_REPO_ORG, tag, BITBUCKET_REPO_USER, BITBUCKET_REPO_APP_PW) 61 | + " }", 62 | Arrays.stream(extra).collect(Collectors.joining("\n"))); 63 | } 64 | 65 | private void settingsBitbucketPersonalAccessTokenAuth(String tag, String... extra) throws IOException { 66 | write("settings.gradle", 67 | "plugins { id 'com.diffplug.blowdryerSetup' }", 68 | String.format("blowdryerSetup { bitbucket('%s', 'commit', '%s').customDomainHttps('%s')" 69 | + ".serverAuth('%s');", BITBUCKET_REPO_PAT_REPO_ORG, tag, BITBUCKET_PRIVATE_SERVER_HOST, BITBUCKET_REPO_PAT) 70 | + " }", 71 | Arrays.stream(extra).collect(Collectors.joining("\n"))); 72 | } 73 | 74 | @Test 75 | public void githubAuthTag() throws IOException { 76 | settingsGithubAuth("master"); 77 | write("build.gradle", 78 | "apply plugin: 'com.diffplug.blowdryer'", 79 | "assert 干.file('sample').text == 'a'"); 80 | gradleRunner().build(); 81 | } 82 | 83 | @Test 84 | public void gitlabAuthTag() throws IOException { 85 | settingsGitlabAuth("init-test-for-auth"); 86 | write("build.gradle", 87 | "apply plugin: 'com.diffplug.blowdryer'", 88 | "assert 干.file('sample').text == 'a'"); 89 | gradleRunner().build(); 90 | } 91 | 92 | @Test 93 | public void bitbucketCloudAuth() throws IOException { 94 | settingsBitbucketBasicAuth("master"); 95 | write("build.gradle", 96 | "apply plugin: 'com.diffplug.blowdryer'", 97 | "assert 干.file('sample').text == 'a'"); 98 | gradleRunner().build(); 99 | } 100 | 101 | @Test 102 | public void bitbucketServerAuth() throws IOException { 103 | settingsBitbucketPersonalAccessTokenAuth("master"); 104 | write("build.gradle", 105 | "apply plugin: 'com.diffplug.blowdryer'", 106 | "assert 干.file('checkstyle/spotless.gradle').text == 'a'"); 107 | gradleRunner().build(); 108 | } 109 | 110 | /** Writes the given content to the given path. */ 111 | protected File write(String path, String... lines) throws IOException { 112 | File file = file(path); 113 | file.getParentFile().mkdirs(); 114 | Files.write(file.toPath(), Arrays.asList(lines), StandardCharsets.UTF_8); 115 | return file; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/blowdryer/BlowdryerPluginConfigurationCacheTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019-2021 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | 19 | import java.io.IOException; 20 | import java.util.Arrays; 21 | import java.util.stream.Collectors; 22 | import org.junit.Test; 23 | 24 | public class BlowdryerPluginConfigurationCacheTest extends GradleHarness { 25 | private static final String GRADLE_PROPERTIES = "gradle.properties"; 26 | private static final String SETTINGS_GRADLE = "settings.gradle"; 27 | private static final String BUILD_GRADLE = "build.gradle"; 28 | 29 | private void settingsGithub(String tag, String... extra) throws IOException { 30 | write(GRADLE_PROPERTIES, "org.gradle.unsafe.configuration-cache=true"); 31 | write(SETTINGS_GRADLE, 32 | "plugins { id 'com.diffplug.blowdryerSetup' }", 33 | "blowdryerSetup { github('diffplug/blowdryer', 'tag', '" + tag + "') }", 34 | Arrays.stream(extra).collect(Collectors.joining("\n"))); 35 | } 36 | 37 | @Test 38 | public void githubTag() throws IOException { 39 | settingsGithub("test/2/a"); 40 | write(BUILD_GRADLE, 41 | "apply plugin: 'com.diffplug.blowdryer'", 42 | "assert 干.file('sample').text == 'a'", 43 | "assert 干.prop('sample', 'name') == 'test'", 44 | "assert 干.prop('sample', 'ver_spotless') == '1.2.0'"); 45 | gradleRunner().build(); 46 | 47 | settingsGithub("test/2/b"); 48 | write(BUILD_GRADLE, 49 | "apply plugin: 'com.diffplug.blowdryer'", 50 | "assert 干.file('sample').text == 'b'", 51 | "assert 干.prop('sample', 'name') == 'testB'", 52 | "assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'"); 53 | gradleRunner().build(); 54 | 55 | // double-check that failures do fail 56 | settingsGithub("test/2/b"); 57 | write(BUILD_GRADLE, 58 | "plugins { id 'com.diffplug.blowdryer' }", 59 | "assert Blowdryer.file('sample').text == 'a'"); 60 | gradleRunner().buildAndFail(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019-2023 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | import java.io.IOException; 19 | import java.util.Arrays; 20 | import java.util.Locale; 21 | import java.util.stream.Collectors; 22 | import org.assertj.core.api.Assertions; 23 | import org.gradle.testkit.runner.BuildResult; 24 | import org.junit.Assume; 25 | import org.junit.Test; 26 | 27 | public class BlowdryerPluginTest extends GradleHarness { 28 | 29 | private static final String SETTINGS_GRADLE = "settings.gradle"; 30 | private static final String BUILD_GRADLE = "build.gradle"; 31 | private static final String BITBUCKET_REPO_ORG = "diffplug"; 32 | 33 | private void settingsGithub(String tag, String... extra) throws IOException { 34 | write(SETTINGS_GRADLE, 35 | "plugins { id 'com.diffplug.blowdryerSetup' }", 36 | "blowdryerSetup { github('diffplug/blowdryer', 'tag', '" + tag + "') }", 37 | Arrays.stream(extra).collect(Collectors.joining("\n"))); 38 | } 39 | 40 | private void settingsGitlab(String tag, String... extra) throws IOException { 41 | write(SETTINGS_GRADLE, 42 | "plugins { id 'com.diffplug.blowdryerSetup' }", 43 | "blowdryerSetup { gitlab('diffplug/blowdryer', 'tag', '" + tag + "') }", 44 | Arrays.stream(extra).collect(Collectors.joining("\n"))); 45 | } 46 | 47 | private void settingsCustomGitlab(String tag, String... extra) throws IOException { 48 | write(SETTINGS_GRADLE, 49 | "plugins { id 'com.diffplug.blowdryerSetup' }", 50 | "blowdryerSetup { gitlab('diffplug/blowdryer', 'tag', '" + tag + "').customDomainHttps('gitlab.com') }", 51 | Arrays.stream(extra).collect(Collectors.joining("\n"))); 52 | } 53 | 54 | private void settingsGitlabRootFolder(String tag, String... extra) throws IOException { 55 | write(SETTINGS_GRADLE, 56 | "plugins { id 'com.diffplug.blowdryerSetup' }", 57 | "blowdryerSetup { repoSubfolder(''); gitlab('diffplug/blowdryer', 'tag', '" + tag + "') }", 58 | Arrays.stream(extra).collect(Collectors.joining("\n"))); 59 | } 60 | 61 | private void settingsBitbucket(String tag, String... extra) throws IOException { 62 | write(SETTINGS_GRADLE, 63 | "plugins { id 'com.diffplug.blowdryerSetup' }", 64 | String.format("blowdryerSetup { bitbucket('%s/blowdryer', 'tag', '%s') }", BITBUCKET_REPO_ORG, tag), 65 | Arrays.stream(extra).collect(Collectors.joining("\n"))); 66 | } 67 | 68 | private void settingsLocalJar(String dependency) throws IOException { 69 | write(SETTINGS_GRADLE, 70 | "plugins { id 'com.diffplug.blowdryerSetup' }", 71 | "blowdryerSetup { localJar(file('" + dependency + "')) }"); 72 | } 73 | 74 | @Test 75 | public void githubTag() throws IOException { 76 | settingsGithub("test/2/a"); 77 | write(BUILD_GRADLE, 78 | "apply plugin: 'com.diffplug.blowdryer'", 79 | "assert 干.file('sample').text == 'a'", 80 | "assert 干.prop('sample', 'name') == 'test'", 81 | "assert 干.prop('sample', 'ver_spotless') == '1.2.0'"); 82 | gradleRunner().build(); 83 | 84 | settingsGithub("test/2/b"); 85 | write(BUILD_GRADLE, 86 | "apply plugin: 'com.diffplug.blowdryer'", 87 | "assert 干.file('sample').text == 'b'", 88 | "assert 干.prop('sample', 'name') == 'testB'", 89 | "assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'"); 90 | gradleRunner().build(); 91 | 92 | // double-check that failures do fail 93 | settingsGithub("test/2/b"); 94 | write(BUILD_GRADLE, 95 | "plugins { id 'com.diffplug.blowdryer' }", 96 | "assert Blowdryer.file('sample').text == 'a'"); 97 | gradleRunner().buildAndFail(); 98 | } 99 | 100 | @Test 101 | public void gitlabTag() throws IOException { 102 | settingsGitlab("test/2/a"); 103 | write(BUILD_GRADLE, 104 | "apply plugin: 'com.diffplug.blowdryer'", 105 | "assert 干.file('sample').text == 'a'", 106 | "assert 干.prop('sample', 'name') == 'test'", 107 | "assert 干.prop('sample', 'ver_spotless') == '1.2.0'"); 108 | gradleRunner().build(); 109 | 110 | settingsGitlab("test/2/b"); 111 | write(BUILD_GRADLE, 112 | "apply plugin: 'com.diffplug.blowdryer'", 113 | "assert 干.file('sample').text == 'b'", 114 | "assert 干.prop('sample', 'name') == 'testB'", 115 | "assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'"); 116 | gradleRunner().build(); 117 | 118 | // double-check that failures do fail 119 | settingsGitlab("test/2/b"); 120 | write(BUILD_GRADLE, 121 | "plugins { id 'com.diffplug.blowdryer' }", 122 | "assert Blowdryer.file('sample').text == 'a'"); 123 | gradleRunner().buildAndFail(); 124 | } 125 | 126 | @Test 127 | public void bitbucketTag() throws IOException { 128 | settingsBitbucket("test/2/a"); 129 | write(BUILD_GRADLE, 130 | "apply plugin: 'com.diffplug.blowdryer'", 131 | "assert 干.file('sample').text == 'a'", 132 | "assert 干.prop('sample', 'name') == 'test'", 133 | "assert 干.prop('sample', 'ver_spotless') == '1.2.0'"); 134 | gradleRunner().build(); 135 | 136 | settingsBitbucket("test/2/b"); 137 | write(BUILD_GRADLE, 138 | "apply plugin: 'com.diffplug.blowdryer'", 139 | "assert 干.file('sample').text == 'b'", 140 | "assert 干.prop('sample', 'name') == 'testB'", 141 | "assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'"); 142 | gradleRunner().build(); 143 | 144 | // double-check that failures do fail 145 | settingsBitbucket("test/2/b"); 146 | write(BUILD_GRADLE, 147 | "plugins { id 'com.diffplug.blowdryer' }", 148 | "assert Blowdryer.file('sample').text == 'a'"); 149 | gradleRunner().buildAndFail(); 150 | } 151 | 152 | @Test 153 | public void customGitlabTag() throws IOException { 154 | settingsCustomGitlab("test/2/a"); 155 | write(BUILD_GRADLE, 156 | "apply plugin: 'com.diffplug.blowdryer'", 157 | "assert 干.file('sample').text == 'a'", 158 | "assert 干.prop('sample', 'name') == 'test'", 159 | "assert 干.prop('sample', 'ver_spotless') == '1.2.0'"); 160 | gradleRunner().build(); 161 | 162 | settingsCustomGitlab("test/2/b"); 163 | write(BUILD_GRADLE, 164 | "apply plugin: 'com.diffplug.blowdryer'", 165 | "assert 干.file('sample').text == 'b'", 166 | "assert 干.prop('sample', 'name') == 'testB'", 167 | "assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'"); 168 | gradleRunner().build(); 169 | 170 | // double-check that failures do fail 171 | settingsCustomGitlab("test/2/b"); 172 | write(BUILD_GRADLE, 173 | "plugins { id 'com.diffplug.blowdryer' }", 174 | "assert Blowdryer.file('sample').text == 'a'"); 175 | gradleRunner().buildAndFail(); 176 | } 177 | 178 | @Test 179 | public void rootfolderGitlabTag() throws IOException { 180 | settingsGitlabRootFolder("test/2/a"); 181 | write(BUILD_GRADLE, 182 | "apply plugin: 'com.diffplug.blowdryer'", 183 | "assert 干.file('src/main/resources/sample').text == 'a'", 184 | "assert 干.prop('src/main/resources/sample', 'name') == 'test'", 185 | "assert 干.prop('src/main/resources/sample', 'ver_spotless') == '1.2.0'"); 186 | gradleRunner().build(); 187 | 188 | settingsGitlabRootFolder("test/2/b"); 189 | write(BUILD_GRADLE, 190 | "apply plugin: 'com.diffplug.blowdryer'", 191 | "assert 干.file('src/main/resources/sample').text == 'b'", 192 | "assert 干.prop('src/main/resources/sample', 'name') == 'testB'", 193 | "assert 干.prop('src/main/resources/sample', 'group') == 'com.diffplug.gradleB'"); 194 | gradleRunner().build(); 195 | 196 | // double-check that failures do fail 197 | settingsGitlabRootFolder("test/2/b"); 198 | write(BUILD_GRADLE, 199 | "plugins { id 'com.diffplug.blowdryer' }", 200 | "assert Blowdryer.file('src/main/resources/sample').text == 'a'"); 201 | gradleRunner().buildAndFail(); 202 | } 203 | 204 | @Test 205 | public void devLocal() throws IOException { 206 | write("../blowdryer-script/src/main/resources/sample", "c"); 207 | write("../blowdryer-script/src/main/resources/sample.properties", 208 | "name=test", 209 | "group=com.diffplug.gradle"); 210 | write(SETTINGS_GRADLE, 211 | "plugins { id 'com.diffplug.blowdryerSetup' }", 212 | "blowdryerSetup { devLocal('../blowdryer-script') }"); 213 | write(BUILD_GRADLE, 214 | "apply plugin: 'com.diffplug.blowdryer'", 215 | // .replace('\\r', '') fixes test on windows 216 | "assert 干.file('sample').text.replace('\\r', '') == 'c\\n'", 217 | "assert 干.prop('sample', 'name') == 'test'", 218 | "assert 干.prop('sample', 'group') == 'com.diffplug.gradle'"); 219 | gradleRunner().build(); 220 | } 221 | 222 | @Test 223 | public void multiproject() throws IOException { 224 | settingsGithub("test/2/a", 225 | "include 'subproject'"); 226 | write(BUILD_GRADLE, 227 | "apply plugin: 'com.diffplug.blowdryer'", 228 | "assert 干.file('sample').text == 'a'", 229 | "assert 干.prop('sample', 'name') == 'test'", 230 | "assert 干.prop('sample', 'group') == 'com.diffplug.gradle'"); 231 | write("subproject/build.gradle", 232 | "assert 干.file('sample').text == 'a'", 233 | "assert 干.prop('sample', 'name') == 'test'", 234 | "assert 干.prop('sample', 'group') == 'com.diffplug.gradle'"); 235 | gradleRunner().build(); 236 | 237 | // double-check that failures do fail 238 | write("subproject/build.gradle", 239 | "import com.diffplug.blowdryer.Blowdryer", 240 | "", 241 | "assert Blowdryer.file('sample').text == 'b'"); 242 | gradleRunner().buildAndFail(); 243 | } 244 | 245 | @Test 246 | public void missingResourceThrowsError() throws IOException { 247 | settingsGithub("test/2/a"); 248 | write(BUILD_GRADLE, 249 | "plugins { id 'com.diffplug.blowdryer' }", 250 | "干.file('notPresent')"); 251 | Assertions.assertThat(gradleRunner().buildAndFail().getOutput().replace("\r\n", "\n")).contains( 252 | "https://raw.githubusercontent.com/diffplug/blowdryer/test/2/a/src/main/resources/notPresent\n" + 253 | " received http code 404\n" + 254 | " 404: Not Found"); 255 | } 256 | 257 | @Test 258 | public void cfgTestGroovy() throws IOException { 259 | write("../blowdryer-script/src/main/resources/sample.properties", 260 | "name=test", 261 | "group=com.diffplug.gradle"); 262 | write(SETTINGS_GRADLE, 263 | "plugins { id 'com.diffplug.blowdryerSetup' }", 264 | "blowdryerSetup { devLocal('../blowdryer-script') }"); 265 | write("../blowdryer-script/src/main/resources/script.gradle", 266 | "import com.diffplug.blowdryer.Blowdryer", 267 | "apply plugin: 'com.diffplug.blowdryer'", 268 | "println 干.proj('pluginPass', 'password for the keyFile')", 269 | "println 干.proj(File.class, 'keyFile', 'location of the keyFile')", 270 | "println 干.prop('sample', 'group')", 271 | ""); 272 | write(BUILD_GRADLE, 273 | "apply plugin: 'com.diffplug.blowdryer'", 274 | "ext.pluginPass = 'supersecret'", 275 | "ext.keyFile = new File('keyFile.txt')", 276 | "apply from: 干.file('script.gradle')"); 277 | Assertions.assertThat(gradleRunner().build().getOutput().replace("\r\n", "\n")).contains( 278 | "> Configure project :\n" + 279 | "supersecret\n" + 280 | "keyFile.txt\n" + 281 | "com.diffplug.gradle\n" + 282 | "\n"); 283 | } 284 | 285 | @Test 286 | public void cfgTestKotlin() throws IOException { 287 | write("../blowdryer-script/src/main/resources/sample.properties", 288 | "name=test", 289 | "group=com.diffplug.gradle"); 290 | write("settings.gradle.kts", 291 | "plugins { id(\"com.diffplug.blowdryerSetup\") }", 292 | "import com.diffplug.blowdryer.BlowdryerSetup", 293 | "configure {", 294 | " devLocal(\"../blowdryer-script\")", 295 | "}"); 296 | write("../blowdryer-script/src/main/resources/script.gradle.kts", 297 | "import com.diffplug.blowdryer.干", 298 | "println(干.proj(project, \"pluginPass\", \"password for the keyFile\"))", 299 | "println(干.proj(project, File::class.java, \"keyFile\", \"location of the keyFile\"))", 300 | "println(干.prop(\"sample\", \"group\"))"); 301 | write("build.gradle.kts", 302 | "import com.diffplug.blowdryer.干", 303 | "val pluginPass by extra(\"supersecret\")", 304 | "val keyFile by extra(File(\"keyFile.txt\"))", 305 | "apply(from = 干.file(\"script.gradle.kts\"))"); 306 | Assertions.assertThat(gradleRunner().build().getOutput().replace("\r\n", "\n")).contains( 307 | "> Configure project :\n" + 308 | "supersecret\n" + 309 | "keyFile.txt\n" + 310 | "com.diffplug.gradle\n" + 311 | "\n"); 312 | } 313 | 314 | @Test 315 | public void settingsTest() throws IOException { 316 | write(SETTINGS_GRADLE, 317 | "plugins { id 'com.diffplug.blowdryerSetup' }", 318 | "blowdryerSetup { github('diffplug/blowdryer', 'tag', 'test/2/a') }", 319 | "import com.diffplug.blowdryer.干", 320 | "assert 干.file('sample').text == 'a'", 321 | "assert 干.prop('sample', 'name') == 'test'", 322 | "assert 干.prop('sample', 'ver_spotless') == '1.2.0'", 323 | "println 'test was success'"); 324 | Assertions.assertThat(gradleRunner().build().getOutput().replace("\r\n", "\n")); 325 | } 326 | 327 | @Test 328 | public void localJarFileDownloadExists() throws IOException { 329 | String jarFile = BlowdryerPluginTest.class.getResource("test.jar").getFile(); 330 | if (System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win")) { 331 | Assertions.assertThat(jarFile).startsWith("/"); 332 | jarFile = jarFile.substring(1); 333 | } 334 | settingsLocalJar(jarFile); 335 | 336 | write(BUILD_GRADLE, 337 | "apply plugin: 'com.diffplug.blowdryer'", 338 | "assert 干.file('sample').exists()"); 339 | 340 | gradleRunner().build(); 341 | } 342 | 343 | @Test 344 | public void localJarRepoSubfolderException() throws IOException { 345 | String jarFile = BlowdryerPluginTest.class.getResource("test.jar").getFile(); 346 | write(SETTINGS_GRADLE, 347 | "plugins { id 'com.diffplug.blowdryerSetup' }", 348 | "blowdryerSetup {", 349 | " repoSubfolder('blah')", 350 | " localJar(file('" + jarFile + "'))", 351 | "}"); 352 | Assertions.assertThat(gradleRunner().buildAndFail().getOutput().replace("\r\n", "\n")).contains("repoSubfolder has no effect when reading from a jar"); 353 | } 354 | 355 | @Test 356 | public void localJarFileDownloadDoesNotExist() throws IOException { 357 | String jarFile = BlowdryerPluginTest.class.getResource("test.jar").getFile(); 358 | settingsLocalJar(jarFile); 359 | 360 | write(BUILD_GRADLE, 361 | "apply plugin: 'com.diffplug.blowdryer'", 362 | "assert 干.file('invalid-file.txt').exists()"); 363 | 364 | gradleRunner().buildAndFail(); 365 | } 366 | 367 | @Test 368 | public void tooOldError() throws IOException { 369 | Assume.assumeTrue(!System.getProperty("java.vm.specification.version").equals("17")); 370 | write(SETTINGS_GRADLE, 371 | "plugins { id 'com.diffplug.blowdryerSetup' }", 372 | "blowdryerSetup { github('diffplug/blowdryer', 'tag', 'test/2/a') }", 373 | "import com.diffplug.blowdryer.干", 374 | "assert 干.file('sample').text == 'a'", 375 | "assert 干.prop('sample', 'name') == 'test'", 376 | "assert 干.prop('sample', 'ver_spotless') == '1.2.0'", 377 | "println 'test was success'"); 378 | Assertions.assertThat(gradleRunner().withGradleVersion("6.7").buildAndFail().getOutput().replace("\r\n", "\n")) 379 | .contains("Blowdryer requires Gradle 6.8 or newer, this was 6.7"); 380 | } 381 | 382 | @Test 383 | public void deprecationError() throws IOException { 384 | settingsGithub("test/2/a"); 385 | BuildResult build = gradleRunner().withGradleVersion("8.5").withArguments("--warning-mode=fail").build(); 386 | Assertions.assertThat(build.getOutput()).doesNotContain("Provider.forUseAtConfigurationTime method has been deprecated"); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/blowdryer/BlowdryerRetryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019-2022 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 19 | import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; 20 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 21 | import static com.github.tomakehurst.wiremock.client.WireMock.verify; 22 | import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | 25 | import com.diffplug.common.base.StandardSystemProperty; 26 | import com.github.tomakehurst.wiremock.client.WireMock; 27 | import com.github.tomakehurst.wiremock.core.WireMockConfiguration; 28 | import com.github.tomakehurst.wiremock.junit.WireMockRule; 29 | import java.io.File; 30 | import java.io.IOException; 31 | import org.junit.Before; 32 | import org.junit.Rule; 33 | import org.junit.Test; 34 | 35 | public class BlowdryerRetryTest extends GradleHarness { 36 | 37 | private static final String RETRY_SCENARIO = "Retry Scenario"; 38 | private static final String CAUSE_LIMIT_FAILED = "Cause limitFailed"; 39 | 40 | @Rule 41 | public WireMockRule wireMockRule = new WireMockRule(WireMockConfiguration.wireMockConfig().dynamicPort().dynamicHttpsPort()); 42 | 43 | @Before 44 | public void setup() throws IOException { 45 | Blowdryer.setResourcePluginNull(); 46 | Blowdryer.initTempDir(StandardSystemProperty.JAVA_IO_TMPDIR.value()); 47 | } 48 | 49 | @Test 50 | public void gitlabTriggersRateLimit() throws IOException { 51 | BlowdryerSetup blowdryerSetup = new BlowdryerSetup(new File(".")); 52 | blowdryerSetup.gitlab("foo/bar", BlowdryerSetup.GitAnchorType.TAG, "1.0"); 53 | String fileContent = "foobar"; 54 | 55 | wireMockRule.stubFor(WireMock.get(WireMock.urlEqualTo("/bar")) 56 | .inScenario(RETRY_SCENARIO) 57 | .whenScenarioStateIs(STARTED) 58 | .willReturn(aResponse() 59 | .withHeader("Retry-After", "1") 60 | .withStatus(429)) 61 | .willSetStateTo(CAUSE_LIMIT_FAILED)); 62 | 63 | wireMockRule.stubFor(WireMock.get(WireMock.urlEqualTo("/bar")) 64 | .inScenario(RETRY_SCENARIO) 65 | .whenScenarioStateIs(CAUSE_LIMIT_FAILED) 66 | .willReturn(aResponse() 67 | .withStatus(200) 68 | .withBody(fileContent))); 69 | 70 | File downloadedFile = Blowdryer.immutableUrl("http://localhost:" + wireMockRule.port() + "/bar"); 71 | 72 | verify(2, getRequestedFor(urlEqualTo("/bar"))); 73 | assertThat(downloadedFile).hasContent(fileContent); 74 | } 75 | 76 | @Test 77 | public void gitlabTriggersRateLimitNoHeader() throws IOException { 78 | BlowdryerSetup blowdryerSetup = new BlowdryerSetup(new File(".")); 79 | blowdryerSetup.gitlab("foo/bar", BlowdryerSetup.GitAnchorType.TAG, "1.0"); 80 | String fileContent = "foobar"; 81 | 82 | wireMockRule.stubFor(WireMock.get(WireMock.urlEqualTo("/bar")) 83 | .inScenario(RETRY_SCENARIO) 84 | .whenScenarioStateIs(STARTED) 85 | .willReturn(aResponse() 86 | .withStatus(429)) 87 | .willSetStateTo(CAUSE_LIMIT_FAILED)); 88 | 89 | wireMockRule.stubFor(WireMock.get(WireMock.urlEqualTo("/bar")) 90 | .inScenario(RETRY_SCENARIO) 91 | .whenScenarioStateIs(CAUSE_LIMIT_FAILED) 92 | .willReturn(aResponse() 93 | .withStatus(200) 94 | .withBody(fileContent))); 95 | 96 | File downloadedFile = Blowdryer.immutableUrl("http://localhost:" + wireMockRule.port() + "/bar"); 97 | 98 | verify(2, getRequestedFor(urlEqualTo("/bar"))); 99 | assertThat(downloadedFile).hasContent(fileContent); 100 | } 101 | 102 | @Test 103 | public void gitlabTriggersRateLimitDate() throws IOException { 104 | BlowdryerSetup blowdryerSetup = new BlowdryerSetup(new File(".")); 105 | blowdryerSetup.gitlab("foo/bar", BlowdryerSetup.GitAnchorType.TAG, "1.0"); 106 | String fileContent = "foobar"; 107 | 108 | wireMockRule.stubFor(WireMock.get(WireMock.urlEqualTo("/bar")) 109 | .inScenario(RETRY_SCENARIO) 110 | .whenScenarioStateIs(STARTED) 111 | .willReturn(aResponse() 112 | .withHeader("Retry-After", "Wed, 21 Oct 2015 07:28:00 GMT") 113 | .withStatus(429)) 114 | .willSetStateTo(CAUSE_LIMIT_FAILED)); 115 | 116 | wireMockRule.stubFor(WireMock.get(WireMock.urlEqualTo("/bar")) 117 | .inScenario(RETRY_SCENARIO) 118 | .whenScenarioStateIs(CAUSE_LIMIT_FAILED) 119 | .willReturn(aResponse() 120 | .withStatus(200) 121 | .withBody(fileContent))); 122 | 123 | File downloadedFile = Blowdryer.immutableUrl("http://localhost:" + wireMockRule.port() + "/bar"); 124 | 125 | verify(2, getRequestedFor(urlEqualTo("/bar"))); 126 | assertThat(downloadedFile).hasContent(fileContent); 127 | } 128 | 129 | @Test 130 | public void gitlabRequestWithoutLimit() throws IOException { 131 | BlowdryerSetup blowdryerSetup = new BlowdryerSetup(new File(".")); 132 | blowdryerSetup.gitlab("foo/bar", BlowdryerSetup.GitAnchorType.TAG, "1.0"); 133 | String fileContent = "foobar"; 134 | 135 | wireMockRule.stubFor(WireMock.get(WireMock.urlEqualTo("/bar")) 136 | .willReturn(aResponse() 137 | .withStatus(200) 138 | .withBody(fileContent))); 139 | 140 | File downloadedFile = Blowdryer.immutableUrl("http://localhost:" + wireMockRule.port() + "/bar"); 141 | 142 | verify(1, getRequestedFor(urlEqualTo("/bar"))); 143 | assertThat(downloadedFile).hasContent(fileContent); 144 | } 145 | 146 | @Test 147 | public void githubRequestWithoutLimit() throws IOException { 148 | BlowdryerSetup blowdryerSetup = new BlowdryerSetup(new File(".")); 149 | blowdryerSetup.github("foo/bar", BlowdryerSetup.GitAnchorType.TAG, "1.0"); 150 | String fileContent = "foobar"; 151 | 152 | wireMockRule.stubFor(WireMock.get(WireMock.urlEqualTo("/bar")) 153 | .willReturn(aResponse() 154 | .withStatus(200) 155 | .withBody(fileContent))); 156 | 157 | File downloadedFile = Blowdryer.immutableUrl("http://localhost:" + wireMockRule.port() + "/bar"); 158 | 159 | verify(1, getRequestedFor(urlEqualTo("/bar"))); 160 | assertThat(downloadedFile).hasContent(fileContent); 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/blowdryer/BlowdryerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019-2023 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | import static java.nio.charset.StandardCharsets.UTF_8; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 21 | import static org.mockito.Mockito.doReturn; 22 | import static org.mockito.Mockito.spy; 23 | 24 | import com.diffplug.blowdryer.Blowdryer.AuthPlugin; 25 | import com.diffplug.blowdryer.Blowdryer.ResourcePlugin; 26 | import com.diffplug.blowdryer.BlowdryerSetup.Bitbucket; 27 | import com.diffplug.blowdryer.BlowdryerSetup.GitAnchorType; 28 | import com.diffplug.common.base.StandardSystemProperty; 29 | import java.lang.reflect.Field; 30 | import java.util.Base64; 31 | import java.util.Locale; 32 | import java.util.UUID; 33 | import okhttp3.Request; 34 | import okhttp3.Request.Builder; 35 | import org.assertj.core.api.Assertions; 36 | import org.junit.BeforeClass; 37 | import org.junit.Test; 38 | 39 | public class BlowdryerTest { 40 | private static final String JAR_FILE_RESOURCE_SEPARATOR = "!/"; 41 | private static final String FILE_PROTOCOL = "file:///"; 42 | 43 | @BeforeClass 44 | public static void setup() { 45 | Blowdryer.initTempDir(StandardSystemProperty.JAVA_IO_TMPDIR.value()); 46 | } 47 | 48 | @Test 49 | public void filenameSafe() { 50 | filenameSafe("https://foo.org/?file=blah.foo&rev=7", "https-foo.org-file-blah.foo-rev-7"); 51 | filenameSafe("http://shortName.com/a+b-0-9~Z", "http-shortName.com-a+b-0-9-Z"); 52 | filenameSafe("https://raw.githubusercontent.com/diffplug/durian-build/07f588e52eb0f31e596eab0228a5df7233a98a14/gradle/spotless/spotless.license.java", 53 | "https-raw.githubusercontent.com-diffplug--3vpUTw--14-gradle-spotless-spotless.license.java"); 54 | } 55 | 56 | private void filenameSafe(String url, String safe) { 57 | assertThat(Blowdryer.filenameSafe(url)).isEqualTo(safe); 58 | } 59 | 60 | @Test 61 | public void cachedFileDeleted_issue_11() { 62 | String test = "https://raw.githubusercontent.com/diffplug/blowdryer/test/2/b/src/main/resources/sample"; 63 | assertThat(Blowdryer.immutableUrl(test)).hasContent("b"); 64 | Blowdryer.immutableUrl(test).delete(); 65 | assertThat(Blowdryer.immutableUrl(test)).hasContent("b"); 66 | } 67 | 68 | @Test 69 | public void immutableUrlOfLocalJar() { 70 | String jarFile = BlowdryerPluginTest.class.getResource("test.jar").getFile(); 71 | if (System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win")) { 72 | Assertions.assertThat(jarFile).startsWith("/"); 73 | jarFile = jarFile.substring(1).replace('\\', '/'); 74 | } 75 | assertThat(Blowdryer.immutableUrl(FILE_PROTOCOL + jarFile + JAR_FILE_RESOURCE_SEPARATOR + "sample")).exists(); 76 | } 77 | 78 | @Test 79 | public void requiredSuffix() { 80 | String jarFile = BlowdryerPluginTest.class.getResource("test.jar").getFile(); 81 | assertThat(Blowdryer.immutableUrl(FILE_PROTOCOL + jarFile + JAR_FILE_RESOURCE_SEPARATOR + "sample", ".suffix").getName()).endsWith(".suffix"); 82 | assertThat(Blowdryer.immutableUrl(FILE_PROTOCOL + jarFile + JAR_FILE_RESOURCE_SEPARATOR + "sample", ".suffix2").getName()).endsWith(".suffix2"); 83 | } 84 | 85 | @Test 86 | public void bitbucketCloud_tagAnchorType() throws Exception { 87 | final String hashRequestUrl = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/refs/tags/testAnchor"; 88 | final String hash = UUID.randomUUID().toString(); 89 | final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/" + hash + "/src/main/resources/test.properties"; 90 | 91 | Bitbucket spy = spy(setupBitbucketTestTarget(GitAnchorType.TAG)).authToken("un:pw"); 92 | doReturn(hash).when(spy).getCommitHashFromBitbucket(hashRequestUrl); 93 | final ResourcePlugin target = getResourcePlugin(); 94 | 95 | assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); 96 | } 97 | 98 | @Test 99 | public void bitbucketCloud_commitAnchorType() throws Exception { 100 | final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/testAnchor/src/main/resources/test.properties"; 101 | setupBitbucketTestTarget(GitAnchorType.COMMIT); 102 | final ResourcePlugin target = getResourcePlugin(); 103 | 104 | assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); 105 | } 106 | 107 | @Test 108 | public void bitbucketCloud_treeAnchorType() throws Exception { 109 | setupBitbucketTestTarget(GitAnchorType.TREE); 110 | final ResourcePlugin target = getResourcePlugin(); 111 | 112 | assertThatThrownBy(() -> target.toImmutableUrl("test.properties")) 113 | .isInstanceOf(UnsupportedOperationException.class) 114 | .hasMessage("TREE not supported for Bitbucket"); 115 | } 116 | 117 | @Test 118 | public void bitbucketServer_tagAnchorType() throws Exception { 119 | final String expected = "https://my.bitbucket.com/projects/testOrg/repos/testRepo/raw/src/main/resources/test.properties?at=refs%2Ftags%2FtestAnchor"; 120 | setupBitbucketTestTarget(GitAnchorType.TAG).customDomainHttps("my.bitbucket.com"); 121 | final ResourcePlugin target = getResourcePlugin(); 122 | 123 | assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); 124 | } 125 | 126 | @Test 127 | public void bitbucketServer_commitAnchorType() throws Exception { 128 | final String expected = "https://my.bitbucket.com/projects/testOrg/repos/testRepo/raw/src/main/resources/test.properties?at=testAnchor"; 129 | setupBitbucketTestTarget(GitAnchorType.COMMIT).customDomainHttps("my.bitbucket.com"); 130 | final ResourcePlugin target = getResourcePlugin(); 131 | 132 | assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); 133 | } 134 | 135 | @Test 136 | public void bitbucketServer_treeAnchorType() throws Exception { 137 | setupBitbucketTestTarget(GitAnchorType.TREE).customDomainHttps("my.bitbucket.com"); 138 | final ResourcePlugin target = getResourcePlugin(); 139 | 140 | assertThatThrownBy(() -> target.toImmutableUrl("test.properties")) 141 | .isInstanceOf(UnsupportedOperationException.class) 142 | .hasMessage("TREE not supported for Bitbucket"); 143 | } 144 | 145 | @Test 146 | public void bitbucketCloudAuth() throws Exception { 147 | final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/testAnchor/src/main/resources/test.properties"; 148 | final String usernameAndAppPassword = String.format("%s:%s", randomUUID(), randomUUID()); 149 | setupBitbucketTestTarget(GitAnchorType.COMMIT).authToken(usernameAndAppPassword); 150 | 151 | final ResourcePlugin target = getResourcePlugin(); 152 | final AuthPlugin otherTarget = getAuthPlugin(); 153 | final Builder requestBuilder = new Builder().url(expected); 154 | otherTarget.addAuthToken(expected, requestBuilder); 155 | final Request request = requestBuilder.build(); 156 | 157 | assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); 158 | final String encoded = Base64.getEncoder().encodeToString((usernameAndAppPassword) 159 | .getBytes(UTF_8)); 160 | assertThat(request.header("Authorization")).isEqualTo(String.format("Basic %s", encoded)); 161 | } 162 | 163 | private Bitbucket setupBitbucketTestTarget(final GitAnchorType anchorType) { 164 | final String repoOrg = "testOrg/testRepo"; 165 | final String anchor = "testAnchor"; 166 | return new BlowdryerSetup(null).bitbucket(repoOrg, anchorType, anchor); 167 | } 168 | 169 | private ResourcePlugin getResourcePlugin() throws Exception { 170 | final Field field = Blowdryer.class.getDeclaredField("plugin"); 171 | field.setAccessible(true); 172 | return (ResourcePlugin) field.get(null); 173 | } 174 | 175 | private AuthPlugin getAuthPlugin() throws Exception { 176 | final Field field = Blowdryer.class.getDeclaredField("authPlugin"); 177 | field.setAccessible(true); 178 | return (AuthPlugin) field.get(null); 179 | } 180 | 181 | private String randomUUID() { 182 | return UUID.randomUUID().toString(); 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/blowdryer/GradleHarness.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2023 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | import java.io.IOException; 19 | import org.gradle.testkit.runner.GradleRunner; 20 | 21 | public class GradleHarness extends ResourceHarness { 22 | /** A gradleRunner(). */ 23 | protected GradleRunner gradleRunner() throws IOException { 24 | GradleRunner runner = GradleRunner.create() 25 | .withProjectDir(rootFolder()) 26 | .withPluginClasspath(); 27 | if (jreVersion() < 16) { 28 | runner.withGradleVersion(BlowdryerSetupPlugin.MINIMUM_GRADLE); 29 | } 30 | return runner; 31 | } 32 | 33 | private static int jreVersion() { 34 | return Integer.parseInt(System.getProperty("java.vm.specification.version")); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/blowdryer/PluginsBlockParsedTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | import java.io.IOException; 19 | import org.assertj.core.api.Assertions; 20 | import org.junit.Test; 21 | 22 | public class PluginsBlockParsedTest extends ResourceHarness { 23 | private static String content(String insidePlugins) { 24 | return "pluginManagement {\r\n" + 25 | " repositories {\r\n" + 26 | " mavenCentral()\r\n" + 27 | " gradlePluginPortal()\r\n" + 28 | " }\r\n" + 29 | "}\r\n" + 30 | "plugins {\r\n" + 31 | insidePlugins + 32 | "}\r\n" + 33 | "rootProject.name = 'blowdryer'\r\n"; 34 | } 35 | 36 | @Test 37 | public void parser() { 38 | String insidePlugins = " // https://plugins.gradle.org/plugin/com.gradle.plugin-publish\r\n" + 39 | " id 'com.gradle.plugin-publish' version '0.20.0' apply false\r\n" + 40 | " // https://github.com/equodev/equo-ide/blob/main/plugin-gradle/CHANGELOG.md\r\n" + 41 | " id 'dev.equo.ide' version '0.12.1' apply false\r\n" + 42 | " // https://github.com/gradle-nexus/publish-plugin/releases\r\n" + 43 | " id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' apply false\r\n"; 44 | String input = content(insidePlugins); 45 | PluginsBlockParsed parsed = new PluginsBlockParsed(input); 46 | Assertions.assertThat(parsed.contentCorrectEndings()).isEqualTo(input); 47 | Assertions.assertThat(parsed.beforePlugins).isEqualTo("pluginManagement {\n" + 48 | " repositories {\n" + 49 | " mavenCentral()\n" + 50 | " gradlePluginPortal()\n" + 51 | " }\n" + 52 | "}"); 53 | Assertions.assertThat(parsed.inPlugins).isEqualTo(" // https://plugins.gradle.org/plugin/com.gradle.plugin-publish\n" + 54 | " id 'com.gradle.plugin-publish' version '0.20.0' apply false\n" + 55 | " // https://github.com/equodev/equo-ide/blob/main/plugin-gradle/CHANGELOG.md\n" + 56 | " id 'dev.equo.ide' version '0.12.1' apply false\n" + 57 | " // https://github.com/gradle-nexus/publish-plugin/releases\n" + 58 | " id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' apply false"); 59 | Assertions.assertThat(parsed.afterPlugins).isEqualTo("rootProject.name = 'blowdryer'\n"); 60 | } 61 | 62 | @Test 63 | public void test() throws IOException { 64 | String insidePlugins = " // https://plugins.gradle.org/plugin/com.gradle.plugin-publish\r\n" + 65 | " id 'com.gradle.plugin-publish' version '0.20.0' apply false\r\n" + 66 | " // https://github.com/equodev/equo-ide/blob/main/plugin-gradle/CHANGELOG.md\r\n" + 67 | " id 'dev.equo.ide' version '0.12.1' apply false\r\n" + 68 | " // https://github.com/gradle-nexus/publish-plugin/releases\r\n" + 69 | " id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' apply false\r\n"; 70 | String input = content(insidePlugins); 71 | write("settings.gradle", input); 72 | 73 | BlowdryerSetup setup = new BlowdryerSetup(rootFolder()); 74 | 75 | // this should succeed 76 | setup.setPluginsBlockTo(pluginVersions -> { 77 | pluginVersions.add(insidePlugins); 78 | }); 79 | 80 | // this should fail 81 | Assertions.assertThatThrownBy(() -> setup.setPluginsBlockTo(pluginVersions -> { 82 | pluginVersions.add("TEST"); 83 | })).hasMessage("settings.gradle plugins block has the wrong content.\n" + 84 | " Add -DsetPluginVersions to overwrite\n" + 85 | " Add -DignorePluginVersions to ignore\n" + 86 | " https://github.com/diffplug/blowdryer#plugin-versions for more info.\n" + 87 | "\n" + 88 | "DESIRED:\n" + 89 | "TEST\n" + 90 | "\n" + 91 | "ACTUAL:\n" + 92 | " // https://plugins.gradle.org/plugin/com.gradle.plugin-publish\n" + 93 | " id 'com.gradle.plugin-publish' version '0.20.0' apply false\n" + 94 | " // https://github.com/equodev/equo-ide/blob/main/plugin-gradle/CHANGELOG.md\n" + 95 | " id 'dev.equo.ide' version '0.12.1' apply false\n" + 96 | " // https://github.com/gradle-nexus/publish-plugin/releases\n" + 97 | " id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' apply false"); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/com/diffplug/blowdryer/ResourceHarness.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019-2020 DiffPlug 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 | * https://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 | package com.diffplug.blowdryer; 17 | 18 | 19 | import com.diffplug.common.io.Resources; 20 | import java.io.File; 21 | import java.io.IOException; 22 | import java.net.URL; 23 | import java.nio.charset.StandardCharsets; 24 | import java.nio.file.Files; 25 | import java.util.ArrayList; 26 | import java.util.Arrays; 27 | import java.util.Collections; 28 | import java.util.List; 29 | import org.assertj.core.api.AbstractFileAssert; 30 | import org.assertj.core.api.Assertions; 31 | import org.assertj.core.api.ListAssert; 32 | import org.junit.Rule; 33 | import org.junit.rules.TemporaryFolder; 34 | 35 | public class ResourceHarness { 36 | /** 37 | * On OS X, the temp folder is a symlink, 38 | * and some of gradle's stuff breaks symlinks. 39 | * By only accessing it through the {@link #rootFolder()} 40 | * and {@link #newFile()} apis, we can guarantee there 41 | * will be no symlink problems. 42 | */ 43 | @Rule 44 | public TemporaryFolder folderDontUseDirectly = new TemporaryFolder(); 45 | 46 | /** Returns the root folder (canonicalized to fix OS X issue) */ 47 | protected File rootFolder() throws IOException { 48 | return folderDontUseDirectly.getRoot().getCanonicalFile(); 49 | } 50 | 51 | /** Returns a File (in a temporary folder) which has the given contents. */ 52 | protected File file(String subpath) throws IOException { 53 | return new File(rootFolder(), subpath); 54 | } 55 | 56 | /** Returns a File (in a temporary folder) which has the given contents. */ 57 | protected File write(String subpath, byte[] content) throws IOException { 58 | File file = file(subpath); 59 | file.getParentFile().mkdirs(); 60 | Files.write(file.toPath(), content); 61 | return file; 62 | } 63 | 64 | /** Writes the given content to the given path. */ 65 | protected File write(String path, String... lines) throws IOException { 66 | File file = file(path); 67 | file.getParentFile().mkdirs(); 68 | Files.write(file.toPath(), Arrays.asList(lines), StandardCharsets.UTF_8); 69 | return file; 70 | } 71 | 72 | protected AbstractFileAssert assertFile(String path) throws IOException { 73 | return Assertions.assertThat(file(path)); 74 | } 75 | 76 | protected ListAssert assertFolderContent(String path) throws IOException { 77 | List children = new ArrayList<>(); 78 | for (File child : file(path).listFiles()) { 79 | children.add(child.getName()); 80 | } 81 | Collections.sort(children); 82 | return Assertions.assertThat(children); 83 | } 84 | 85 | /** Returns the contents of the given file from the src/test/resources directory. */ 86 | protected static byte[] readTestResource(String filename) throws IOException { 87 | URL url = ResourceHarness.class.getResource("/" + filename); 88 | if (url == null) { 89 | throw new IllegalArgumentException("No such resource " + filename); 90 | } 91 | return Resources.toByteArray(url); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/test/resources/com/diffplug/blowdryer/test.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diffplug/blowdryer/9ec93ca95c11d1d41230401d38d94617b5843313/src/test/resources/com/diffplug/blowdryer/test.jar --------------------------------------------------------------------------------