Skip to content

Commit

Permalink
Introduce ModuleFeaturesConfig (#1101)
Browse files Browse the repository at this point in the history
This allows us to support client-side configuration and overrides of the
module feature rules in module topography

<!--
  ⬆ 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 Nov 15, 2024
1 parent 050d708 commit 146bfd0
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package foundry.gradle

import foundry.common.FoundryKeys
import foundry.gradle.FoundryProperties.Companion.CACHED_PROVIDER_EXT_NAME
import foundry.gradle.anvil.AnvilMode
import foundry.gradle.artifacts.FoundryArtifact
import foundry.gradle.properties.PropertyResolver
Expand All @@ -39,15 +38,22 @@ public class FoundryProperties
internal constructor(
private val projectName: String,
private val resolver: PropertyResolver,
private val fileProvider: (String) -> RegularFile,
private val regularFileProvider: (String) -> RegularFile,
private val rootDirFileProvider: (String) -> RegularFile,
internal val versions: FoundryVersions,
) {

private fun presenceProperty(key: String): Boolean = optionalStringProperty(key) != null

private fun fileProperty(key: String): File? =
optionalStringProperty(key)?.let(fileProvider)?.asFile
private fun fileProperty(key: String, useRoot: Boolean = false): File? =
optionalStringProperty(key)
?.let(if (useRoot) rootDirFileProvider else regularFileProvider)
?.asFile

private fun fileProvider(key: String, useRoot: Boolean = false): Provider<RegularFile> =
resolver
.optionalStringProvider(key)
.map(if (useRoot) rootDirFileProvider else regularFileProvider)

private fun intProperty(key: String, defaultValue: Int = -1): Int =
resolver.intValue(key, defaultValue = defaultValue)
Expand Down Expand Up @@ -149,7 +155,7 @@ internal constructor(
* dependencies shadow jobs.
*/
public val versionsJson: File?
get() = fileProperty("foundry.versionsJson")
get() = fileProperty("foundry.versionsJson", useRoot = true)

/**
* An alias name to a libs.versions.toml bundle for common Android Compose dependencies that
Expand Down Expand Up @@ -481,7 +487,7 @@ internal constructor(
* affected in this build.
*/
public val affectedProjects: File?
get() = fileProperty("foundry.avoidance.affectedProjectsFile")
get() = fileProperty("foundry.avoidance.affectedProjectsFile", useRoot = true)

/* Controls for Java/JVM/JDK versions uses in compilations and execution of tests. */

Expand Down Expand Up @@ -741,6 +747,13 @@ internal constructor(
public val topographyAutoFix: Provider<Boolean>
get() = resolver.booleanProvider("foundry.topography.validation.autoFix", defaultValue = false)

/**
* Property pointing at a features config JSON file for
* [foundry.gradle.topography.ModuleFeaturesConfig].
*/
public val topographyFeaturesConfig: Provider<RegularFile>
get() = fileProvider("foundry.topography.features.config", useRoot = true)

internal fun requireAndroidSdkProperties(): AndroidSdkProperties {
val compileSdk = compileSdkVersion ?: error("foundry.android.compileSdkVersion not set")
val minSdk = minSdkVersion?.toInt() ?: error("foundry.android.minSdkVersion not set")
Expand Down Expand Up @@ -826,7 +839,7 @@ internal constructor(
return FoundryProperties(
projectName = project.name,
resolver = resolver,
fileProvider = project.layout.projectDirectory::file,
regularFileProvider = project.layout.projectDirectory::file,
rootDirFileProvider = project.rootProject.layout.projectDirectory::file,
versions = versions,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,13 @@ import foundry.gradle.properties.mapToBoolean
import foundry.gradle.properties.setDisallowChanges
import foundry.gradle.register
import foundry.gradle.tasks.mustRunAfterSourceGeneratingTasks
import foundry.gradle.topography.KnownFeatures
import foundry.gradle.topography.DefaultFeatures
import foundry.gradle.topography.ModuleTopography
import foundry.gradle.topography.ModuleTopographyTask
import foundry.gradle.util.toJson
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import okio.source
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.Task
Expand Down Expand Up @@ -425,7 +424,7 @@ internal abstract class ModuleStatsCollectorTask @Inject constructor(objects: Ob

for (feature in topography.features) {
when (feature) {
KnownFeatures.DaggerCompiler.name -> finalTags.add(TAG_DAGGER_COMPILER)
DefaultFeatures.DaggerCompiler.name -> finalTags.add(TAG_DAGGER_COMPILER)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,69 +15,21 @@
*/
package foundry.gradle.topography

import com.squareup.moshi.JsonClass
import foundry.common.json.JsonTools
import java.nio.file.Path
import kotlin.reflect.full.declaredMemberProperties
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Provider

@JsonClass(generateAdapter = true)
public data class ModuleTopography(
val name: String,
val gradlePath: String,
val features: Set<String>,
val plugins: Set<String>,
) {
public fun writeJsonTo(property: Provider<out FileSystemLocation>, prettyPrint: Boolean = false) {
writeJsonTo(property.get().asFile.toPath(), prettyPrint)
}

public fun writeJsonTo(path: Path, prettyPrint: Boolean = false) {
JsonTools.toJson(path, this, prettyPrint)
}

public companion object {
public fun from(provider: Provider<out FileSystemLocation>): ModuleTopography =
from(provider.get().asFile.toPath())

public fun from(path: Path): ModuleTopography = JsonTools.fromJson<ModuleTopography>(path)
}
}

@JsonClass(generateAdapter = true)
public data class ModuleFeature(
val name: String,
val explanation: String,
val advice: String,
val removalPatterns: Set<Regex>?,
/**
* Generated sources root dir relative to the project dir, if any. Files are checked recursively.
*/
val generatedSourcesDir: String? = null,
val generatedSourcesExtensions: Set<String> = emptySet(),
val matchingText: Set<String> = emptySet(),
val matchingTextFileExtensions: Set<String> = emptySet(),
/**
* If specified, looks for any sources in this dir relative to the project dir. Files are checked
* recursively.
*/
val matchingSourcesDir: String? = null,
val matchingPlugin: String? = null,
)

// TODO eventually move these to JSON configs?
internal object KnownFeatures {
fun load(): Map<String, ModuleFeature> {
return KnownFeatures::class
internal object DefaultFeatures {
private val cachedValue by lazy {
DefaultFeatures::class
.declaredMemberProperties
.filter { it.returnType.classifier == ModuleFeature::class }
.associate {
val feature = it.get(KnownFeatures) as ModuleFeature
val feature = it.get(DefaultFeatures) as ModuleFeature
feature.name to feature
}
}

fun load(): Map<String, ModuleFeature> = cachedValue

internal val AndroidTest =
ModuleFeature(
name = "androidTest",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2024 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.topography

import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
public data class ModuleFeature(
val name: String,
val explanation: String,
val advice: String,
val removalPatterns: Set<Regex>?,
/**
* Generated sources root dir relative to the project dir, if any. Files are checked recursively.
*/
val generatedSourcesDir: String? = null,
val generatedSourcesExtensions: Set<String> = emptySet(),
val matchingText: Set<String> = emptySet(),
val matchingTextFileExtensions: Set<String> = emptySet(),
/**
* If specified, looks for any sources in this dir relative to the project dir. Files are checked
* recursively.
*/
val matchingSourcesDir: String? = null,
val matchingPlugin: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2024 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.topography

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import foundry.common.json.JsonTools
import foundry.common.json.JsonTools.readJsonValueMap
import java.nio.file.Path

/**
* Represents a configuration for module features that can be JSON-encoded.
*
* @property _features the set of user-defined [features][ModuleFeature].
* @property _buildUponDefaults indicates whether these should build upon the [DefaultFeatures] set.
* @property _defaultFeatureOverrides adhoc overrides of default feature values. These should be a
* subset of [ModuleFeature] properties and will be overlaid onto them
*/
@Suppress("PropertyName")
@JsonClass(generateAdapter = true)
internal data class ModuleFeaturesConfig(
@Json(name = "features") val _features: Set<ModuleFeature> = emptySet(),
@Json(name = "buildUponDefaults") val _buildUponDefaults: Boolean = true,
@Json(name = "defaultFeatureOverrides")
val _defaultFeatureOverrides: List<Map<String, Any>> = emptyList(),
) {

fun loadFeatures(): Map<String, ModuleFeature> {
val inputFeatures = _features.associateBy { it.name }
val defaultFeatures: Map<String, ModuleFeature> =
if (_buildUponDefaults) {
val defaults = DefaultFeatures.load()
buildMap {
putAll(defaults)
for (override in _defaultFeatureOverrides) {
val overrideName =
override["name"] as? String?
?: error("No feature name defined in override '$override'")
val defaultToOverride =
defaults[overrideName] ?: error("No default feature found for '$overrideName'")
// To simply do this, we just finagle the default to a JSON map and then overlay the new
// one onto it
val defaultJsonValueMap = JsonTools.toJsonBuffer(defaultToOverride).readJsonValueMap()
val newJsonValueMap = defaultJsonValueMap + override
val newFeature = JsonTools.fromJsonValue<ModuleFeature>(newJsonValueMap)
put(overrideName, newFeature)
}
}
} else {
emptyMap()
}
return defaultFeatures + inputFeatures
}

companion object {
val DEFAULT = ModuleFeaturesConfig()

fun load(path: Path): ModuleFeaturesConfig {
return JsonTools.fromJson(path)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2024 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.topography

import com.squareup.moshi.JsonClass
import foundry.common.json.JsonTools
import java.nio.file.Path
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Provider

@JsonClass(generateAdapter = true)
public data class ModuleTopography(
val name: String,
val gradlePath: String,
val features: Set<String>,
val plugins: Set<String>,
) {
public fun writeJsonTo(property: Provider<out FileSystemLocation>, prettyPrint: Boolean = false) {
writeJsonTo(property.get().asFile.toPath(), prettyPrint)
}

public fun writeJsonTo(path: Path, prettyPrint: Boolean = false) {
JsonTools.toJson(path, this, prettyPrint)
}

public companion object {
public fun from(provider: Provider<out FileSystemLocation>): ModuleTopography =
from(provider.get().asFile.toPath())

public fun from(path: Path): ModuleTopography = JsonTools.fromJson<ModuleTopography>(path)
}
}
Loading

0 comments on commit 146bfd0

Please sign in to comment.