Handling projects coupled at configuration time


(Matt Khan) #1

tl;dr - Is it possible to control the order in which build scripts are evaluated?

long version.

I have n projects in a simple hierarchy

my-framework
   my-framework-core
   my-framework-api
my-app
   my-app-core
   my-app-config

Each project does a certain amount of configuration in a ‘projectsEvaluated’ callback. The configuration of each child depends on the configuration done by its parent. This works fine because the callbacks are executed in the order they are registered (which is the order they are listed).

In certain circumstances, I need the execution order to be

my-framework
my-app
   my-framework-core
   my-framework-api
   my-app-core
   my-app-config

i.e. parents then children

In those circumstances I will be operating off a build declared by a different settings.gradle.

Is this possible?


(Peter Niederwieser) #2

There is ‘Project.evaluationDependsOn()’, but I recommend to use it sparingly. Maybe you can find a solution that doesn’t depend on the order of ‘projectsEvaluated’ callbacks.


(Matt Khan) #3

In this case the logic the root project is responsible for setting ‘project.group’ from a static prefix + a suffix that is set by the build script via a custom object exposed as an extension.

A container is exposed to the child projects which lets the build declare “internal dependencies”, i.e. a dependency that is produced by our build but which may or may not be in the current project. The plugin turns each entry into either an external module dependency or a project dependency based on whether a project is found in the build that is identified to produce that artifact. To do this, it needs to know the group and so the code that sets the group must have run by now.

Now I write this out, it sounds like this is really a good fit for convention mapping (which I don’t use so far) as then I’d just get the right value when I call the getter (assuming one can add a convention mapping to a project which appears not to be the case given that DefaultProject is annotated by ‘@NoConventionMapping’).

The reason to do this is so that the generated idea project uses a module dependency when the producer of the artifact is present.


(Peter Niederwieser) #4

One solution might be to only offer a “group extension” for the root project, and have it eagerly set ‘project.group’ for all projects. If each project has its own “group extension”, you might have to defer the decision whether to resolve an internal dependency to a project or external dependency, as you can’t guarantee that the involved “group extensions” have already been evaluated when you hit a project’s internal dependency container.


(Matt Khan) #5

I think what I need is to trigger some logic after the last project has been evaluated. Is the configuration phase always single threaded?


(Peter Niederwieser) #6

Currently it is. You can use ‘gradle.projectsEvaluated {}’.


(Matt Khan) #7

my latest implementation of this is based on a ‘projectsEvaluated’ block which does the following

project.gradle.projectsEvaluated {
            TreeSet<Project> projects = accumulateProjects(project.rootProject, [] as TreeSet<Project>)
            if (projects.last() == project) {
                projects.findAll{ it.extensions.findByName('internals') != null }.each { it.internals.resolve(it) }
            }
        }
      protected TreeSet<Project> accumulateProjects(Project project, TreeSet<Project> container) {
        container << project
        project.subprojects.each { accumulateProjects(it, container) }
        container
    }

I couldn’t find a simpler way to get the entire set of projects in the build.

The ‘internals’ extension referenced above is an extension that is modelled on ‘DefaultDependencyHandler’, it has a ‘methodMissing’ impl that just records the args that come through so it can replay them through the ‘DependencyHandler’ later.

def calls = []
    Object methodMissing(String name, Object args) {
        calls << [name, args]
        null
    }

The ‘resolve’ method is pretty much a cut and paste of how ‘DefaultDependencyHandler’ works. It unpacks the args into either a call to ‘DependencyHandler.add(config, notation, closure)’ or ‘DependencyHandler.add(config, notation)’ while still supporting the existing use case of n individual dependencies declared against a single configuration.

/**
     * Resolves each recorded call into either a module dependency or a project dependency based on whether the
     * dependency corresponds to a project declared in this build.
     * @param project the project to apply the deps to.
     */
    void resolve(Project project) {
        calls.each {
            resolve(valueOf(it[0]), it[1], project)
        }
    }
      /**
     * Resolves a single recorded call into either a project dependency or an internal dependency.
     * @param confName the conf.
     * @param args the args.
     * @param project the project to add dependency to.
     */
    void resolve(String confName, def args, Project project) {
        def conf = project.configurations.findByName(confName)
        if (conf == null) {
            throw new InvalidProjectException("Unable to resolve dependency [${args}], " +
                                                    "configuration ${confName} does not exist")
        } else {
            Object[] normalizedArgs = GUtil.collectionize(args)
            if (normalizedArgs.length == 2 && normalizedArgs[1] instanceof Closure) {
                resolveSingle(project, confName, normalizedArgs[0], (Closure) normalizedArgs[1])
            } else if (normalizedArgs.length == 1) {
                resolveSingle(project, confName, normalizedArgs[0], null)
            } else {
                normalizedArgs.each { notation ->
                    resolveSingle(project, confName, notation, null)
                }
            }
        }
    }
      /**
     * Resolves the args down to something to call {@link org.gradle.api.artifacts.dsl.DependencyHandler#add(java.lang.String, java.lang.Object)}
     * or {@link org.gradle.api.artifacts.dsl.DependencyHandler#add(java.lang.String, java.lang.Object, groovy.lang.Closure)}.
     * @param project the project.
     * @param confName the configuration name.
     * @param arg descriptor for a module dependency.
     * @param configurator an optional closure.
     */
    protected void resolveSingle(Project project, String confName, Object arg, Closure configurator) {
        final dep = project.dependencies.create(arg)
        Project depAsProject = asProject(dep, project.rootProject)
        if (depAsProject != null) {
            project.dependencies.add(confName, depAsProject, configurator)
        } else {
            project.dependencies.add(confName, arg, configurator)
        }
    }
      /**
     * Resolves the arg into a project dependency.
      * @param dependency the external dependency.
     * @param project the owning project.
     * @return a dependency that corresponds to the supplied arg.
     */
    Project asProject(Dependency dependency, Project project) {
        if (project.group == dependency.group && project.name == dependency.name) {
            project
        } else {
            def matches = project.subprojects.collect { asProject(dependency, it) }.flatten().findAll { it != null }
            if (matches.isEmpty()) {
                null
            } else {
                matches[0]
            }
        }
    }

I think most of the time was spent working out how that ‘dependencies{}’ actually works, plenty of black magic there!