Downloading files with Gradle caching a.k.a. is it safe to temporarily overwrite the repository configuration?


#1

Use case
I want to download arbitrary files during build execution because not all of my build dependencies are available in a structured repository and I also need to setup some tools in the project for development.

As some of those files are large I want Gradle to cache them outside of the project directory so they don’t have to be re-downloaded.

Proposed solution
I can set up an Ivy repository with a custom pattern so Gradle downloads my files using it’s dependency resolution and caching while ignoring the missing Ivy metadata. As I don’t want to clutter my repository configuration with many fake repositories that slow down my build I came up with a solution that temporarily overwrites the repository configuration.

/**
 * Downloads the specified file using Gradle's resolution and caching strategy.
 *
 * Note that this method resolves and downloads the file immediately. Avoid calling this during configuration phase,
 * e.g. by wrapping it into a closure.
 *
 * Implementation notes:
 * This configures a custom Ivy repository for the supplied URL and temporarily replaces the existing project repositories.
 *
 * @param downloadUrl the URL of the file to download
 * @param changing specifies if up-to-date checks should be performed; set this to false if you are sure that the file never changes
 * @return the downloaded and cached file
 */
File downloadDependency(String downloadUrl, boolean changing = true) {
    logger.info("Resolving custom dependency: {}", downloadUrl)

    URL u = new URL(downloadUrl)
    String group = u.getHost().split('\\.').reverse().join('.')
    String name = u.getFile()

    Dependency dependency = dependencies.create([group: group, name: name])
    dependency.changing = changing
    Configuration configuration = project.configurations.detachedConfiguration(dependency)
    configuration.transitive = false
    configuration.resolutionStrategy.cacheChangingModulesFor 0, 'seconds'

    List<ArtifactRepository> savedRepositories = new ArrayList<>()
    savedRepositories.addAll(project.repositories)
    try {
        project.repositories.clear()

        project.repositories.ivy {
            url "${u.protocol}://${u.host}"
            layout 'pattern', {
                artifact '[artifact]'
                ivy 'ignore-ivy-file.xml'
            }
        }
        return configuration.resolve().iterator().next()
    } finally {
        project.repositories.clear()
        project.repositories.addAll(savedRepositories)
    }
}

Questions

  1. Is it safe to temporarily overwrite the configured repositories? Could there be any side-effects?
  2. Are there better solutions to my problem?

(James Justinic) #2

Plugins like the Gradle Node Plugin have been doing this for a couple years. This works as long as your projects are decoupled (you could also break this on a pre-4.0 version by resolving a configuration in a ParallelizableTask with intra-project parallelism enabled). In the execution phase, only one task is running in a project, or if using the Worker API, there’s no mutable state.

You can absolutely still break this if your projects are coupled in a multi-project build. If a user creates a task that resolves a configuration from a different project, the task in the other project can try to resolve the configuration at the same time as the task in the same project has replaced the repositories. The other task will fail.