```* What went wrong: Could not determine the depe...
# community-support
h
Copy code
* What went wrong:
Could not determine the dependencies of task ':projectVersionExport'.
> Could not resolve all dependencies for configuration ':projectVersionExportConfig'.
   > Could not resolve project :test-kit.
     Required by:
         root project :
      > A dependency was declared on configuration 'projectVersionExportSubConfig' of 'project :test-kit' but no variant with that configuration name exists.
Is there any way to find out what added the dependency for this configuration? Because it seems as if Gradle added the dependency by itself.
v
The whole internet does not know anything about
projectVersionExportSubConfig
or
projectVersionExport
, so it is quite unlikely that something outside your buildscript is adding that, especially not Gradle itself. šŸ™‚ Also depending on a named configuration in another project is considered bad practice, which also supports that it is quite unlikely that Gradle added it by itself. Check the dependency declarations in your root project's build script. If you don't find it, maybe one of your plugins is doing that, you could for example try to remove one by one to see where it comes from.
h
Copy code
val subProjectTaskConfig = AbramsProjectVersionExport.createSubProjectTaskConfiguration(project)
val subProjectTask = AbramsProjectVersionExport.registerSubProjectTask(project)

artifacts {
    add(subProjectTaskConfig.name, subProjectTask)
}

if (project == project.rootProject) {
    val rootProjectConfig = AbramsProjectVersionExport.createRootProjectTaskConfiguration(project)
    val subProjects = if (project.subprojects.isEmpty()) listOf(project) else project.subprojects
    subProjects.forEach { subProject ->
        val depProvider = project.provider {
            val config = subProject.configurations.findByName(SUB_EXPORT_TASK_CONFIG)
            if (config != null) {
                project.dependencies.project(
                    mapOf(
                        "path" to subProject.path,
                        "configuration" to SUB_EXPORT_TASK_CONFIG,
                    ),
                )
            } else {
                null
            }
        }

        depProvider.orNull?.let {
            project.dependencies.addProvider(rootProjectConfig.name, project.provider { it })
        }
    }
    AbramsProjectVersionExport.registerRootProjectTask(rootProjectConfig, project)
}
Is the project-version-export Plugin. It is added to all projects - except test-kit. I tried to avoid adding the dependency to the root project if it doesn't exist in the child project... But it seems I'm missing something. My problem is I cannot understand where or how the configuration is formed. It'd be great to know how the dependency was added. Is there a way to get Gradle to give more insight into the dependency / resolution process?
Copy code
package zone.abrams.gradle.plugins.convention.internal

import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFile
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskProvider

class AbramsProjectVersionExport {
    companion object {
        const val SUB_EXPORT_TASK_CONFIG: String = "projectVersionExportSubConfig"

        private const val SUB_EXPORT_TASK_NAME: String = "projectVersionExportSub"
        private const val SUB_EXPORT_FILE_NAME: String = "projectVersionExportSub.env"
        private const val ROOT_EXPORT_FILE_NAME: String = "projectVersionExport.env"
        private const val ROOT_EXPORT_TASK_CONFIG: String = "projectVersionExportConfig"
        private const val ROOT_EXPORT_TASK_NAME: String = "projectVersionExport"
        private const val EXPORT_TASK_GROUP: String = "Abrams Build"
        private const val EXPORT_TASK_DESCRIPTION: String = "Export the project version to a bash env file."

        fun createSubProjectTaskConfiguration(project: Project): NamedDomainObjectProvider<Configuration> =
            project.configurations.register(SUB_EXPORT_TASK_CONFIG) {
                isCanBeResolved = false
                isTransitive = false
                description = "Collects version export files from subprojects"
            }

        fun createRootProjectTaskConfiguration(project: Project): NamedDomainObjectProvider<Configuration> =
            project.configurations.register(ROOT_EXPORT_TASK_CONFIG) {
                isCanBeConsumed = false
                isTransitive = false
                description = "Aggregates version export files from subprojects"
            }

        fun registerRootProjectTask(
            configuration: NamedDomainObjectProvider<Configuration>,
            project: Project,
        ): TaskProvider<Task> = project.tasks.register(ROOT_EXPORT_TASK_NAME) {
            val sharedFilesProvider: Provider<FileCollection> = configuration.map { it as FileCollection }
            val versionExportFileProvider: Provider<RegularFile> =
                project.layout.buildDirectory.file(ROOT_EXPORT_FILE_NAME)

            inputs.files(sharedFilesProvider)
            outputs.file(versionExportFileProvider)

            group = EXPORT_TASK_GROUP
            description = EXPORT_TASK_DESCRIPTION

            doFirst {
                val outputFile = versionExportFileProvider.get().asFile
                val content = sharedFilesProvider
                    .get()
                    .filter { it.exists() && it.isFile }
                    .joinToString(separator = "\n") { file -> file.readText() }

                outputFile.writeText(content)
                logger.lifecycle("Written version file export: '{}'", outputFile.absolutePath)
            }
        }

        fun registerSubProjectTask(project: Project): TaskProvider<Task> =
            project.tasks.register(SUB_EXPORT_TASK_NAME) {
                inputs.property("projectName", project.provider { project.name })
                inputs.property("projectVersion", project.provider { project.version })

                val versionExportFile = project.layout.buildDirectory.file(SUB_EXPORT_FILE_NAME)
                outputs.file(versionExportFile)

                group = EXPORT_TASK_GROUP
                description = EXPORT_TASK_DESCRIPTION

                doLast {
                    versionExportFile.get().asFile.writeText(
                        "${inputs.properties["projectName"]}=${inputs.properties["projectVersion"]}",
                    )
                }
            }
    }
}
Just for full reference the tasks
v
You must not reach into other project's models like you do for example with
subProject.configurations
, that is almost as bad as doing cross-project configuration. Either list the projects you want to depend on manually, or if that is not an option you can for example make sure that all projects - including
test-kit
- have such a variant even if it does not contain any artifacts, or if that is not an option maybe an artifact view would also be a possibility, as an artifact view on which you select different attributes is allowed to not find an artifact for all dependencies but only returns the ones that have it. As I just mentioned, depending on a configuration of another project by name is also discouraged bad practice and you should use attribute-aware resolving like documented at https://docs.gradle.org/current/userguide/how_to_share_outputs_between_projects.html.
h
It is an convention plugin, so the projects are unknown. I will rewrite it with attribute aware resolving. The subproject.configurations reach was an attempt to only include the correct projects, as I wasn't able to figure out how to get Gradle to tell me what or how the configuration was formed.
šŸ‘Œ 1
v
From a cursory look, you also might consider a completely different approach
Currently you write one file per project to disk with the information just to share it with the root project that merges them into one file if I have seen that correctly.
h
Yes, exactly. The root task then depends on the configuration of all projects version export files
v
You might consider using a shared build service instead. All projects that have the convention plugin applied can at configuration time write that information to the shared build service, and the task in the root project can then take the value from the shared build service. Then you also save the disk IO to create one file per project and read them again in the root project task.
Should work fine I think and should be easier to setup and less prone to issues like that.
h
Copy code
abstract class AbramsProjectVersionExportBuildService :
    BuildService<BuildServiceParameters.None>,
    AutoCloseable {

    private val versions = ConcurrentHashMap<String, String>()

    fun registerProject(name: String, version: String) {
        versions[name] = version
    }

    fun getEntries(): Map<String, String> = versions.toSortedMap()

    override fun close() {
        // No-op
    }
}
abstract class AbramsProjectVersionExportRootTask : DefaultTask() {
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @get:Internal
    abstract val buildService: Property<AbramsProjectVersionExportBuildService>

    @TaskAction
    fun writeEnvFile() {
        val entries = buildService.get().getEntries()
        val content = entries.entries.joinToString("\n") { (name, version) -> "$name=$version" }
        outputFile.get().asFile.writeText(content)
        logger.lifecycle("Version export written to: ${outputFile.get().asFile.absolutePath}")
    }
}
abstract class AbramsProjectVersionExportSubTask : DefaultTask() {
    @get:Input
    abstract val projectName: Property<String>

    @get:Input
    abstract val projectVersion: Property<String>

    @get:Internal
    abstract val buildService: Property<AbramsProjectVersionExportBuildService>

    @TaskAction
    fun export() {
        buildService.get().registerProject(projectName.get(), projectVersion.get())
    }
}
as classes. convention-plugin code based on classes:
Copy code
val serviceProvider = project.gradle.sharedServices.registerIfAbsent(
    "abramsProjectVersionExportBuildService",
    AbramsProjectVersionExportBuildService::class.java,
) {}

if (project == project.rootProject) {
    project.tasks.register("projectVersionExport", AbramsProjectVersionExportRootTask::class.java) {
        group = "Abrams Build"
        description = "Export all registered project versions to a bash env file."

        outputFile.set(project.layout.buildDirectory.file("projectVersionExport.env"))
        buildService.set(serviceProvider)

        project.subprojects.forEach { subProject ->
            subProject.tasks.findByName("projectVersionExportSub")?.let {
                dependsOn(it)
            }
        }
    }
} else {
    project.tasks.register("projectVersionExportSub", AbramsProjectVersionExportSubTask::class.java) {
        group = "Abrams Build"
        description = "Register project version in the shared export build service."

        projectName.set(project.name)
        projectVersion.set(project.version.toString())
        buildService.set(serviceProvider)
    }
}
That does work. Just to make sure that I didn't add yet another brainfart... projectVersionExport depends on other tasks - as the subprojects have to run projectVersionExportSub. Is that safe or is that yet another booby trap in the future?
v
• the
buildService
should not be
@Internal
but
@ServiceReference
, or alternatively you also need to manually declare
usesService
for the tasks that use the service. • With the
@ServiceReference
you then also do not need to manually wire the service in, but Gradle will automatically inject it • The
AbramsProjectVersionExportSubTask
should not exist at all, just add those values to the build service when you configure the projects in your convention plugin • Yes, you still evilly reach into the other projects' model by using
subProject.tasks
, that is just as bad as using
subProject.configurations
, luckily if you did like I originally suggested and now clarified, there should not be any task dependency necessary anyway, but just by applying the plugin and thus configuring the projects the build service should already have the necessary values •
tasks.findByName
is always a bad idea, even within the same project, as you break task-configuration avoidance by that, causing the task to always be realized and configured, no matter whether it is going to be executed or not šŸ™‚
h
Thanks for the spanking šŸ˜›
v
Well, you asked for it. šŸ˜„
šŸ™Œ 1