{"id":20713826,"url":"https://github.com/diffplug/blowdryer","last_synced_at":"2025-04-23T08:08:14.015Z","repository":{"id":39926241,"uuid":"225066692","full_name":"diffplug/blowdryer","owner":"diffplug","description":"Keep your gradle builds dry 干","archived":false,"fork":false,"pushed_at":"2023-12-08T19:53:19.000Z","size":1293,"stargazers_count":28,"open_issues_count":6,"forks_count":5,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-23T08:08:06.229Z","etag":null,"topics":["dry","gradle","gradle-build","gradle-plugin"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/diffplug.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-11-30T20:34:24.000Z","updated_at":"2025-01-10T17:26:21.000Z","dependencies_parsed_at":"2024-11-19T02:01:07.433Z","dependency_job_id":null,"html_url":"https://github.com/diffplug/blowdryer","commit_stats":null,"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diffplug%2Fblowdryer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diffplug%2Fblowdryer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diffplug%2Fblowdryer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diffplug%2Fblowdryer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/diffplug","download_url":"https://codeload.github.com/diffplug/blowdryer/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250395288,"owners_count":21423400,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dry","gradle","gradle-build","gradle-plugin"],"created_at":"2024-11-17T02:27:30.476Z","updated_at":"2025-04-23T08:08:13.996Z","avatar_url":"https://github.com/diffplug.png","language":"Java","readme":"# \u003cimg align=\"left\" src=\"logo.png\"\u003e Blowdryer: keep your gradle builds dry\n\n\u003c!---freshmark shields\noutput = [\n    link(shield('Gradle plugin', 'gradle plugin', 'com.diffplug.blowdryer', 'blue'), 'https://plugins.gradle.org/plugin/com.diffplug.blowdryer'),\n    link(shield('Changelog', 'changelog', versionLast, 'blue'), 'CHANGELOG.md'),\n    link(shield('Maven central', 'mavencentral', 'here', 'blue'), 'https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22com.diffplug%22%20AND%20a%3A%22blowdryer%22'),\n    link(shield('Javadoc', 'javadoc', 'here', 'blue'), 'https://javadoc.io/doc/com.diffplug/blowdryer/{{versionLast}}/index.html'),\n].join('\\n');\n--\u003e\n[![Gradle plugin](https://img.shields.io/badge/gradle_plugin-com.diffplug.blowdryer-blue.svg)](https://plugins.gradle.org/plugin/com.diffplug.blowdryer)\n[![Changelog](https://img.shields.io/badge/changelog-1.7.1-blue.svg)](CHANGELOG.md)\n[![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)\n[![Javadoc](https://img.shields.io/badge/javadoc-here-blue.svg)](https://javadoc.io/doc/com.diffplug/blowdryer/1.7.1/index.html)\n\u003c!---freshmark /shields --\u003e\n\nIf you have multiple loosely-related gradle projects in separate repositories, then you probably have these problems:\n\n- challenging to keep build files consistent (copy-paste doesn't scale)\n- frustrating to fix the same build upgrade problems over and over in multiple repositories\n- a single \"master plugin\" which applies plugins for you is too restrictive\n  - hard to debug\n  - hard to experiment and innovate\n\nBlowdryer 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.\n\n\u003c!---freshmark version\noutput = prefixDelimiterReplace(input, \"id 'com.diffplug.blowdryerSetup' version '\", \"'\", versionLast)\noutput = prefixDelimiterReplace(output, 'id(\"com.diffplug.blowdryerSetup\") version \"', '\"', versionLast)\noutput = prefixDelimiterReplace(output, 'https://javadoc.io/static/com.diffplug/blowdryer/', '/', versionLast)\noutput = prefixDelimiterReplace(output, \"'com.diffplug:blowdryer:\", \"'\", versionLast)\n--\u003e\n\n## How to use it\n\nFirst, 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.\n\nThen, in the `settings.gradle` for the project that you want to suck these into, do this:\n\n```gradle\nplugins {\n  id 'com.diffplug.blowdryerSetup' version '1.7.1'\n}\n\nblowdryerSetup {\n  github('acme/blowdryer-acme', 'tag', 'v1.4.5')\n  //                         or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14'\n  //                         or 'tree',   'a5df7233a98a1407f588e52eb0f31e596eab0228'\n\n  // or gitlab('acme/blowdryer-acme', 'tag', 'v1.4.5').authToken('abc123').customDomainHttp('acme.org')\n  // or bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').authToken('abc123').customDomainHttps('acme.org')\n}\n```\n* Reference on how to create [application password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/)\nfor Bitbucket Cloud private repo access.\u003cbr/\u003e\n* Reference on how to create [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html)\nfor Bitbucket Server private repo access.\n\nNow, 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:\n\n```gradle\napply from: Blowdryer.file('someScript.gradle')\nsomePlugin {\n  configFile Blowdryer.file('somePluginConfig.xml')\n  configProp Blowdryer.prop('propfile', 'key') // key from propfile.properties\n}\n```\n\n`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`.\n\n`Blowdryer.prop()` parses a java `.properties` file which was downloaded using `Blowdryer.file()`, and then returns the value associated with the given key.\n\n### Chinese for \"dry\" (干)\n\nIf 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.\n\n```gradle\napply from: 干.file('someScript.gradle')\nsomePlugin {\n  configFile 干.file('somePluginConfig.xml')\n  configProp 干.prop('propfile', 'key')\n}\n```\n\n### Script plugins\n\nWhen 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')`:\n\n```gradle\n// build.gradle\next.pluginPass = 'supersecret'\next.keyFile = new File('keyFile')\napply from: 干.file('someScript.gradle')\n\n// someScript.gradle\nsomePlugin {\n  pass 干.proj('pluginPass', 'password for the keyFile')\n  // if the property isn't a String, you have to specify the class you expect\n  keyFile 干.proj(File.class, 'keyFile', 'location of the keyFile')\n}\n```\n\nIf 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).\n\n#### Script plugin gotchas\n\nScript 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.\n\n## Dev workflow\n\nTo change and test scripts before you push them up to GitHub, you can do this:\n\n```gradle\n// settings.gradle\nblowdryerSetup {\n  //github 'acme/blowdryer-acme', 'tag', 'v1.4.5'\n  devLocal '../path-to-local-blowdryer-acme'\n}\n```\n\nThe 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:\n\n- easily create/improve a plugin in one project using `devLocal '../blowdryer-acme'`\n- commit the script, then tag and push to `acme/blowdryer-acme`\n- because the `blowdryer-acme` version is immutably pinned **per-project**, you'll never break existing builds as you make changes\n- 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.\n\n### `repoSubfolder`\n\nIf you want your scripts to come from a different subfolder, you can change it:\n\n```gradle\n// settings.gradle\nblowdryerSetup {\n  repoSubfolder 'some/other/dir/but/why'\n  github 'acme/blowdryer-acme', 'tag', 'v1.4.5'\n}\n```\n\nThe 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.\n\n### Packaging as jar\n\n```gradle\n// settings.gradle\nblowdryerSetup {\n  localJar(file('/absolute/path/to/dependency.jar'))\n}\n```\n\nTo pull this jar from a maven repository, see [#21](https://github.com/diffplug/blowdryer/issues/21).\n\n## Plugin versions\n\nWe recommend that your `settings.gradle` should look like this:\n\n```gradle\nplugins {\n  id 'com.diffplug.blowdryerSetup' version '1.7.1'\n  id 'acme.java' version '1.0.0' apply false\n  id 'acme.kotlin' version '2.0.0' apply false\n}\nblowdryerSetup {\n  github('acme/blowdryer-acme', 'tag', 'v1.4.5')\n  setPluginsBlockTo {\n    it.file('plugin.versions')\n  }\n}\n```\n\nFirst 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).\n\nThe 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\n\n```gradle\n  id 'com.diffplug.blowdryerSetup' version '1.7.1'\n  id 'acme.java' version '1.0.0' apply false\n  id 'acme.kotlin' version '2.0.0' apply false\n```\n\nBlowdryer 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).\n\n### Updating plugin versions\n\nThe workflow goes like this:\n\n1. Enter `devLocal` mode (demonstrated [above](#dev-workflow))\n2. Update the `plugin.versions` file\n3. When you try to run your build, you will get an error\n  - \u003e settings.gradle plugins block has the wrong content. Add -DsetPluginVersions to overwrite\n4. Add `-DsetPluginVersions` to your command line\n5. You'll get another error\n - \u003e settings.gradle plugins block was written successfully. Plugin versions have been updated, try again.\n6. Now the plugins block will be up-to-date and your next build will succeed\n\n### Tweaking the `plugin.versions`\n\nIt 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:\n\n```gradle\nsetPluginsBlockTo {\n  it.file('plugin.versions')\n  it.file('kotlin-extras.versions')\n  it.add(\"  id 'special-plugin-for-just-this-project' version '1.0.0'\")\n  it.remove(\"   id 'acme.java' version '1.0.0' apply false\")\n  it.replace('1.7.20', '1.8.0') // update Kotlin version but only for this build\n}\n```\n\n### Compared to version catalogs\n\nRecent 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.\n\nDisappointingly, you can't use `libs.versions.toml` inside the `settings.gradle` file, which is exactly the place that we need it.\n\n## API Reference\n\nYou 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).\n\n```gradle\n// com.diffplug.blowdryer.干 is alias of com.diffplug.blowdryer.Blowdryer\nstatic File   干.file(String resource)\nstatic String 干.prop(String propFile, String key)\nstatic String 干.proj(Project proj, String String key, String description)\nstatic \u003cT\u003e T  干.proj(Project proj, Class\u003cT\u003e clazz, String String key, String description)\nstatic File   干.immutableUrl(String guaranteedImmutableUrl)\nstatic File   干.immutableUrl(String guaranteedImmutableUrl, String fileSuffix)\n  // 干.immutableUrl('https://foo.org/?file=blah.foo\u0026rev=7') returns a file which ends in `.foo-rev-7`\n  // 干.immutableUrl('https://foo.org/?file=blah.foo\u0026rev=7', '.foo') returns a file which ends in `.foo`\n```\n\n- [javadoc `Blowdryer`](https://javadoc.io/doc/com.diffplug/blowdryer/1.7.0/com/diffplug/blowdryer/Blowdryer.html)\n- [javadoc `BlowdryerSetup`](https://javadoc.io/doc/com.diffplug/blowdryer/1.7.0/com/diffplug/blowdryer/BlowdryerSetup.html)\n- [javadoc `BlowdryerSetup.PluginsBlock`](https://javadoc.io/doc/com.diffplug/blowdryer/latest/com/diffplug/blowdryer/BlowdryerSetup.PluginsBlock.html)\n\nIf 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.\n\n### Using with Kotlin\n\nThe 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.\n\n```kotlin\n// settings.gradle.kts\nplugins {\n  id(\"com.diffplug.blowdryerSetup\") version \"1.7.1\"\n}\nimport com.diffplug.blowdryer.BlowdryerSetup\nimport com.diffplug.blowdryer.BlowdryerSetup.GitAnchorType\nconfigure\u003cBlowdryerSetup\u003e {\n  github(\"acme/blowdryer-acme\", GitAnchorType.TAG, \"v1.4.5\")\n  //devLocal(\"../path-to-local-blowdryer-acme\")\n}\n\n// inside settings.gradle.kts, build.gradle.kts, or any-script.gradle.kts\nimport com.diffplug.blowdryer.干 // or .Blowdryer\n\napply(from = 干.file(\"script.gradle.kts\"))\nsomePlugin {\n  configFile 干.file(\"somePluginConfig.xml\")\n  configProp 干.prop(\"propfile\", \"key\")\n  pass       干.proj(project, \"pluginPass\", \"password for the keyFile\")\n  keyFile    干.proj(project, File.class, \"keyFile\", \"location of the keyFile\")\n}\n```\n\n\u003ca name=\"setup-with-something-besides-github\"\u003e\u003c/a\u003e\n\n### Other packaging options\n\n[`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.\n\nWhen 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:\n\n```java\n//blowdryer {\n//  github 'acme/blowdryer-acme', 'tag', 'v1.4.5'\npublic GitHub github(String repoOrg, GitAnchorType anchorType, String anchor) {\n  String root = \"https://raw.githubusercontent.com/\" + repoOrg + \"/\" + anchor + \"/\" + repoSubfolder + \"/\";\n  Blowdryer.setResourcePlugin(resource -\u003e root + resource);\n  return \u003cfluent_configurator_for_optional_auth_token\u003e;\n}\n```\n\nIf 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`.\n\n```gradle\nblowdryerSetup {\n  experimental { source -\u003e 'https://someImmutableUrlScheme/' + source }\n}\n```\n\n## In the wild\n\nHere are resource repositories in the wild (PRs welcome for others!)\n\n- https://github.com/diffplug/blowdryer-diffplug\n- https://github.com/mytakedotorg/blowdryer-mtdo\n\n## Blowdryer for [gulp](https://gulpjs.com/), etc.\n\nIt 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!\n\n## Requirements\n\nRequires Java 8+ and Graadle 6.8+.\n\n## Acknowledgements\n\n\u003c!---freshmark /version --\u003e\n\n- 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).\n- Thanks to [Sergey Sklarov](https://github.com/bHacklv) for implementing [Bitbucket support](https://github.com/diffplug/blowdryer/pull/23).\n- Thanks to [Chris Serra](https://github.com/chris-serra) for implementing [local jar support](https://github.com/diffplug/blowdryer/pull/20).\n- 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.\n- [Gradle](https://gradle.com/) is *so* good.\n- Maintained by [DiffPlug](https://www.diffplug.com/).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiffplug%2Fblowdryer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdiffplug%2Fblowdryer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiffplug%2Fblowdryer/lists"}