Skip to content

Commit

Permalink
Add validation task infrastructure + java/kotlin version matcher (#1247)
Browse files Browse the repository at this point in the history
This introduces a new concept of `validateFoundryProject` as a lifecycle
task that can depend on a few common validations. The first one I'm
adding here is a new one to validate that a `.java_version` file (if
present) matches the `jdk` version defined in a version catalog, which
we wanna support to make some other tooling more automatic like renovate
and github actions.

Example of the .java_version file
https://github.com/square/moshi/pull/1941/files

<!--
  ⬆ Put your description above this! ⬆

  Please be descriptive and detailed.
  
Please read our [Contributing
Guidelines](https://github.com/tinyspeck/foundry/blob/main/.github/CONTRIBUTING.md)
and [Code of Conduct](https://slackhq.github.io/code-of-conduct).

Don't worry about deleting this, it's not visible in the PR!
-->
  • Loading branch information
ZacSweers authored Mar 10, 2025
1 parent df93808 commit 5be37dd
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Changelog
**Unreleased**
--------------

- Add optional `validate(Kotlin|Java)VersionMatches` tasks to keep files like `.java_version` synced with version catalogs.

0.24.11
-------

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,14 @@ internal constructor(
KotlinVersion.fromVersion(it)
}

/** Defines .kotlin_version. */
public val kotlinVersionFilePath: String?
get() = optionalStringProperty("foundry.kotlin.kotlin-version-file-path", blankIsNull = true)

/** Defines .java_version. */
public val javaVersionFilePath: String?
get() = optionalStringProperty("foundry.jvm.java-version-file-path", blankIsNull = true)

/** Defines a required vendor for JDK toolchains. */
public val jvmVendor: Provider<String>
get() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ import foundry.gradle.properties.sneakyNull
import foundry.gradle.stats.ModuleStatsTasks
import foundry.gradle.tasks.AndroidTestApksTask
import foundry.gradle.tasks.CoreBootstrapTask
import foundry.gradle.tasks.FoundryValidationTask
import foundry.gradle.tasks.GjfDownloadTask
import foundry.gradle.tasks.InstallCommitHooksTask
import foundry.gradle.tasks.KtLintDownloadTask
import foundry.gradle.tasks.KtfmtDownloadTask
import foundry.gradle.tasks.SortDependenciesDownloadTask
import foundry.gradle.tasks.ValidateVersionsMatch
import foundry.gradle.tasks.robolectric.UpdateRobolectricJarsTask
import foundry.gradle.testing.EmulatorWtfTests
import foundry.gradle.testing.RoborazziTests
Expand All @@ -49,6 +51,7 @@ import foundry.gradle.util.Thermals
import foundry.gradle.util.ThermalsData
import java.util.Locale
import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.configuration.BuildFeatures
Expand Down Expand Up @@ -164,6 +167,31 @@ internal class FoundryRootPlugin @Inject constructor(private val buildFeatures:
AndroidSourcesConfigurer.patchSdkSources(compileSdk, project, latestCompileSdkWithSources)
}
}

FoundryValidationTask.registerLifecycleTask(project)

foundryProperties.javaVersionFilePath?.let { javaVersionFilePath ->
foundryProperties.versions.jdk.getOrNull()?.let { catalogVersion ->
ValidateVersionsMatch.register(
project = project,
type = "javaVersion",
versionFilePath = javaVersionFilePath,
catalogVersion = catalogVersion.toString(),
foundryVersions = foundryProperties.versions,
)
}
}

foundryProperties.kotlinVersionFilePath?.let { javaVersionFilePath ->
ValidateVersionsMatch.register(
project = project,
type = "kotlinVersion",
versionFilePath = javaVersionFilePath,
catalogVersion = foundryProperties.versions.kotlin,
foundryVersions = foundryProperties.versions,
)
}

project.configureFoundryRootBuildscript(
foundryProperties.versions.jdk.asProvider(project.providers),
foundryProperties.jvmVendor.map(JvmVendorSpec::matching).orNull,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal class FoundryVersions(
private val libResolver: (String) -> Optional<String>,
bundleResolver: (String) -> Optional<Provider<ExternalModuleDependencyBundle>>,
val boms: Set<Provider<MinimalExternalModuleDependency>>,
val catalogName: String,
) {

/**
Expand All @@ -48,6 +49,7 @@ internal class FoundryVersions(
it.endsWith(".bom")
}
.mapTo(LinkedHashSet()) { catalog.findLibrary(it).get() },
catalogName = catalog.name,
)

// Have to use Optional because ConcurrentHashMap doesn't allow nulls for absence
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2025 Slack Technologies, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package foundry.gradle.tasks

import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.TaskProvider

/** Marker interface for Foundry validation tasks that can be depended on by type. */
public interface FoundryValidationTask : Task {
public companion object {
internal fun registerLifecycleTask(project: Project): TaskProvider<out Task> {
return LifecycleTask.register(project, "validateFoundryProject") {
dependsOn(project.tasks.withType(FoundryValidationTask::class.java))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2025 Slack Technologies, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package foundry.gradle.tasks

import foundry.gradle.register
import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.UntrackedTask

@UntrackedTask(because = "Just a lifecycle task")
internal abstract class LifecycleTask : DefaultTask() {
companion object {
internal fun register(
project: Project,
name: String,
group: String = "foundry",
action: Action<LifecycleTask> = Action {},
): TaskProvider<LifecycleTask> {
return project.tasks.register<LifecycleTask>(name) {
this.group = group
action.execute(this)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2025 Slack Technologies, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package foundry.gradle.tasks

import foundry.gradle.FoundryVersions
import foundry.gradle.capitalizeUS
import foundry.gradle.register
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction

/**
* A Gradle task that validates whether version specified in a given [versionFile] (for example:
* `.java_version`) matches the expected version defined in a version catalog ([catalogVersion]). If
* the versions match, an [outputFile] is generated with the status "valid". If they do not match,
* the task fails with a descriptive error message.
*
* This is useful for projects that define the version in multiple places, as some tools like
* Renovate and GitHub actions work well with a `.java_version` file and some caching mechanisms
* want a single manifest file.
*/
@CacheableTask
public abstract class ValidateVersionsMatch : DefaultTask(), FoundryValidationTask {
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
public abstract val versionFile: RegularFileProperty

@get:Input public abstract val versionFileRelativePath: Property<String>
@get:Input public abstract val catalogName: Property<String>
@get:Input public abstract val catalogVersion: Property<String>

@get:OutputFile public abstract val outputFile: RegularFileProperty

init {
group = "foundry"
}

@TaskAction
internal fun validate() {
val fileVersion = versionFile.asFile.get().readText().trim()
val requiredVersion = catalogVersion.get()

check(fileVersion == requiredVersion) {
"Version ($fileVersion) in file '${versionFileRelativePath.get()}' does not match the version in ${catalogName.get()}.versions.toml ($requiredVersion). Please ensure these are aligned"
}

outputFile.asFile.get().writeText("valid")
}

internal companion object {
fun register(
project: Project,
type: String,
versionFilePath: String,
catalogVersion: String,
foundryVersions: FoundryVersions,
) {
project.tasks.register<ValidateVersionsMatch>("validate${type.capitalizeUS()}Matches") {
versionFile.set(project.layout.projectDirectory.file(versionFilePath))
versionFileRelativePath.set(versionFilePath)
this.catalogVersion.set(catalogVersion)
catalogName.set(foundryVersions.catalogName)
outputFile.set(
project.layout.buildDirectory.file("foundry/version-matches/$type/valid.txt")
)
}
}
}
}

0 comments on commit 5be37dd

Please sign in to comment.