Custom Gradle Build System for Handling Android Submodules as Resources

How can I make the task depend on minifyReleaseWithR8? I tried

tasks {
    register<Copy>("copyExtension") {
        dependsOn("minifyReleaseWithR8")

        from(layout.buildDirectory.get().file("intermediates/dex/release/minifyReleaseWithR8/classes.dex"))
        into(layout.buildDirectory.get().dir("revanced").dir(Path(name).parent.pathString))

        rename("classes.dex", Path(name).fileName.pathString)
    }
}

but I get:

If I don’t preserve the target folder is cleared but what if the producer needs this folder? The producer only needs to not clobber the target file in that directory and not the entire directory.
I used a Copy task like you suggested.

I would have quickly made the changes and show it, but you always send screenshots of code

I assumed it would be easier for you with syntax highlighting, but I’ll send code blocks from now on.

Better use paths of task properties where possible.

How can I use a tasks output? I believe named(taskName).outputs works.

How can I make the task depend on minifyReleaseWithR8 ?

Exactly how you did it if the task is not registered yet.
But still, I’d highly recommend to use a Sync task without preserve, not a Copy task.

Also, do not use layout.buildDirectory.get(). buildDirectory is not a constant, it can change. By get()ing it, you use the value configured right now. Instead just remove the get() like from(layout.buildDirectory.file("intermediates/dex/release/minifyReleaseWithR8/classes.dex")) and into(layout.buildDirectory.dir("revanced${Path(name).parent.pathString}")).

but I get:

Also this is text.
Same statement regarding screenshots.
Never share screenshots of code or text unless the screenshots provides more information than the pure text, which is not the case here.

The error is not coming from the code you showed, but probably from how you define the srcDir, so no idea what you did there now.

If I don’t preserve the target folder is cleared but what if the producer needs this folder?

Of course it is cleared, that’s the sense of Sync.
It is like Copy but then removes everything it did not copy there itself, resulting in exactly what you want and need wihtout the risk of stale files being in there.

The producer only needs to not clobber the target file in that directory and not the entire directory.

An output directory should always be exclusive for a task anyway.
If any other task also puts things into the same output directory, that is called overlapping outputs and is pretty bad for various reasons and should always be avoided anyway.

I assumed it would be easier for you with syntax highlighting, but I’ll send code blocks from now on.

If you use code-blocks properly, the syntax highlighting is usually fully sufficient.
And if not in a case, I could still copy out the text and paste it to an IDE. :slight_smile:

How can I use a tasks output? I believe named(taskName).outputs works.

Ah, forget it in this case. as the relevant task is not registered yet, it would be a bit cumbersome and have other drawbacks. So in this specific case maybe indeed use the hard-coded paths.

1 Like

Now that you mention it, I am using the Dark Reader extension which seemingly disabled syntax highlighting, so I assumed there was none for code blocks.

Seems like “extensions” was something already defined in the current context and since I named my variable “extensions” the first definition was shading it. This is why naming is important.

Right now I have this in my producer:


val extensionPath = Path("extensions/shared.rve")

val syncExtensionTask = tasks.register<Sync>("syncExtension") {
    dependsOn("minifyReleaseWithR8")

    from(layout.buildDirectory.file("intermediates/dex/release/minifyReleaseWithR8/classes.dex"))
    into(layout.buildDirectory.dir("revanced/${extensionPath.parent.pathString}"))

    rename("classes.dex", extensionPath.fileName.pathString)
}

val extensionDependencyScopeConfiguration by configurations.creating {
    isCanBeConsumed = false
    isCanBeResolved = false
    isVisible = false
}

val extensionConfiguration by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
    isVisible = false

    extendsFrom(extensionDependencyScopeConfiguration)
}

artifacts.add(
    extensionConfiguration.name,
    layout.buildDirectory.dir("revanced"),
) {
    builtBy(syncExtensionTask)
}

and this in my consumer:

val extensionsDependencyScopeConfiguration by configurations.dependencyScope("extensionDependencyScope")
val extensionsConfiguration = configurations.resolvable("extensionConfiguration") {
    extendsFrom(extensionsDependencyScopeConfiguration)
}

sourceSets.main {
    resources.srcDir(extensionsConfiguration)
}
dependencies {
    extensionsDependencyScopeConfiguration(
        project(
            mapOf(
                "path" to ":extensions:shared",
                "configuration" to "extensionConfiguration",
            ),
        ),
    )
}

This seems to work as expected. The artifact is being added where I need it. The way it is now though, I am unable to add new producers and automatically share their artifacts. I would need to change the consumer build script to do that. Each producer would have a configuration called extensionConfiguration so I believe I need to iterate over all producers and depend on their configuration? Something like:

dependencies {
     producers.forEach { producer ->
       extensionsDependencyScopeConfiguration(
          project(
              mapOf(
                  "path" to producer,
                  "configuration" to "extensionConfiguration",
              ),
          ),
      )
    }
}

To add all the consumers automatically and depend on them I currently am doing this in my settings.gradle.kts file:

include(":patches")

file("extensions").listFiles()?.forEach { extension ->
    include(":extensions:${extension.name}")
    extension.listFiles()?.forEach { subExtension ->
        if (subExtension.resolve("build.gradle.kts").exists()) {
            include(":extensions:${extension.name}:${subExtension.name}")
        }
    }
}

As you can see, this is quite ugly. Is there any better approach to include projects automatically under a specific directory? My supplier projects may also depend on nested projects, so the current approach is not viable without recursively iterating through all nested directories, including the projects.

Once this is sorted out, I’ll consider the current forum post solved and proceed with a new one regarding setting up a plugin so that consumers do not have to specify the same blocks every time.

Right now I have this in my producer

Again, remove the extensionDependencyScopeConfiguration in your producer, it is pointless. :slight_smile:

and this in my consumer

I would either consistently use the new convenience methods, so configurations.consumable(...) for extensionConfiguration in the producer, or the manual isCanBe... way also in the consumer. Just for consistency. :slight_smile:

I would need to change the consumer build script to do that.

val extensionProjects = project(":extensions").subprojects
dependencies {
    extensionProjects.forEach {
        extensionsDependencyScopeConfiguration(
            project(
                mapOf(
                    "path" to it.path,
                    "configuration" to "extensionConfiguration",
                ),
            ),
        )
    }
}

As you can see, this is quite ugly. Is there any better approach to include projects automatically under a specific directory?

Maybe there is some 3rd party settings plugin that can do it for you, hiding the ugliness.
Or maybe there is some Kotlin sugar that makes it less ugly to you.
Ugly always is very subjective.
Maybe you do like this one better:

fileTree("extensions")
    .matching {
        include("*/build.gradle.kts")
        include("*/*/build.gradle.kts")
    }
    .forEach {
        include(it.relativeTo(settingsDir).toPath().joinToString(":"))
    }

Or if you want any depth not only first and second level, then just include("**/build.gradle.kts") instead of the two includes in the matching.

It looks to work well.

This seems to return all sub-projects, not just the direct children. So, if an extension project depends on another sub-project, the consumer project should not depend on it. Is there any way to get the direct children, or would I need to filter:

val extensionsProject = project(":extensions")
val extensionProjects = extensionsProject.subprojects.filter {
    it.parent == extensionsProject
}

This seems to return all sub-projects, not just the direct children.

Exactly, it is .subprojects, not .childProjects.values :wink: