Hi, maybe someone can share their wisdom with me. ...
# community-support
h
Hi, maybe someone can share their wisdom with me. For a multi-project Gradle setup, where each subproject has its version, I was trying to write a small task to export the version information of each subproject. (Task is set up with an extension for the targetFile in an plugin)... Curiosity got me and I wanted to know if there is perhaps a better way to do it? Main goal is just to have an easy "bash" env style file where each line consists of <project_name>=<version> . We have multiple projects, thus the plugin. We have a custom gradle wrapper distribution, would it maybe be better to pack it inside an .init.gradle.kts? Seems like the simple task clashes with the configuration cache, too.
Copy code
abstract class VersionExportTask : DefaultTask() {
    @get:OutputFile
    abstract val targetFile: RegularFileProperty

    @TaskAction
    fun export() {
        if (project != project.rootProject) {
            return
        }

        val file = targetFile.get().asFile
        file.parentFile.mkdirs()

        project.subprojects.forEach {
            file.writeText("${it.name}=${it.version}")
        }
    }
}
v
Even without CC, accessing
project
at task execution time is deprecated. And with CC you can of course not do this, as the configuration model is not available at task execution time. So, no, this is not "so simple" as one might assume. 😄
You have to gather the information at configuration time and then just write the file at execution time. But of course you should not reach into other projects models at all, as that immediately introduces project coupling, which works against some more sophisticated Gradle features and optimizations like isolated projects, parallel configuration, ...
I guess you need to have a task in each project that generates its own line, have a consumable configuration that publishes that file, and then have in one project dependencies on all other projects, gathering those files and combing them into one or something along those lines. 🤷‍♂️
e
hmm. could you create a configuration that depends on all subprojects, try to resolve it, filter for local project component identifiers, and print their versions?
👌 1
v
Ah, yeah, that might work maybe.
t
Or have a task in each project add its version to a build service, and the task at the root depends on (or just mustRunAfter) all those tasks and writes to a file (I suppose the build service could also directly write to file at the end of the build)
h
Thanks for the input, I'm trying to process it ^^ Regarding the creating a configuration... https://docs.gradle.org/current/userguide/variant_aware_resolution.html#seven-variant-aware-resolution I assume I'm on the right track regarding the idea (still very new to gradle)? I'm not sure I understood it correctly... https://docs.gradle.org/current/userguide/build_services.html I'll try the approach of Thomas Broyer. The configuration approach seems interesting, but complicated. Thanks to everyone for the input 🙂
v
That will not work. You must not depend on tasks in other projects directly. Same as above
h
I'm thinking I'm getting somewhere...
Copy code
fun createConfiguration(): Configuration {
    if (project != project.rootProject) {
        val exportProjectVersionConfiguration: Configuration by configurations.creating {
            isCanBeResolved = false
        }
        return exportProjectVersionConfiguration
    }
    val exportProjectVersionConfiguration: Configuration by configurations.creating {
        isCanBeConsumed = false
    }
    return exportProjectVersionConfiguration
}

fun createTask(configuration: Configuration): TaskProvider<Task> {
    if (project != project.rootProject) {
        return tasks.register("exportProjectVersion") {
            val versionExportFile = layout.buildDirectory.file("exportProjectVersion.txt")
            outputs.file(versionExportFile)

            doLast {
                versionExportFile.get().asFile.writeText("${project.name}=${project.version}")
            }
        }
    }
    return tasks.register("exportProjectVersion") {
        val sharedFiles: FileCollection = configuration
        inputs.files(sharedFiles)
        doFirst {
            val content = sharedFiles.joinToString(separator = "\n", transform = { file -> file.readText() })
            logger.lifecycle("Shared file contains the text: '{}'", content)
        }
    }
}

val exportProjectVersionConfiguration = createConfiguration()
val exportProjectVersionTask = createTask(exportProjectVersionConfiguration)

if (project != project.rootProject) {
    artifacts {
        add(exportProjectVersionConfiguration.name, exportProjectVersionTask)
    }
} else {
    dependencies {
        project.subprojects.forEach {
            exportProjectVersionConfiguration(project(it.path, exportProjectVersionConfiguration.name))
        }
    }
}
I just stuffed this into a convention plugin script, as this is configured in all relevant projects and I could apply it at the root level. It works and does the job... https://docs.gradle.org/8.13/userguide/upgrading_version_7.html#task_project leads to https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution In
createTask
I still call
project.name
and
project.version
which is a nono as Vampire already explained. That's the missing piece of the puzzle for me...
could you create a configuration that depends on all subprojects, try to resolve it, filter for local project component identifiers, and print their versions
If I'm not mistaken, the approach means that I can "fish" somehow out of the configuration the project component identifiers and the version... Which would imply that I don't access the projects anymore, as I just access the configuration. Has someone maybe a code snippet, Gradle link or sth like that or an example? Thanks in advance for your patience!
v
Yes, that snippet is basically the publish through file approach, short of getting rid of the
project.
access at execution time. Besides that you also missed to declare them as inputs so your up-to-date checks would be broken. Use
Property<String>
for name and version or one for the whole line, declare it as input property and query it at execution time, and the approach should work I think.
For that other approach which might be better if it works as it does not need to write and read files, you probably need the
ResolutionResult
API from https://docs.gradle.org/current/userguide/dependency_resolution_basics.html.
h
Okay, good to know. But how do I pass it exactly in? I'd assume it has to be an Provider. That's the part I can't wrap my head around... Or am I completely overthinking it and it is
Copy code
inputs.property("projectName", project.name)
inputs.property("projectVersion", project.version)
it appears to be too easy to be true. XD
v
Yes, too easy and thus wrong 😄
A
Property<String>
IS a
Provider<String>
, so where is the problem? You create a property, use a
provider { }
in which you get name / version so it is read as late as possible, give it to
inputs.property
and
get()
it at execution time. Which part do you have problems with?
If you do it like you showed it will at least be considered as inputs, but you still do not have it available at execution time, and you read the values the moment that line is executed so might changes done later in the configuration phase.
h
Oh lord... You're completely right. I completely missed the obvious point that the provider will lead to an delegation... hmkay my bad Thanks for pointing out the elephant in the room.
👌 1
😵‍💫