Custom (non-Java) platform configuration

In my build I have a subproject that downloads and unpacks a bunch of artifacts, specified with a configuration like this:

val languageLibs by configurations.creating {
  isCanBeConsumed = false
  resolutionStrategy.activateDependencyLocking()
}

dependencies {
  languageLibs("com.example:lib1:2022.2.+")
  languageLibs("com.example:lib2:1.87.+")
}

I am using dynamic versions with dependency locking.

I add an output directory which contains unpacked languageLibs to the default configuration:

artifacts.add("default", outputDirectory) {
    builtBy(extractLanguageLibs)
}

I want to provide an alternative configuration where I only export the versions of the dependencies. As far as I understand, this is analogous to BOM/platform dependencies in Maven/Java.

I’m doing it as follows:

configurations.consumable("platform") {
    extendsFrom(languageLibs)

    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category::class.java, Category.REGULAR_PLATFORM))
    }
}

But the platform configuration does not pick up the constraints (dependency locks) of languageLibs and when a consumer uses this configuration, the dynamic versions get resolved differently.

How can I make it so that they resolve to the same version? shouldResolveConsistentlyWith(...) does not help, dependencyConstraints.addAll(languageLibs.allDependencyConstraints) doesn’t work either.

This is the workaround I implemented:

// 'platform' configuration contains individual libraries as transitive dependencies, useful as a source of dependencies
// when publishing an artifact that depends on the libraries.
configurations.consumable("platform") {
    extendsFrom(languageLibs)

    // This complex piece of code propagates locked dependencies as constraints to this configuration.
    // It is necessary because `extendsFrom` above does not propagate the constraints.
    //
    // I have not found an easier way to do this. I'm not sure whether this approach will cause the configuration
    // to be resolved too early or have other side effects. We'll see.
    //
    // The way it works is it resolves the `languageLibs` configuration and turns the first-level resolved dependencies
    // into dependency constraints.
    //
    // It probably should recurse into the transitive dependencies, but it currently doesn't.
    dependencyConstraints.addAllLater(languageLibs.incoming.resolutionResult
            .rootComponent
            .map {
                it.dependencies
                        .filterIsInstance<ResolvedDependencyResult>()
                        .map { it.selected.id }
                        .filterIsInstance<ModuleComponentIdentifier>()
                        .map {
                            project.dependencies.constraints.create("${it.group}:${it.module}:${it.version}!!")
                        }
            }
    )
}