├── minify-common ├── api │ └── minify-common.api ├── gradle.properties ├── minify-common.gradle.kts └── src │ └── jvmMain │ └── resources │ └── META-INF │ └── proguard │ └── common.pro ├── minify-crashlytics ├── api │ └── minify-crashlytics.api ├── gradle.properties ├── src │ └── jvmMain │ │ └── resources │ │ └── META-INF │ │ └── proguard │ │ └── crashlytics.pro └── minify-crashlytics.gradle.kts ├── docs ├── images │ ├── project-structure.png │ ├── icon-freeletics.svg │ └── logo-freeletics.svg ├── features │ ├── base-10-burst.md │ ├── base-5-metro.md │ ├── base-6-poko.md │ ├── base-7-kopy.md │ ├── multiplatform-3-skie.md │ ├── base-1-explicit-api.md │ ├── base-2-opt-in.md │ ├── multiplatform-android-3-resources.md │ ├── multiplatform-android-2-parcelize.md │ ├── multiplatform-android-4-proguard.md │ ├── base-4-serialization.md │ ├── multiplatform-android-1-paparazzi.md │ ├── android-app-2-resvalue.md │ ├── multiplatform-2-compose-resources.md │ ├── base-3-compose.md │ ├── android-app-1-buildconfig.md │ ├── base-12-internal-publishing.md │ ├── base-6-khonshu.md │ ├── base-9-room.md │ ├── base-11-oss-publishing.md │ ├── base-8-sqldelight.md │ └── multiplatform-1-targets.md ├── defaults │ ├── multiplatform-1-kotlin.md │ ├── gradle-plugin-2-validation.md │ ├── gradle-plugin-1-config.md │ ├── android-app-1-config.md │ ├── multiplatform-android-1-config.md │ ├── base-1-configuration.md │ └── base-2-default-dependencies.md ├── css │ └── app.css ├── plugins-monorepo │ ├── root.md │ ├── core.md │ ├── domain.md │ ├── nav.md │ ├── feature.md │ └── settings.md └── plugins-common │ ├── gradle.md │ ├── android-app.md │ └── multiplatform.md ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── gradle-daemon-jvm.properties ├── plugins ├── gradle.properties └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── freeletics │ │ └── gradle │ │ ├── monorepo │ │ ├── util │ │ │ ├── String.kt │ │ │ ├── AppType.kt │ │ │ ├── Git.kt │ │ │ └── ProjectType.kt │ │ ├── plugin │ │ │ ├── LegacyExtension.kt │ │ │ ├── NavPlugin.kt │ │ │ ├── LegacyPlugin.kt │ │ │ ├── CorePlugin.kt │ │ │ ├── AppPlugin.kt │ │ │ ├── DomainPlugin.kt │ │ │ ├── FeaturePlugin.kt │ │ │ └── AppDesktopPlugin.kt │ │ ├── setup │ │ │ ├── LicenseeSetup.kt │ │ │ ├── CrashlyticsSetup.kt │ │ │ ├── Platform.kt │ │ │ └── DisableTasks.kt │ │ └── tasks │ │ │ ├── UpdateLicensesTask.kt │ │ │ ├── VerifyLicensesTask.kt │ │ │ ├── GoogleServicesTask.kt │ │ │ ├── ComputeVersionNameTask.kt │ │ │ ├── CheckDependencyRules.kt │ │ │ ├── ComputeGitShaTask.kt │ │ │ ├── ComputeVersionCodeTask.kt │ │ │ └── ComputeGitTimestampTask.kt │ │ ├── plugin │ │ ├── CodegenExtension.kt │ │ ├── FreeleticsMultiplatformAndroidExtension.kt │ │ ├── FreeleticsMultiplatformPlugin.kt │ │ ├── FreeleticsAndroidAppExtension.kt │ │ ├── FreeleticsGradlePluginPlugin.kt │ │ ├── CodegenTask.kt │ │ ├── CodegenPlugin.kt │ │ ├── FreeleticsAndroidAppPlugin.kt │ │ └── FreeleticsBaseExtension.kt │ │ ├── util │ │ ├── Properties.kt │ │ ├── VersionCatalog.kt │ │ ├── PackageName.kt │ │ └── Extensions.kt │ │ └── setup │ │ ├── KspSetup.kt │ │ ├── SqlDelightSetup.kt │ │ ├── ComposeSetup.kt │ │ ├── AndroidLintSetup.kt │ │ ├── PaparazziSetup.kt │ │ ├── KmpJsWasmRepositories.kt │ │ └── AndroidTargetSetup.kt │ └── test │ └── kotlin │ └── com │ └── freeletics │ └── gradle │ └── monorepo │ ├── util │ ├── FakeGit.kt │ └── AppTypeTest.kt │ └── tasks │ ├── ComputeVersionBasedOnDateTest.kt │ └── ComputeVersionNameTest.kt ├── scripts-s3 ├── gradle.properties ├── README.md ├── scripts-s3.gradle.kts ├── src │ └── jvmMain │ │ └── kotlin │ │ └── com │ │ └── freeletics │ │ └── gradle │ │ └── scripts │ │ ├── QrCode.kt │ │ ├── S3Options.kt │ │ └── S3Uploader.kt └── api │ └── scripts-s3.api ├── codegen ├── gradle.properties ├── api │ └── codegen.api ├── codegen.gradle.kts └── src │ └── jvmMain │ └── kotlin │ └── com │ └── freeletics │ └── codegen │ └── CodeGenerator.kt ├── plugin-settings ├── gradle.properties ├── src │ └── main │ │ └── kotlin │ │ └── com │ │ └── freeletics │ │ └── gradle │ │ └── plugin │ │ ├── Utils.kt │ │ ├── FindProjects.kt │ │ └── SettingsExtension.kt ├── plugin-settings.gradle.kts └── api │ └── plugin-settings.api ├── scripts-github ├── gradle.properties ├── scripts-github.gradle.kts ├── api │ └── scripts-github.api ├── README.md └── src │ └── jvmMain │ └── kotlin │ └── com │ └── freeletics │ └── gradle │ └── scripts │ └── GithubOptions.kt ├── scripts-slack ├── gradle.properties ├── scripts-slack.gradle.kts ├── src │ └── jvmMain │ │ └── kotlin │ │ └── com │ │ └── freeletics │ │ └── gradle │ │ └── scripts │ │ ├── PlatformOptions.kt │ │ ├── ReleaseTrainOptions.kt │ │ └── SlackMessageCli.kt ├── README.md └── api │ └── scripts-slack.api ├── .gitignore ├── scripts-formatting ├── gradle.properties ├── src │ └── jvmMain │ │ └── kotlin │ │ └── com │ │ └── freeletics │ │ └── gradle │ │ └── scripts │ │ ├── Formatter.kt │ │ ├── FormattingResult.kt │ │ ├── KtFmtCli.kt │ │ ├── KtLintCli.kt │ │ ├── GradleDependenciesCli.kt │ │ ├── GradleDependenciesFormatter.kt │ │ ├── KtFmtFormatter.kt │ │ ├── FindFiles.kt │ │ ├── FormattingCli.kt │ │ └── KtLintFormatter.kt ├── scripts-formatting.gradle.kts ├── README.md └── api │ └── scripts-formatting.api ├── scripts-google ├── gradle.properties ├── README.md ├── scripts-google.gradle.kts ├── src │ └── jvmMain │ │ └── kotlin │ │ └── com │ │ └── freeletics │ │ └── gradle │ │ └── scripts │ │ ├── GoogleApiOptions.kt │ │ ├── GooglePlayReleaseVersion.kt │ │ ├── GoogleSheetsReader.kt │ │ ├── TimeoutHttpCredentialsAdapter.kt │ │ └── GooglePlayPublisher.kt └── api │ └── scripts-google.api ├── RELEASING.md ├── scripts └── ktlint.main.kts ├── gradle.properties ├── .editorconfig ├── .github ├── workflows │ ├── build.yml │ ├── publish-release.yml │ ├── publish-snapshot.yml │ └── publish-docs.yml └── renovate.json5 ├── settings.gradle.kts ├── README.md ├── mkdocs.yml ├── kotlinw └── gradlew.bat /minify-common/api/minify-common.api: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /minify-crashlytics/api/minify-crashlytics.api: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/project-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeletics/freeletics-gradle-plugins/HEAD/docs/images/project-structure.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeletics/freeletics-gradle-plugins/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /minify-common/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=minify-common 2 | POM_NAME=Minify common 3 | POM_DESCRIPTION=Common minify rules for R8 4 | -------------------------------------------------------------------------------- /plugins/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=plugins 2 | POM_NAME=Freeletics Gradle plugins 3 | POM_DESCRIPTION=A set of reusable Gradle plugins 4 | -------------------------------------------------------------------------------- /scripts-s3/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=scripts-s3 2 | POM_NAME=S3 Script helpers 3 | POM_DESCRIPTION=Collection of S3 related kts helpers 4 | -------------------------------------------------------------------------------- /codegen/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=codegen 2 | POM_NAME=Freeletics Codegen 3 | POM_DESCRIPTION=Provides an interface for implementing code generation 4 | -------------------------------------------------------------------------------- /minify-crashlytics/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=minify-crashlytics 2 | POM_NAME=Minify crashlytics 3 | POM_DESCRIPTION=Crashlytics minify rules for R8 4 | -------------------------------------------------------------------------------- /plugin-settings/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=settings-plugin 2 | POM_NAME=Freeletics Gradle settings plugin 3 | POM_DESCRIPTION=A Gradle settings plugin 4 | -------------------------------------------------------------------------------- /scripts-github/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=scripts-github 2 | POM_NAME=Github Script helpers 3 | POM_DESCRIPTION=Collection of Github related kts helpers 4 | -------------------------------------------------------------------------------- /scripts-slack/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=scripts-slack 2 | POM_NAME=Slack Script helpers 3 | POM_DESCRIPTION=Collection of Slack related kts helpers 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | local.properties 4 | .DS_Store 5 | build/ 6 | kotlin-js-store/ 7 | .kotlin/ 8 | 9 | docs/index.md 10 | docs/changelog.md 11 | -------------------------------------------------------------------------------- /scripts-formatting/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=scripts-formatting 2 | POM_NAME=Formatting Scripts 3 | POM_DESCRIPTION=Collection of formatting related kts scripts 4 | -------------------------------------------------------------------------------- /scripts-google/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=scripts-google 2 | POM_NAME=Google API Script helpers 3 | POM_DESCRIPTION=Collection of Google API related kts helpers 4 | -------------------------------------------------------------------------------- /codegen/api/codegen.api: -------------------------------------------------------------------------------- 1 | public abstract interface class com/freeletics/codegen/CodeGenerator { 2 | public abstract fun generate (Ljava/util/List;Ljava/io/File;Ljava/io/File;)V 3 | } 4 | 5 | -------------------------------------------------------------------------------- /scripts-s3/README.md: -------------------------------------------------------------------------------- 1 | # Google Scripts 2 | 3 | Provides `upload` helper methods and a `S3Options` clikt helper. 4 | 5 | ``` 6 | com.freeletics.gradle:scripts-s3: 7 | ``` 8 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/util/String.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.util 2 | 3 | internal fun String.capitalize() = replaceFirstChar { it.titlecase() } 4 | -------------------------------------------------------------------------------- /docs/features/base-10-burst.md: -------------------------------------------------------------------------------- 1 | ### Burst 2 | 3 | To enable [Burst](https://github.com/cashapp/burst) for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | useBurst() 8 | } 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/features/base-5-metro.md: -------------------------------------------------------------------------------- 1 | ### Metro 2 | 3 | To enable [Metro](https://zacsweers.github.io/metro) for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | useMetro() 8 | } 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/features/base-6-poko.md: -------------------------------------------------------------------------------- 1 | ### Poko 2 | 3 | To enable [Poko](https://github.com/drewhamilton/Poko) for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | usePoko() 8 | } 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/features/base-7-kopy.md: -------------------------------------------------------------------------------- 1 | ### Kopy 2 | 3 | To enable [Kopy](https://github.com/JavierSegoviaCordoba/kopy) for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | useKopy() 8 | } 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/features/multiplatform-3-skie.md: -------------------------------------------------------------------------------- 1 | ### SKIE 2 | 3 | To enable [SKIE](https://github.com/touchlab/skie) for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | useSkie() 8 | } 9 | ``` 10 | -------------------------------------------------------------------------------- /codegen/codegen.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.freeletics.gradle.multiplatform") 3 | } 4 | 5 | freeletics { 6 | enableOssPublishing() 7 | multiplatform { 8 | addJvmTarget() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Releasing 2 | ======== 3 | 4 | 1. Make sure `CHANGELOG.md` is up-to-date on `main` for the impeding release. 5 | 2. `git tag -a X.Y.X -m "Version X.Y.Z"` (where X.Y.Z is the new version) 6 | 3. `git push --tags` 7 | -------------------------------------------------------------------------------- /docs/features/base-1-explicit-api.md: -------------------------------------------------------------------------------- 1 | ### Explicit API 2 | 3 | It's possible to enable Kotlin's explicit API mode directly from the DSL by calling: 4 | 5 | ```kotlin 6 | freeletics { 7 | explicitApi() 8 | } 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/features/base-2-opt-in.md: -------------------------------------------------------------------------------- 1 | ### Experimental API opt in 2 | 3 | It's possible to opt into an experimental API for the module directly from the DSL by calling: 4 | 5 | ```kotlin 6 | freeletics { 7 | optIn("...") 8 | } 9 | ``` 10 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/plugin/LegacyExtension.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.plugin 2 | 3 | public abstract class LegacyExtension { 4 | public var allowLegacyDependencies: Boolean = false 5 | } 6 | -------------------------------------------------------------------------------- /scripts-formatting/src/jvmMain/kotlin/com/freeletics/gradle/scripts/Formatter.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import java.nio.file.Path 4 | 5 | public interface Formatter { 6 | public fun format(path: Path): FormattingResult 7 | } 8 | -------------------------------------------------------------------------------- /docs/features/multiplatform-android-3-resources.md: -------------------------------------------------------------------------------- 1 | ### Android resources 2 | 3 | To enable Android resources for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | android { 8 | enableAndroidResources() 9 | } 10 | } 11 | ``` 12 | -------------------------------------------------------------------------------- /scripts/ktlint.main.kts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kotlin 2 | 3 | @file:DependsOn("com.freeletics.gradle:scripts-formatting-jvm:0.36.1") 4 | 5 | import com.freeletics.gradle.scripts.KtLintCli 6 | import com.github.ajalt.clikt.core.main 7 | 8 | KtLintCli().main(args) 9 | -------------------------------------------------------------------------------- /minify-common/minify-common.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.freeletics.gradle.multiplatform") 3 | } 4 | 5 | freeletics { 6 | enableOssPublishing() 7 | multiplatform { 8 | addJvmTarget() 9 | } 10 | } 11 | 12 | dependencies { 13 | } 14 | -------------------------------------------------------------------------------- /minify-crashlytics/src/jvmMain/resources/META-INF/proguard/crashlytics.pro: -------------------------------------------------------------------------------- 1 | # Docs https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports?platform=android 2 | -keepattributes SourceFile,LineNumberTable 3 | -keep public class * extends java.lang.Exception 4 | -------------------------------------------------------------------------------- /codegen/src/jvmMain/kotlin/com/freeletics/codegen/CodeGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.codegen 2 | 3 | import java.io.File 4 | 5 | public interface CodeGenerator { 6 | public fun generate(arguments: List, sourceDirectory: File, targetDirectory: File) 7 | } 8 | -------------------------------------------------------------------------------- /minify-crashlytics/minify-crashlytics.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.freeletics.gradle.multiplatform") 3 | } 4 | 5 | freeletics { 6 | enableOssPublishing() 7 | multiplatform { 8 | addJvmTarget() 9 | } 10 | } 11 | 12 | dependencies { 13 | } 14 | -------------------------------------------------------------------------------- /docs/features/multiplatform-android-2-parcelize.md: -------------------------------------------------------------------------------- 1 | ### Parcelize 2 | 3 | To enable [Parcelize](https://developer.android.com/kotlin/parcelize) for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | android { 8 | enableParcelize() 9 | } 10 | } 11 | ``` 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xms1g -Xmx2g -XX:MaxMetaspaceSize=1g 2 | org.gradle.parallel=true 3 | 4 | fgp.kotlin.extraWarnings=false 5 | 6 | GROUP=com.freeletics.gradle 7 | VERSION_NAME=1.0-SNAPSHOT 8 | INCEPTION_YEAR=2022 9 | POM_REPO_NAME=freeletics-gradle-plugins 10 | -------------------------------------------------------------------------------- /docs/defaults/multiplatform-1-kotlin.md: -------------------------------------------------------------------------------- 1 | ### Multiplatform Kotlin configuration 2 | 3 | - The default hierarchy template is applied automatically. 4 | - `expect` and `actual` classes are enabled in compiler through the `-Xexpect-actual-classes` option. 5 | - Android Lint is applied to the module. 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /docs/features/multiplatform-android-4-proguard.md: -------------------------------------------------------------------------------- 1 | ### Consumer proguard files 2 | 3 | To publish proguard files for the consumer of the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | android { 8 | consumerProguardFiles("path/to/file") // supports multiple files being passed 9 | } 10 | } 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/features/base-4-serialization.md: -------------------------------------------------------------------------------- 1 | ### kotlinx.serialization 2 | 3 | To enable `kotlinx.serialziation` for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | useSerialization() 8 | } 9 | ``` 10 | 11 | This expects the version catalog to have a `kotlinx-serialization` entry with the 12 | `org.jetbrains.kotlinx:kotlinx-serialization-core` library. 13 | -------------------------------------------------------------------------------- /scripts-github/scripts-github.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.freeletics.gradle.multiplatform") 3 | } 4 | 5 | freeletics { 6 | enableOssPublishing() 7 | multiplatform { 8 | addJvmTarget() 9 | } 10 | } 11 | 12 | dependencies { 13 | "jvmMainApi"(libs.kotlin.stdlib) 14 | "jvmMainApi"(libs.clikt) 15 | "jvmMainApi"(libs.clikt.core) 16 | } 17 | -------------------------------------------------------------------------------- /scripts-google/README.md: -------------------------------------------------------------------------------- 1 | # Google Scripts 2 | 3 | Provides `GooglePlayPublisher` and `GoogleSheetsReader` to easily interact with 4 | the Google Play and Google Sheets API. 5 | 6 | Also contains `GoogleApiOptions` to obtain the json key of a Google service account 7 | from clikt scripts. 8 | 9 | ``` 10 | com.freeletics.gradle:scripts-google: 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/defaults/gradle-plugin-2-validation.md: -------------------------------------------------------------------------------- 1 | ### Validations 2 | 3 | - `enableStricterValidation` is set to true to make Gradle validate cacheable tasks more strictly. 4 | - Automatically enables Android Lint for the module and applies the `androidx.lint:lint-gradle` lint checks. 5 | - Expects a version catalog entry called `androidx-lint-gradle` pointing to the lint checks to exist. 6 | -------------------------------------------------------------------------------- /scripts-formatting/src/jvmMain/kotlin/com/freeletics/gradle/scripts/FormattingResult.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | public sealed interface FormattingResult { 4 | public data object AlreadyFormatted : FormattingResult 5 | 6 | public data object Formatted : FormattingResult 7 | 8 | public data class Error(val message: String) : FormattingResult 9 | } 10 | -------------------------------------------------------------------------------- /docs/features/multiplatform-android-1-paparazzi.md: -------------------------------------------------------------------------------- 1 | ### Paparazzi 2 | 3 | To enable [Paparazzi](https://github.com/cashapp/paparazzi) for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | android { 8 | usePaparazzi() 9 | } 10 | } 11 | ``` 12 | 13 | This also configures that running Gradle's `check` or `build` tasks will automatically include `verifyPaparazzi`. 14 | -------------------------------------------------------------------------------- /docs/defaults/gradle-plugin-1-config.md: -------------------------------------------------------------------------------- 1 | ### Java and Kotlin configuration for Gradle plugins 2 | 3 | - Configures the Java compilation to use the `java-target` version. 4 | - Hardcodes the Kotlin API and language level to the [latest supported by Gradle](https://docs.gradle.org/current/userguide/compatibility.html#kotlin). 5 | - Configures the Kotlin compiler to use classes for SAM conversion and lambdas. 6 | -------------------------------------------------------------------------------- /scripts-slack/scripts-slack.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.freeletics.gradle.multiplatform") 3 | } 4 | 5 | freeletics { 6 | enableOssPublishing() 7 | multiplatform { 8 | addJvmTarget() 9 | } 10 | } 11 | 12 | dependencies { 13 | "jvmMainApi"(libs.kotlin.stdlib) 14 | "jvmMainApi"(libs.clikt) 15 | "jvmMainApi"(libs.clikt.core) 16 | "jvmMainApi"(libs.slack) 17 | } 18 | -------------------------------------------------------------------------------- /docs/features/android-app-2-resvalue.md: -------------------------------------------------------------------------------- 1 | ### Res values 2 | 3 | To add a custom res value call: 4 | 5 | ```kotlin 6 | freeletics { 7 | android { 8 | // create a res value with the given value 9 | resValue("type", "name", "value") 10 | // create a res value with separate values for debug and release 11 | resValue("type", "name", "debug value", "release value") 12 | } 13 | } 14 | ``` 15 | -------------------------------------------------------------------------------- /scripts-s3/scripts-s3.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.freeletics.gradle.multiplatform") 3 | } 4 | 5 | freeletics { 6 | enableOssPublishing() 7 | multiplatform { 8 | addJvmTarget() 9 | } 10 | } 11 | 12 | dependencies { 13 | "jvmMainApi"(libs.kotlin.stdlib) 14 | "jvmMainApi"(libs.clikt) 15 | "jvmMainApi"(libs.clikt.core) 16 | "jvmMainImplementation"(libs.s3) 17 | "jvmMainImplementation"(libs.zxing) 18 | } 19 | -------------------------------------------------------------------------------- /docs/features/multiplatform-2-compose-resources.md: -------------------------------------------------------------------------------- 1 | ### Compose resources 2 | 3 | Enable [Compose resources](https://kotlinlang.org/docs/multiplatform/compose-multiplatform-resources.html) for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | multiplatform { 8 | useComposeResources() 9 | } 10 | } 11 | ``` 12 | 13 | This will configure the `Res` class generation to always run and will configure the package name based on the module. 14 | -------------------------------------------------------------------------------- /docs/features/base-3-compose.md: -------------------------------------------------------------------------------- 1 | ### Compose 2 | 3 | To enable Jetpack Compose for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | useCompose() 8 | } 9 | ``` 10 | 11 | It's possible to enable compose compiler reports and metrics by setting the following 2 properties. Afterwards 12 | the outputs can be found in the build folder. 13 | ```properties 14 | fgp.compose.enableCompilerMetrics=true 15 | fgp.compose.enableCompilerReports=true` 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/features/android-app-1-buildconfig.md: -------------------------------------------------------------------------------- 1 | ### Build config fields 2 | 3 | To add a custom build config field call: 4 | 5 | ```kotlin 6 | freeletics { 7 | android { 8 | // create a BuildConfig field with the given value 9 | buildConfigField("type", "name", "value") 10 | // create a BuildConfig field with separate values for debug and release 11 | buildConfigField("type", "name", "debug value", "release value") 12 | } 13 | } 14 | ``` 15 | -------------------------------------------------------------------------------- /plugin-settings/src/main/kotlin/com/freeletics/gradle/plugin/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.plugin 2 | 3 | import org.gradle.api.initialization.Settings 4 | 5 | internal fun Settings.stringProperty(name: String): String? { 6 | return providers.gradleProperty(name).orNull 7 | } 8 | 9 | internal fun Settings.booleanProperty(name: String, default: Boolean): Boolean { 10 | return providers.gradleProperty(name).map { it.toBoolean() }.orElse(default).get() 11 | } 12 | -------------------------------------------------------------------------------- /scripts-formatting/src/jvmMain/kotlin/com/freeletics/gradle/scripts/KtFmtCli.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.github.ajalt.clikt.core.Context 4 | import java.nio.file.PathMatcher 5 | 6 | public class KtFmtCli : FormattingCli() { 7 | override fun createPathMatcher(): PathMatcher = kotlinMatcher 8 | 9 | override fun createFormatter(): Formatter = KtFmtFormatter() 10 | 11 | override fun help(context: Context): String = "CLI that runs ktfmt." 12 | } 13 | -------------------------------------------------------------------------------- /scripts-formatting/src/jvmMain/kotlin/com/freeletics/gradle/scripts/KtLintCli.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.github.ajalt.clikt.core.Context 4 | import java.nio.file.PathMatcher 5 | 6 | public class KtLintCli : FormattingCli() { 7 | override fun createPathMatcher(): PathMatcher = kotlinMatcher 8 | 9 | override fun createFormatter(): Formatter = KtLintFormatter() 10 | 11 | override fun help(context: Context): String = "CLI that runs ktlint." 12 | } 13 | -------------------------------------------------------------------------------- /docs/features/base-12-internal-publishing.md: -------------------------------------------------------------------------------- 1 | ## Internal publishing 2 | 3 | Publishing to an internal repo can be enabled by calling the following function: 4 | 5 | ```kotlin 6 | freeletics { 7 | enableInternalPublishing() 8 | } 9 | ``` 10 | 11 | A `fgp.internalArtifacts.url` gradle property needs to be defined with the URL of the remote repository. The username 12 | and password for this repository are expected to be set through `internalArtifactsUsername` and `internalArtifactsPassword`. 13 | -------------------------------------------------------------------------------- /docs/images/icon-freeletics.svg: -------------------------------------------------------------------------------- 1 | 6 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /docs/features/base-6-khonshu.md: -------------------------------------------------------------------------------- 1 | ### Khonshu 2 | 3 | To enable [Khonshu](https://freeletics.github.io/khonshu) for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | useKhonshu() 8 | } 9 | ``` 10 | 11 | Both Metro and KSP will be automatically enabled when using Khonshu. 12 | 13 | This expects the version catalog to have the following 2 entries: 14 | - `khonshu-codegen-runtime` pointing to `com.freeletics.khonshu:codegen-runtime` 15 | - `khonshu-codegen-compiler` pointing to `com.freeletics.khonshu:codegen-compiler` 16 | -------------------------------------------------------------------------------- /plugin-settings/plugin-settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.freeletics.gradle.gradle") 3 | } 4 | 5 | freeletics { 6 | enableOssPublishing() 7 | } 8 | 9 | dependencies { 10 | implementation(libs.develocity.gradle) 11 | implementation(libs.toolchains.gradle) 12 | } 13 | 14 | gradlePlugin { 15 | plugins { 16 | create("settingsPlugin") { 17 | id = "com.freeletics.gradle.settings" 18 | implementationClass = "com.freeletics.gradle.plugin.SettingsPlugin" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts-formatting/src/jvmMain/kotlin/com/freeletics/gradle/scripts/GradleDependenciesCli.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.github.ajalt.clikt.core.Context 4 | import java.nio.file.PathMatcher 5 | 6 | public class GradleDependenciesCli : FormattingCli() { 7 | override fun createPathMatcher(): PathMatcher = gradleMatcher 8 | 9 | override fun createFormatter(): Formatter = GradleDependenciesFormatter() 10 | 11 | override fun help(context: Context): String = "CLI that runs gradle-dependency-sorter." 12 | } 13 | -------------------------------------------------------------------------------- /docs/defaults/android-app-1-config.md: -------------------------------------------------------------------------------- 1 | ### Android configuration 2 | 3 | - The `compileSdk` is set to the `android-compile` version catalog version 4 | - The `minSdk` is set to the `android-min` version catalog version 5 | - The `targetSdk` is set to the `android-target` version catalog version 6 | - Optionally the `buildToolsVersion` is set to the `android-buildTools` version catalog version if it's present in the 7 | version catalog. 8 | - If the `android-desugarjdklibs` library is defined the version catalog core library desugaring is be enabled 9 | automatically. 10 | -------------------------------------------------------------------------------- /docs/defaults/multiplatform-android-1-config.md: -------------------------------------------------------------------------------- 1 | ### Android target configuration 2 | 3 | If an Android target is added: 4 | 5 | - The `compileSdk` is set to the `android-compile` version catalog version 6 | - The `minSdk` is set to the `android-min` version catalog version 7 | - Optionally the `buildToolsVersion` is set to the `android-buildTools` version catalog version if it's present in the 8 | version catalog. 9 | - If the `android-desugarjdklibs` library is defined the version catalog core library desugaring is be enabled 10 | automatically. 11 | - Host tests are enabled. 12 | -------------------------------------------------------------------------------- /docs/features/base-9-room.md: -------------------------------------------------------------------------------- 1 | ### Room 2 | 3 | To enable [Room](https://developer.android.com/jetpack/androidx/releases/room) for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | useRoom() 8 | } 9 | ``` 10 | 11 | This will automatically apply KSP to the project and will use Kotlin code generation. 12 | 13 | Optionally a `schemaLocation` can be provided. 14 | 15 | This expects the version catalog to have the following 2 entries: 16 | - `androidx-room-runtime` pointing to `androidx.room:room-runtime` 17 | - `androidx-room-compiler` pointing to `androidx.room:room-compiler` 18 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/plugin/CodegenExtension.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.codegen 2 | 3 | import com.freeletics.codegen.CodeGenerator 4 | import org.gradle.api.file.DirectoryProperty 5 | import org.gradle.api.provider.ListProperty 6 | 7 | public interface CodegenExtension { 8 | /** 9 | * The `arguments` that are passed to [CodeGenerator]. 10 | */ 11 | public val arguments: ListProperty 12 | 13 | /** 14 | * The directory which contains the source scripts. 15 | */ 16 | public val sourceDirectory: DirectoryProperty 17 | } 18 | -------------------------------------------------------------------------------- /docs/features/base-11-oss-publishing.md: -------------------------------------------------------------------------------- 1 | ## OSS publishing 2 | 3 | Publishing to Maven Central using the [gradle-maven-publish-plugin](https://vanniktech.github.io/gradle-maven-publish-plugin). 4 | 5 | ```kotlin 6 | freeletics { 7 | enableOssPublishing() 8 | } 9 | ``` 10 | 11 | Enabling this will also apply [Dokka](https://github.com/kotlin/dokka), enable ABI validation and enable explicit API 12 | mode. 13 | 14 | For the configuration of the POM, only the following Gradle properties need to be defined: 15 | - `GROUP` 16 | - `VERSION_NAME` 17 | - `POM_REPO_NAME` 18 | - `POM_ARTIFACT_ID` 19 | - `POM_NAME` 20 | - `POM_DESCRIPTION` 21 | -------------------------------------------------------------------------------- /docs/defaults/base-1-configuration.md: -------------------------------------------------------------------------------- 1 | ### Common configuration 2 | 3 | The plugin applies the following common configuration: 4 | 5 | - The Java and Kotlin toolchain are set to the `java-toolchain` version catalog version. 6 | - Kotlin's progressive mode is enabled. 7 | - Kotlin compiler warnings are treated as errors. 8 | - This can be disabled by setting `fgp.kotlin.warningsAsErrors=false` 9 | - It's possible to ignore deprecation warning by setting `fgp.kotlin.suppressDeprecationWarnings=true` 10 | - Several Kotlin compiler flags are set to improve the default behavior. 11 | - All produced archives are configured to be reproducible. 12 | -------------------------------------------------------------------------------- /docs/css/app.css: -------------------------------------------------------------------------------- 1 | body, input { 2 | font-family: "Helvetica Neue",helvetica,sans-serif; 3 | } 4 | 5 | .md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4 { 6 | font-family: "Helvetica Neue",helvetica,sans-serif; 7 | line-height: normal; 8 | font-weight: bold; 9 | color: #283237; 10 | } 11 | 12 | .md-typeset a { 13 | color: #3375b8; 14 | } 15 | 16 | :root { 17 | --md-primary-fg-color: #283237; 18 | --md-primary-fg-color--light: #283237; 19 | --md-primary-fg-color--dark: #283237; 20 | 21 | --md-primary-accent-color: #3375b8; 22 | --md-primary-accent-color--transparent: #3375b8; 23 | } 24 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/util/Properties.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.util 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.provider.Provider 5 | 6 | internal fun Project.stringProperties(prefix: String): Provider> { 7 | return providers.gradlePropertiesPrefixedBy(prefix) 8 | } 9 | 10 | internal fun Project.stringProperty(name: String): Provider = providers.gradleProperty(name) 11 | 12 | internal fun Project.booleanProperty(name: String, defaultValue: Boolean): Provider { 13 | return stringProperty(name).map { it.toBoolean() }.orElse(defaultValue) 14 | } 15 | -------------------------------------------------------------------------------- /docs/features/base-8-sqldelight.md: -------------------------------------------------------------------------------- 1 | ### SQLDelight 2 | 3 | To enable [SQLDelight](https://sqldelight.github.io/sqldelight) for the module call: 4 | 5 | ```kotlin 6 | freeletics { 7 | useSqlDelight() 8 | } 9 | ``` 10 | 11 | There are 2 optional parameters for this function: 12 | - the `name` for the generated database which defaults to `database` 13 | - a `dependency` if this module depends on another module's SQLDelight definitions 14 | 15 | To change the used [SQL dialect](https://sqldelight.github.io/sqldelight/latest/multiplatform_sqlite/gradle/#dialect) 16 | add the dialect of your choice to the version catalog with an entry called `sqldelight-dialect`. 17 | -------------------------------------------------------------------------------- /plugins/src/test/kotlin/com/freeletics/gradle/monorepo/util/FakeGit.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.util 2 | 3 | internal class FakeGit( 4 | var branch: String = "main", 5 | var commitSha: String = "abcdefghij", 6 | var commitTimestamp: String = "2022-10-21 16:36:11 +0200", 7 | var describe: String = "", 8 | var describeNonExact: String = "", 9 | ) : Git { 10 | override fun branch() = branch 11 | 12 | override fun commitSha() = commitSha 13 | 14 | override fun commitTimestamp() = commitTimestamp 15 | 16 | override fun describe(match: String, exactMatch: Boolean) = if (exactMatch) describe else describeNonExact 17 | } 18 | -------------------------------------------------------------------------------- /scripts-github/api/scripts-github.api: -------------------------------------------------------------------------------- 1 | public final class com/freeletics/gradle/scripts/GithubOptions : com/github/ajalt/clikt/parameters/groups/OptionGroup { 2 | public fun ()V 3 | public final fun getBranch ()Ljava/lang/String; 4 | public final fun getBuildNumber ()Ljava/lang/String; 5 | public final fun getCommitSha1 ()Ljava/lang/String; 6 | public final fun getJobName ()Ljava/lang/String; 7 | public final fun getJobUrl ()Ljava/lang/String; 8 | public final fun getRepoName ()Ljava/lang/String; 9 | public final fun getRepoSlug ()Ljava/lang/String; 10 | public final fun getRunId ()Ljava/lang/String; 11 | public final fun getUserName ()Ljava/lang/String; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /scripts-s3/src/jvmMain/kotlin/com/freeletics/gradle/scripts/QrCode.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.google.zxing.BarcodeFormat 4 | import com.google.zxing.client.j2se.MatrixToImageWriter 5 | import com.google.zxing.qrcode.QRCodeWriter 6 | import java.io.ByteArrayOutputStream 7 | 8 | public fun createQrCode(uri: String): ByteArray { 9 | val writer = QRCodeWriter() 10 | val bitMatrix = writer.encode(uri, BarcodeFormat.QR_CODE, IMAGE_SIZE, IMAGE_SIZE) 11 | return ByteArrayOutputStream().use { 12 | MatrixToImageWriter.writeToStream(bitMatrix, "png", it) 13 | it.toByteArray() 14 | } 15 | } 16 | 17 | private const val IMAGE_SIZE = 300 18 | -------------------------------------------------------------------------------- /scripts-formatting/scripts-formatting.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.freeletics.gradle.multiplatform") 3 | } 4 | 5 | freeletics { 6 | enableOssPublishing() 7 | multiplatform { 8 | addJvmTarget() 9 | } 10 | } 11 | 12 | dependencies { 13 | "jvmMainApi"(libs.kotlin.stdlib) 14 | "jvmMainApi"(libs.clikt) 15 | "jvmMainApi"(libs.clikt.core) 16 | "jvmMainImplementation"(libs.coroutines) 17 | "jvmMainImplementation"(libs.ktlint.rule.engine) 18 | "jvmMainImplementation"(libs.ktlint.rule.engine.core) 19 | "jvmMainImplementation"(libs.ktlint.rules) 20 | "jvmMainImplementation"(libs.ktfmt) 21 | "jvmMainImplementation"(libs.gradle.sorter) 22 | } 23 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/setup/KspSetup.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.setup 2 | 3 | import com.google.devtools.ksp.gradle.KspExtension 4 | import org.gradle.api.Project 5 | import org.gradle.process.CommandLineArgumentProvider 6 | 7 | internal fun Project.configureProcessing(arguments: List = emptyList()) { 8 | plugins.apply("com.google.devtools.ksp") 9 | 10 | extensions.configure(KspExtension::class.java) { extension -> 11 | arguments.forEach { 12 | extension.arg(it) 13 | } 14 | } 15 | } 16 | 17 | internal fun basicArgument(key: String, value: String) = CommandLineArgumentProvider { listOf("$key=$value") } 18 | -------------------------------------------------------------------------------- /scripts-google/scripts-google.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.freeletics.gradle.multiplatform") 3 | } 4 | 5 | freeletics { 6 | enableOssPublishing() 7 | multiplatform { 8 | addJvmTarget() 9 | } 10 | } 11 | 12 | dependencies { 13 | "jvmMainApi"(libs.kotlin.stdlib) 14 | "jvmMainApi"(libs.clikt) 15 | "jvmMainApi"(libs.clikt.core) 16 | "jvmMainApi"(libs.google.play) 17 | "jvmMainImplementation"(libs.google.sheets) 18 | "jvmMainImplementation"(libs.google.apiclient) 19 | "jvmMainImplementation"(libs.google.http) 20 | "jvmMainImplementation"(libs.google.http.gson) 21 | "jvmMainImplementation"(libs.google.credentials) 22 | "jvmMainImplementation"(libs.google.oauth) 23 | } 24 | -------------------------------------------------------------------------------- /scripts-github/README.md: -------------------------------------------------------------------------------- 1 | # Github Scripts 2 | 3 | This provides a `OptionGroup` helper called `githubOptions` which gives access 4 | to common Github values like the repository name, job number or branch. 5 | 6 | ### Example usage 7 | 8 | ```kts 9 | #!/usr/bin/env kotlin 10 | 11 | @file:DependsOn("com.freeletics.gradle:scripts-github:") 12 | 13 | import com.freeletics.gradle.scripts.GithubOptions 14 | import com.github.ajalt.clikt.core.CliktCommand 15 | import com.github.ajalt.clikt.core.main 16 | 17 | class MyCli : CliktCommand() { 18 | private val github by GithubOptions() 19 | 20 | override fun run() { 21 | send("Hello from job ${github.jobName}") 22 | } 23 | } 24 | 25 | MyCli().main(args) 26 | ``` 27 | -------------------------------------------------------------------------------- /scripts-slack/src/jvmMain/kotlin/com/freeletics/gradle/scripts/PlatformOptions.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.github.ajalt.clikt.parameters.groups.OptionGroup 4 | import com.github.ajalt.clikt.parameters.options.option 5 | import com.github.ajalt.clikt.parameters.options.required 6 | import com.github.ajalt.clikt.parameters.types.enum 7 | 8 | public class PlatformOptions : OptionGroup("Platform options") { 9 | public val platform: Platform by option( 10 | "--platform", 11 | help = "The platform this command is executed for", 12 | ).enum().required() 13 | } 14 | 15 | public enum class Platform { 16 | ANDROID, 17 | IOS, 18 | ; 19 | 20 | public val value: String 21 | get() = name.lowercase() 22 | } 23 | -------------------------------------------------------------------------------- /scripts-google/src/jvmMain/kotlin/com/freeletics/gradle/scripts/GoogleApiOptions.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.github.ajalt.clikt.parameters.groups.OptionGroup 4 | import com.github.ajalt.clikt.parameters.options.convert 5 | import com.github.ajalt.clikt.parameters.options.option 6 | import com.github.ajalt.clikt.parameters.options.required 7 | import java.util.Base64 8 | 9 | public class GoogleApiOptions : OptionGroup("Google API options") { 10 | public val jsonKeyData: String by option( 11 | "--json-key-data", 12 | envvar = "GOOGLE_PLAY_JSON_KEY_DATA", 13 | help = "The json key for a Google Play service account. Needs to be provided in base64 encoded form.", 14 | ).convert { Base64.getDecoder().decode(it).toString(Charsets.UTF_8) }.required() 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_size = 4 5 | indent_style = space 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | max_line_length = 120 9 | tab_width = 4 10 | ij_continuation_indent_size = 4 11 | 12 | [*.{kt,kts}] 13 | ij_kotlin_imports_layout=* 14 | ij_kotlin_allow_trailing_comma = true 15 | ij_kotlin_allow_trailing_comma_on_call_site = true 16 | 17 | # 18 | # ktlint disabled rules 19 | # 20 | ktlint_standard_function-expression-body = disabled 21 | ktlint_standard_binary-expression-wrapping = disabled 22 | ktlint_standard_multiline-expression-wrapping = disabled 23 | ktlint_standard_chain-method-continuation = disabled 24 | ktlint_standard_class-signature = disabled 25 | ktlint_standard_function-signature = disabled 26 | ktlint_standard_expression-operand-wrapping = disabled 27 | -------------------------------------------------------------------------------- /scripts-formatting/src/jvmMain/kotlin/com/freeletics/gradle/scripts/GradleDependenciesFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.squareup.sort.Sorter 4 | import java.nio.file.Path 5 | import kotlin.io.path.writeText 6 | 7 | internal class GradleDependenciesFormatter : Formatter { 8 | override fun format(path: Path): FormattingResult { 9 | val code = Sorter.of(path, Sorter.Config(insertBlankLines = true)) 10 | return if (code.hasParseErrors()) { 11 | FormattingResult.Error("Unexpected error formatting $path: ${code.getParseError()!!.message}") 12 | } else if (code.isSorted()) { 13 | return FormattingResult.AlreadyFormatted 14 | } else { 15 | path.writeText(code.rewritten()) 16 | return FormattingResult.Formatted 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/setup/LicenseeSetup.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.setup 2 | 3 | import app.cash.licensee.LicenseeExtension 4 | import app.cash.licensee.SpdxId 5 | import com.freeletics.gradle.monorepo.tasks.UpdateLicensesTask.Companion.registerUpdateLicensesTask 6 | import com.freeletics.gradle.monorepo.tasks.VerifyLicensesTask.Companion.registerVerifyLicensesTask 7 | import org.gradle.api.Project 8 | 9 | internal fun Project.configureLicensee() { 10 | plugins.apply("app.cash.licensee") 11 | 12 | registerUpdateLicensesTask() 13 | registerVerifyLicensesTask() 14 | 15 | extensions.configure(LicenseeExtension::class.java) { 16 | it.allow(SpdxId.Apache_20) 17 | it.allow(SpdxId.BSD_3_Clause) 18 | it.allow(SpdxId.MIT) 19 | it.allow(SpdxId.MIT_0) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v6.0.1 17 | 18 | - name: Install JDK 19 | uses: actions/setup-java@v5.1.0 20 | with: 21 | distribution: zulu 22 | java-version: 24 23 | 24 | - uses: gradle/actions/setup-gradle@v5.0.0 25 | with: 26 | validate-wrappers: true 27 | 28 | - name: Build with Gradle 29 | run: ./gradlew build buildHealth --stacktrace 30 | 31 | - name: Check code style 32 | run: ./kotlinw scripts/ktlint.main.kts --fail-on-changes 33 | -------------------------------------------------------------------------------- /plugins/src/test/kotlin/com/freeletics/gradle/monorepo/tasks/ComputeVersionBasedOnDateTest.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.tasks 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import java.time.LocalDate 5 | import org.junit.Test 6 | 7 | class ComputeVersionBasedOnDateTest { 8 | @Test 9 | fun simple() { 10 | assertThat(versionBasedOnDate(LocalDate.of(2024, 11, 12))).isEqualTo("24.46.0") 11 | } 12 | 13 | @Test 14 | fun `end of year`() { 15 | assertThat(versionBasedOnDate(LocalDate.of(2024, 12, 29))).isEqualTo("24.52.0") 16 | } 17 | 18 | @Test 19 | fun `first week of next year, but still in previous year`() { 20 | assertThat(versionBasedOnDate(LocalDate.of(2024, 12, 30))).isEqualTo("25.1.0") 21 | } 22 | 23 | @Test 24 | fun `beginning of year`() { 25 | assertThat(versionBasedOnDate(LocalDate.of(2025, 1, 1))).isEqualTo("25.1.0") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/plugins-monorepo/root.md: -------------------------------------------------------------------------------- 1 | # Root plugin 2 | 3 | Add the following to the root `build.gradle` or `build.gradle.kts` to apply the plugin: 4 | 5 | ```groovy 6 | plugins { 7 | id("com.freeletics.gradle.root").version("") 8 | } 9 | ``` 10 | 11 | ## Features 12 | 13 | ### Java Platform 14 | 15 | The root plugin applies the `java-platform` plugin and automatically adds all dependencies from 16 | all version catalogs to it. This way subprojects can add the root project as a platform to enforce 17 | versions across the whole repository. The monorepo plugins automatically use this. 18 | 19 | ### Dependency Analysis 20 | 21 | Applies the [Dependency Analysis Gradle Plugin][1] and adds some default configuration for it: 22 | - fail on any advice 23 | - adds some default excludes based on default dependencies from the other plugins 24 | - creates bundles for common libraries like compose 25 | 26 | ### Other 27 | 28 | - Configures the `updateDaemonJvm` task to use `AZUL` as the JDK vendor. 29 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/util/AppType.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.util 2 | 3 | import org.gradle.api.Project 4 | 5 | internal data class AppType( 6 | val name: String, 7 | ) 8 | 9 | internal fun Project.appType(): AppType? { 10 | return path.toAppType() 11 | } 12 | 13 | internal fun String.toAppType(): AppType? { 14 | val parts = split(":") 15 | if (parts[1] == "app") { 16 | val suffix = parts[2].substringAfterLast("-") 17 | // for app modules that are platform specific variants (-android, -desktop) we shouldn't consider the suffix 18 | return if (platformSuffixes.contains(suffix)) { 19 | AppType(parts[2].substringBeforeLast("-")) 20 | } else { 21 | AppType(parts[2]) 22 | } 23 | } else if (parts[1].contains("-")) { 24 | return AppType(parts[1].substringAfter("-")) 25 | } 26 | return null 27 | } 28 | 29 | private val platformSuffixes = listOf("android", "desktop") 30 | -------------------------------------------------------------------------------- /scripts-formatting/src/jvmMain/kotlin/com/freeletics/gradle/scripts/KtFmtFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.facebook.ktfmt.format.Formatter as KtFormatter 4 | import java.nio.file.Path 5 | import kotlin.io.path.readText 6 | import kotlin.io.path.writeText 7 | 8 | internal class KtFmtFormatter : Formatter { 9 | private val options = KtFormatter.KOTLINLANG_FORMAT.copy(maxWidth = 120) 10 | 11 | override fun format(path: Path): FormattingResult { 12 | val code = path.readText() 13 | val formatted = try { 14 | KtFormatter.format(options, code) 15 | } catch (e: Exception) { 16 | return FormattingResult.Error("Unexpected error formatting $path: ${e.message}") 17 | } 18 | 19 | if (formatted == code) { 20 | return FormattingResult.AlreadyFormatted 21 | } else { 22 | path.writeText(formatted) 23 | return FormattingResult.Formatted 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/plugin/FreeleticsMultiplatformAndroidExtension.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.plugin 2 | 3 | import com.freeletics.gradle.setup.configurePaparazzi 4 | import com.freeletics.gradle.util.androidMultiplatform 5 | import org.gradle.api.Project 6 | 7 | public abstract class FreeleticsMultiplatformAndroidExtension(private val project: Project) { 8 | public fun usePaparazzi() { 9 | project.configurePaparazzi() 10 | } 11 | 12 | public fun enableParcelize() { 13 | project.plugins.apply("org.jetbrains.kotlin.plugin.parcelize") 14 | } 15 | 16 | public fun enableAndroidResources() { 17 | project.androidMultiplatform { 18 | androidResources.enable = true 19 | } 20 | } 21 | 22 | @Suppress("UnstableApiUsage") 23 | public fun consumerProguardFiles(vararg files: String) { 24 | project.androidMultiplatform { 25 | optimization.consumerKeepRules.files(*files) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | exclusiveContent { 4 | forRepository { google() } 5 | 6 | filter { 7 | includeGroupByRegex("com\\.android.*") 8 | includeGroupByRegex("androidx.*") 9 | includeGroupByRegex("com.google.firebase.*") 10 | 11 | includeGroup("com.google.testing.platform") 12 | includeGroup("com.google.android.apps.common.testing.accessibility.framework") 13 | } 14 | } 15 | 16 | exclusiveContent { 17 | forRepository { gradlePluginPortal() } 18 | 19 | filter { 20 | includeGroupByRegex("com.gradle.*") 21 | includeGroupByRegex("org.gradle.*") 22 | } 23 | } 24 | 25 | mavenCentral() 26 | } 27 | } 28 | 29 | plugins { 30 | id("com.freeletics.gradle.settings").version("0.36.1") 31 | } 32 | 33 | rootProject.name = "freeletics-gradle-plugins-monorepo" 34 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/tasks/UpdateLicensesTask.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.tasks 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.tasks.Copy 5 | import org.gradle.work.DisableCachingByDefault 6 | 7 | @DisableCachingByDefault(because = "Copies files") 8 | public abstract class UpdateLicensesTask : Copy() { 9 | internal companion object { 10 | fun Project.registerUpdateLicensesTask() { 11 | tasks.register("updateLicenses", UpdateLicensesTask::class.java) { task -> 12 | task.from(project.layout.buildDirectory.file("reports/licensee/androidRelease/artifacts.json")) 13 | task.into("src/main/assets") 14 | 15 | task.rename("artifacts.json", "license_acknowledgements.json") 16 | 17 | task.filter { line -> 18 | if (line.contains("\"version\": \"")) "" else line 19 | } 20 | 21 | task.dependsOn("licenseeAndroidRelease") 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/features/multiplatform-1-targets.md: -------------------------------------------------------------------------------- 1 | ### Adding multiplatform targets 2 | 3 | There are several functions to easily add targets to the module: 4 | 5 | ```kotlin 6 | freeletics { 7 | multiplatform { 8 | // adds all targets that a also supported by the coroutines project 9 | // has a `limitToComposeTargets` boolean parameter that can be set to true to only add targets supported by 10 | // the compose runtime 11 | addCommonTargets() 12 | // adds jvm as a target 13 | addJvmTarget() 14 | // adds Android as a target 15 | addAndroidTarget { 16 | // Android DSL options 17 | } 18 | // adds `iosArm64`, `iosSimulatorArm64` as targets 19 | // has a `includeX64` boolean parameter to also include `iosX64` 20 | addIosTargets() 21 | // same as above but will also configure everything to create a XCFramework 22 | addIosTargetsWithXcFramework("frameworkName") { framework -> 23 | // optionally configure the framework 24 | } 25 | } 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/setup/SqlDelightSetup.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.setup 2 | 3 | import app.cash.sqldelight.gradle.SqlDelightExtension 4 | import com.freeletics.gradle.util.defaultPackageName 5 | import com.freeletics.gradle.util.getDependencyOrNull 6 | import org.gradle.api.Project 7 | import org.gradle.api.artifacts.ProjectDependency 8 | 9 | internal fun Project.setupSqlDelight( 10 | name: String, 11 | dependency: ProjectDependency?, 12 | ) { 13 | plugins.apply("app.cash.sqldelight") 14 | 15 | extensions.configure(SqlDelightExtension::class.java) { sqldelight -> 16 | sqldelight.databases.create(name) { db -> 17 | db.packageName.set(defaultPackageName()) 18 | db.deriveSchemaFromMigrations.set(true) 19 | val dialect = getDependencyOrNull("sqldelight-dialect") 20 | if (dialect != null) { 21 | db.dialect(dialect) 22 | } 23 | if (dependency != null) { 24 | db.dependency(dependency) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gradle/gradle-daemon-jvm.properties: -------------------------------------------------------------------------------- 1 | #This file is generated by updateDaemonJvm 2 | toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/796901cbf89fc850a3f3bb8ddfe4506c/redirect 3 | toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/26af28a3208b58b524a51fc519bca85a/redirect 4 | toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/796901cbf89fc850a3f3bb8ddfe4506c/redirect 5 | toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/26af28a3208b58b524a51fc519bca85a/redirect 6 | toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/761743ab934ff2cc7d8e409918f67acf/redirect 7 | toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/d5f8fa0927b058deea7bc5543fc3e371/redirect 8 | toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/796901cbf89fc850a3f3bb8ddfe4506c/redirect 9 | toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/26af28a3208b58b524a51fc519bca85a/redirect 10 | toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/f765ffc0b109346d16cfa1fed6eb856a/redirect 11 | toolchainVendor=AZUL 12 | toolchainVersion=24 13 | -------------------------------------------------------------------------------- /docs/plugins-common/gradle.md: -------------------------------------------------------------------------------- 1 | # Gradle plugin 2 | 3 | Add the following to the root `build.gradle` or `build.gradle.kts` to apply the plugin: 4 | 5 | ```groovy 6 | plugins { 7 | id("com.freeletics.gradle.gradle").version("") 8 | } 9 | ``` 10 | 11 | ## Defaults 12 | 13 | --8<-- "docs/defaults/base-1-configuration.md" 14 | --8<-- "docs/defaults/base-2-default-dependencies.md" 15 | --8<-- "docs/defaults/gradle-plugin-1-config.md" 16 | --8<-- "docs/defaults/gradle-plugin-2-validation.md" 17 | 18 | ## Features 19 | 20 | --8<-- "docs/features/base-1-explicit-api.md" 21 | --8<-- "docs/features/base-2-opt-in.md" 22 | --8<-- "docs/features/base-3-compose.md" 23 | --8<-- "docs/features/base-4-serialization.md" 24 | --8<-- "docs/features/base-5-metro.md" 25 | --8<-- "docs/features/base-6-khonshu.md" 26 | --8<-- "docs/features/base-6-poko.md" 27 | --8<-- "docs/features/base-7-kopy.md" 28 | --8<-- "docs/features/base-8-sqldelight.md" 29 | --8<-- "docs/features/base-9-room.md" 30 | --8<-- "docs/features/base-10-burst.md" 31 | --8<-- "docs/features/base-11-oss-publishing.md" 32 | --8<-- "docs/features/base-12-internal-publishing.md" 33 | -------------------------------------------------------------------------------- /scripts-slack/src/jvmMain/kotlin/com/freeletics/gradle/scripts/ReleaseTrainOptions.kt: -------------------------------------------------------------------------------- 1 | 2 | package com.freeletics.gradle.scripts 3 | 4 | import com.github.ajalt.clikt.parameters.groups.OptionGroup 5 | import com.github.ajalt.clikt.parameters.options.option 6 | import com.github.ajalt.clikt.parameters.options.required 7 | 8 | public class ReleaseTrainOptions : OptionGroup("Release Train options") { 9 | public val name: String by option( 10 | "--app", 11 | help = "The app this executed for, e.g. `freeletics`", 12 | ).required() 13 | 14 | public val displayName: String get() = name.replaceFirstChar { it.titlecase() } 15 | 16 | public val versionName: String by option( 17 | "--version-name", 18 | help = "The version name of the app", 19 | ).required() 20 | 21 | public val versionCode: String? by option( 22 | "--version-code", 23 | help = "The version code of the app", 24 | ) 25 | 26 | public val branch: String by option( 27 | "--branch", 28 | envvar = "GITHUB_REF_NAME", 29 | help = "The branch of the release train", 30 | ).required() 31 | } 32 | -------------------------------------------------------------------------------- /scripts-slack/README.md: -------------------------------------------------------------------------------- 1 | # Slack Scripts 2 | 3 | Provides `SlackMessageCli` that makes it easy to write clikt scripts that send Slack messages. 4 | 5 | This also provides several `OptionGroup` helpers: 6 | - `PlatformOptions` 7 | - `ReleaseTrainOptions` 8 | 9 | ### Example usage 10 | 11 | ```kts 12 | #!/usr/bin/env kotlin 13 | 14 | @file:DependsOn("com.freeletics.gradle:scripts-slack:") 15 | 16 | import com.freeletics.gradle.scripts.CircleCiOptions 17 | import com.freeletics.gradle.scripts.SlackMessageCli 18 | import com.github.ajalt.clikt.parameters.groups.provideDelegate 19 | import com.github.ajalt.clikt.parameters.options.option 20 | import com.github.ajalt.clikt.parameters.options.required 21 | import com.github.ajalt.clikt.core.main 22 | 23 | class MySlackCli : SlackMessageCli() { 24 | override val webHookUrl: String by option( 25 | "--web-hook-url", 26 | help = "The web hook url that is used to send messages", 27 | ).required() 28 | 29 | private val circleCi by CircleCiOptions() 30 | 31 | override fun run() { 32 | send("Hello from job ${circleCi.jobName}") 33 | } 34 | } 35 | 36 | MySlackCli().main(args) 37 | ``` 38 | -------------------------------------------------------------------------------- /scripts-google/src/jvmMain/kotlin/com/freeletics/gradle/scripts/GooglePlayReleaseVersion.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | public sealed class GooglePlayReleaseVersion() { 4 | public abstract val name: String 5 | public abstract val code: Long 6 | 7 | public val namePrefix: String get() = name.split(".").run { 8 | check(size == 3) { "Invalid version $name" } 9 | "${get(0)}.${get(1)}" 10 | } 11 | 12 | public val codePrefix: Long get() = code / 1000 13 | 14 | public data class Simple( 15 | override val name: String, 16 | override val code: Long, 17 | ) : GooglePlayReleaseVersion() { 18 | override fun toString(): String { 19 | return "$name ($code)" 20 | } 21 | } 22 | 23 | public data class WithRollout( 24 | override val name: String, 25 | override val code: Long, 26 | val rolloutPercentage: Double, 27 | ) : GooglePlayReleaseVersion() { 28 | public fun asSimple(): GooglePlayReleaseVersion = Simple(name, code) 29 | 30 | override fun toString(): String { 31 | return "$name ($code) @ $rolloutPercentage" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/plugins-common/android-app.md: -------------------------------------------------------------------------------- 1 | # Android app plugin 2 | 3 | Add the following to the root `build.gradle` or `build.gradle.kts` to apply the plugin: 4 | 5 | ```groovy 6 | plugins { 7 | id("com.freeletics.gradle.android.app").version("") 8 | } 9 | ``` 10 | 11 | ## Defaults 12 | 13 | --8<-- "docs/defaults/base-1-configuration.md" 14 | --8<-- "docs/defaults/base-2-default-dependencies.md" 15 | --8<-- "docs/defaults/android-app-1-config.md" 16 | 17 | 18 | ## Features 19 | 20 | --8<-- "docs/features/base-1-explicit-api.md" 21 | --8<-- "docs/features/base-2-opt-in.md" 22 | --8<-- "docs/features/base-3-compose.md" 23 | --8<-- "docs/features/base-4-serialization.md" 24 | --8<-- "docs/features/base-5-metro.md" 25 | --8<-- "docs/features/base-6-khonshu.md" 26 | --8<-- "docs/features/base-6-poko.md" 27 | --8<-- "docs/features/base-7-kopy.md" 28 | --8<-- "docs/features/base-8-sqldelight.md" 29 | --8<-- "docs/features/base-9-room.md" 30 | --8<-- "docs/features/base-10-burst.md" 31 | --8<-- "docs/features/base-11-oss-publishing.md" 32 | --8<-- "docs/features/base-12-internal-publishing.md" 33 | --8<-- "docs/features/android-app-1-buildconfig.md" 34 | --8<-- "docs/features/android-app-2-resvalue.md" 35 | -------------------------------------------------------------------------------- /scripts-google/src/jvmMain/kotlin/com/freeletics/gradle/scripts/GoogleSheetsReader.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport 4 | import com.google.api.client.json.gson.GsonFactory 5 | import com.google.api.services.sheets.v4.Sheets 6 | import com.google.api.services.sheets.v4.SheetsScopes.SPREADSHEETS_READONLY 7 | import com.google.auth.http.HttpCredentialsAdapter 8 | import com.google.auth.oauth2.GoogleCredentials 9 | import java.io.ByteArrayInputStream 10 | 11 | public class GoogleSheetsReader(applicationName: String, jsonKey: String) { 12 | private val credentials = GoogleCredentials.fromStream(ByteArrayInputStream(jsonKey.toByteArray())) 13 | .createScoped(listOf(SPREADSHEETS_READONLY)) 14 | 15 | private val sheets = Sheets.Builder( 16 | GoogleNetHttpTransport.newTrustedTransport(), 17 | GsonFactory.getDefaultInstance(), 18 | HttpCredentialsAdapter(credentials), 19 | ).setApplicationName(applicationName).build() 20 | 21 | public fun read(spreadsheetId: String, range: String): List> { 22 | return sheets.spreadsheets() 23 | .values() 24 | .get(spreadsheetId, range) 25 | .execute()!! 26 | .getValues() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/plugin/FreeleticsMultiplatformPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.plugin 2 | 3 | import com.freeletics.gradle.setup.configureStandaloneLint 4 | import com.freeletics.gradle.setup.disableDefaultJsRepositories 5 | import com.freeletics.gradle.util.compilerOptionsCommon 6 | import com.freeletics.gradle.util.freeleticsExtension 7 | import com.freeletics.gradle.util.kotlin 8 | import com.freeletics.gradle.util.kotlinMultiplatform 9 | import org.gradle.api.Plugin 10 | import org.gradle.api.Project 11 | 12 | public abstract class FreeleticsMultiplatformPlugin : Plugin { 13 | override fun apply(target: Project) { 14 | target.plugins.apply("org.jetbrains.kotlin.multiplatform") 15 | target.plugins.apply(FreeleticsBasePlugin::class.java) 16 | 17 | target.freeleticsExtension.extensions.create("multiplatform", FreeleticsMultiplatformExtension::class.java) 18 | 19 | target.kotlinMultiplatform { 20 | applyDefaultHierarchyTemplate() 21 | } 22 | 23 | target.kotlin { 24 | compilerOptionsCommon { 25 | freeCompilerArgs.add("-Xexpect-actual-classes") 26 | } 27 | } 28 | 29 | target.configureStandaloneLint() 30 | target.disableDefaultJsRepositories() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/defaults/base-2-default-dependencies.md: -------------------------------------------------------------------------------- 1 | ### Default dependencies 2 | 3 | The plugins can dynamically add default dependencies to all modules. This can be enabled 4 | by creating any of the following bundles: 5 | 6 | ```toml 7 | [bundles] 8 | # any dependency in this bundle is automatically added to all modules as implementation dependency 9 | default-all = [ ... ] 10 | # any dependency in this bundle is automatically added to all modules as compileOnly dependency 11 | default-all-compile = [ ... ] 12 | # any dependency in this bundle is automatically added to all Android modules as implementation dependency 13 | default-android = [ ... ] 14 | # any dependency in this bundle is automatically added to all Android modules as compileOnly dependency 15 | default-android-compile = [ ... ] 16 | # any dependency in this bundle is automatically added to all modules as testImplementation dependency 17 | default-testing = [ ... ] 18 | # any dependency in this bundle is automatically added to all modules as testCompileOnly dependency 19 | default-testing-compile = [ ... ] 20 | # any dependency in this bundle is automatically added to all modules as testRuntimeOnly dependency 21 | default-testing-runtime = [ ... ] 22 | # any dependency in this bundle is automatically added to all modules as lintChecks dependency 23 | default-lint = [ ... ] 24 | ``` 25 | -------------------------------------------------------------------------------- /scripts-google/src/jvmMain/kotlin/com/freeletics/gradle/scripts/TimeoutHttpCredentialsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler 4 | import com.google.api.client.http.HttpRequest 5 | import com.google.api.client.util.ExponentialBackOff 6 | import com.google.auth.Credentials 7 | import com.google.auth.http.HttpCredentialsAdapter 8 | import kotlin.time.Duration 9 | 10 | /** 11 | * Wrapper around [Credentials] that increases the request timeout. 12 | */ 13 | internal class TimeoutHttpCredentialsAdapter( 14 | credentials: Credentials, 15 | private val timeout: Duration? = null, 16 | ) : HttpCredentialsAdapter(credentials) { 17 | override fun initialize(request: HttpRequest) { 18 | if (timeout != null) { 19 | val backOffHandler = HttpBackOffUnsuccessfulResponseHandler( 20 | ExponentialBackOff.Builder() 21 | .setMaxElapsedTimeMillis(timeout.inWholeMilliseconds.toInt()) 22 | .build(), 23 | ) 24 | request.setConnectTimeout(timeout.inWholeMilliseconds.toInt()) 25 | request.setReadTimeout(timeout.inWholeMilliseconds.toInt()) 26 | request.setUnsuccessfulResponseHandler(backOffHandler) 27 | } 28 | super.initialize(request) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/plugin/FreeleticsAndroidAppExtension.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.plugin 2 | 3 | import com.freeletics.gradle.util.androidApp 4 | import org.gradle.api.Project 5 | 6 | public abstract class FreeleticsAndroidAppExtension(private val project: Project) { 7 | public fun buildConfigField(type: String, name: String, value: String) { 8 | project.androidApp { 9 | defaultConfig.buildConfigField(type, name, value) 10 | } 11 | } 12 | 13 | public fun buildConfigField(type: String, name: String, debugValue: String, releaseValue: String) { 14 | project.androidApp { 15 | buildTypes.getByName("debug").buildConfigField(type, name, debugValue) 16 | buildTypes.getByName("release").buildConfigField(type, name, releaseValue) 17 | } 18 | } 19 | 20 | public fun resValue(type: String, name: String, value: String) { 21 | project.androidApp { 22 | defaultConfig.resValue(type, name, value) 23 | } 24 | } 25 | 26 | public fun resValue(type: String, name: String, debugValue: String, releaseValue: String) { 27 | project.androidApp { 28 | buildTypes.getByName("debug").resValue(type, name, debugValue) 29 | buildTypes.getByName("release").resValue(type, name, releaseValue) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Freeletics Gradle plugins 2 | 3 | A collection of convention plugins and build tools that are used internally at Freeletics. 4 | 5 | - [monorepo plugins](docs/monorepo.md) which are specific to the structure of the Freeletics Android mono repository, 6 | provide more defaults and are more opinionated 7 | - [base plugins](docs/plugins-common) which share the same basic configuration of the monorepo plugins but don't require 8 | a specific module structure. These are used internally by the monoroepo plugins and we are using these for open source 9 | some projects (like this one) and some secondary projects. 10 | - scripts: a collection of ready to use command line scripts and helper that can be used from `.main.kts` scripts 11 | 12 | 13 | # License 14 | 15 | ``` 16 | Copyright 2023 Freeletics GmbH. 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | You may obtain a copy of the License at 21 | 22 | http://www.apache.org/licenses/LICENSE-2.0 23 | 24 | Unless required by applicable law or agreed to in writing, software 25 | distributed under the License is distributed on an "AS IS" BASIS, 26 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | See the License for the specific language governing permissions and 28 | limitations under the License. 29 | ``` 30 | -------------------------------------------------------------------------------- /plugins/src/test/kotlin/com/freeletics/gradle/monorepo/tasks/ComputeVersionNameTest.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.tasks 2 | 3 | import com.freeletics.gradle.monorepo.util.FakeGit 4 | import com.google.common.truth.Truth.assertThat 5 | import org.junit.Assert 6 | import org.junit.Test 7 | 8 | internal class ComputeVersionNameTest { 9 | @Test 10 | fun `when there is a matching tag`() { 11 | val git = FakeGit( 12 | branch = "main", 13 | describeNonExact = "fl/v4.5.6", 14 | ) 15 | 16 | assertThat(computeVersionName(git, "fl")).isEqualTo("4.5.6") 17 | } 18 | 19 | @Test 20 | fun `when there is a matching tag in the past`() { 21 | val git = FakeGit( 22 | branch = "main", 23 | describeNonExact = "fl/v4.5.6-23-abcdefghij", 24 | ) 25 | 26 | assertThat(computeVersionName(git, "fl")).isEqualTo("4.5.6-23-abcdefghij") 27 | } 28 | 29 | @Test 30 | fun `when there are no tags`() { 31 | val git = FakeGit( 32 | branch = "main", 33 | describeNonExact = "", 34 | ) 35 | 36 | val exception = Assert.assertThrows(IllegalStateException::class.java) { 37 | computeVersionName(git, "fl") 38 | } 39 | assertThat(exception).hasMessageThat().isEqualTo( 40 | "Did not find a previous release/tag", 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /plugin-settings/api/plugin-settings.api: -------------------------------------------------------------------------------- 1 | public abstract class com/freeletics/gradle/plugin/SettingsExtension { 2 | public fun (Lorg/gradle/api/initialization/Settings;)V 3 | public final fun discoverProjectsIn (Z[Ljava/lang/String;)V 4 | public final fun discoverProjectsIn ([Ljava/lang/String;)V 5 | public static synthetic fun discoverProjectsIn$default (Lcom/freeletics/gradle/plugin/SettingsExtension;Z[Ljava/lang/String;ILjava/lang/Object;)V 6 | public final fun includeFlowRedux ()V 7 | public final fun includeFlowRedux (Ljava/lang/String;)V 8 | public static synthetic fun includeFlowRedux$default (Lcom/freeletics/gradle/plugin/SettingsExtension;Ljava/lang/String;ILjava/lang/Object;)V 9 | public final fun includeKhonshu ()V 10 | public final fun includeKhonshu (Ljava/lang/String;)V 11 | public static synthetic fun includeKhonshu$default (Lcom/freeletics/gradle/plugin/SettingsExtension;Ljava/lang/String;ILjava/lang/Object;)V 12 | public final fun snapshots ()V 13 | public final fun snapshots (Ljava/lang/String;)V 14 | public static synthetic fun snapshots$default (Lcom/freeletics/gradle/plugin/SettingsExtension;Ljava/lang/String;ILjava/lang/Object;)V 15 | } 16 | 17 | public abstract class com/freeletics/gradle/plugin/SettingsPlugin : org/gradle/api/Plugin { 18 | public fun ()V 19 | public synthetic fun apply (Ljava/lang/Object;)V 20 | public fun apply (Lorg/gradle/api/initialization/Settings;)V 21 | } 22 | 23 | -------------------------------------------------------------------------------- /docs/plugins-monorepo/core.md: -------------------------------------------------------------------------------- 1 | # Core plugin 2 | 3 | Add the following to the root `build.gradle` or `build.gradle.kts` to apply the plugin: 4 | 5 | ```groovy 6 | plugins { 7 | id("com.freeletics.gradle.core").version("") 8 | } 9 | ``` 10 | 11 | ## Defaults 12 | 13 | --8<-- "docs/defaults/base-1-configuration.md" 14 | --8<-- "docs/defaults/base-2-default-dependencies.md" 15 | --8<-- "docs/defaults/multiplatform-1-kotlin.md" 16 | --8<-- "docs/defaults/multiplatform-android-1-config.md" 17 | 18 | ## Features 19 | 20 | --8<-- "docs/features/base-1-explicit-api.md" 21 | --8<-- "docs/features/base-2-opt-in.md" 22 | --8<-- "docs/features/base-3-compose.md" 23 | --8<-- "docs/features/base-4-serialization.md" 24 | --8<-- "docs/features/base-5-metro.md" 25 | --8<-- "docs/features/base-6-khonshu.md" 26 | --8<-- "docs/features/base-6-poko.md" 27 | --8<-- "docs/features/base-7-kopy.md" 28 | --8<-- "docs/features/base-8-sqldelight.md" 29 | --8<-- "docs/features/base-9-room.md" 30 | --8<-- "docs/features/base-10-burst.md" 31 | --8<-- "docs/features/base-11-oss-publishing.md" 32 | --8<-- "docs/features/base-12-internal-publishing.md" 33 | --8<-- "docs/features/multiplatform-1-targets.md" 34 | --8<-- "docs/features/multiplatform-2-compose-resources.md" 35 | --8<-- "docs/features/multiplatform-3-skie.md" 36 | --8<-- "docs/features/multiplatform-android-1-paparazzi.md) 37 | --8<-- "docs/features/multiplatform-android-2-parcelize.md) 38 | --8<-- "docs/features/multiplatform-android-3-resources.md) 39 | --8<-- "docs/features/multiplatform-android-4-proguard.md) 40 | -------------------------------------------------------------------------------- /scripts-s3/src/jvmMain/kotlin/com/freeletics/gradle/scripts/S3Options.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.github.ajalt.clikt.parameters.groups.OptionGroup 4 | import com.github.ajalt.clikt.parameters.options.convert 5 | import com.github.ajalt.clikt.parameters.options.option 6 | import com.github.ajalt.clikt.parameters.options.required 7 | import kotlin.time.Duration 8 | 9 | public interface BaseS3Options { 10 | public val region: String 11 | public val bucket: String 12 | public val key: String 13 | public val validFor: Duration 14 | } 15 | 16 | public data class SimpleS3Options( 17 | override val region: String, 18 | override val bucket: String, 19 | override val key: String, 20 | override val validFor: Duration, 21 | ) : BaseS3Options 22 | 23 | public class S3Options : OptionGroup("AWS S3 options"), BaseS3Options { 24 | override val region: String by option( 25 | "--s3-region", 26 | envvar = "AWS_REGION", 27 | help = "The region that the bucket is in", 28 | ).required() 29 | 30 | override val bucket: String by option( 31 | "--s3-bucket", 32 | help = "The bucket to upload to", 33 | ).required() 34 | 35 | override val key: String by option( 36 | "--s3-key", 37 | help = "The key of the uploaded object", 38 | ).required() 39 | 40 | override val validFor: Duration by option( 41 | "--valid-for", 42 | help = "How long the URL is valid for in ISO-8601 Duration format", 43 | ).convert { Duration.parse(it) }.required() 44 | } 45 | -------------------------------------------------------------------------------- /docs/plugins-monorepo/domain.md: -------------------------------------------------------------------------------- 1 | # Domain plugin 2 | 3 | Add the following to the root `build.gradle` or `build.gradle.kts` to apply the plugin: 4 | 5 | ```groovy 6 | plugins { 7 | id("com.freeletics.gradle.domain").version("") 8 | } 9 | ``` 10 | 11 | ## Default configurations 12 | 13 | --8<-- "docs/defaults/base-1-configuration.md" 14 | --8<-- "docs/defaults/base-2-default-dependencies.md" 15 | --8<-- "docs/defaults/multiplatform-1-kotlin.md" 16 | --8<-- "docs/defaults/multiplatform-android-1-config.md" 17 | 18 | ## Features 19 | 20 | --8<-- "docs/features/base-1-explicit-api.md" 21 | --8<-- "docs/features/base-2-opt-in.md" 22 | --8<-- "docs/features/base-3-compose.md" 23 | --8<-- "docs/features/base-4-serialization.md" 24 | --8<-- "docs/features/base-5-metro.md" 25 | --8<-- "docs/features/base-6-khonshu.md" 26 | --8<-- "docs/features/base-6-poko.md" 27 | --8<-- "docs/features/base-7-kopy.md" 28 | --8<-- "docs/features/base-8-sqldelight.md" 29 | --8<-- "docs/features/base-9-room.md" 30 | --8<-- "docs/features/base-10-burst.md" 31 | --8<-- "docs/features/base-11-oss-publishing.md" 32 | --8<-- "docs/features/base-12-internal-publishing.md" 33 | --8<-- "docs/features/multiplatform-1-targets.md" 34 | --8<-- "docs/features/multiplatform-2-compose-resources.md" 35 | --8<-- "docs/features/multiplatform-3-skie.md" 36 | --8<-- "docs/features/multiplatform-android-1-paparazzi.md" 37 | --8<-- "docs/features/multiplatform-android-2-parcelize.md" 38 | --8<-- "docs/features/multiplatform-android-3-resources.md" 39 | --8<-- "docs/features/multiplatform-android-4-proguard.md) 40 | -------------------------------------------------------------------------------- /docs/plugins-common/multiplatform.md: -------------------------------------------------------------------------------- 1 | # Multiplatform plugin 2 | 3 | Add the following to the root `build.gradle` or `build.gradle.kts` to apply the plugin: 4 | 5 | ```groovy 6 | plugins { 7 | id("com.freeletics.gradle.multiplatform").version("") 8 | } 9 | ``` 10 | 11 | ## Defaults 12 | 13 | --8<-- "docs/defaults/base-1-configuration.md" 14 | --8<-- "docs/defaults/base-2-default-dependencies.md" 15 | --8<-- "docs/defaults/multiplatform-1-kotlin.md" 16 | --8<-- "docs/defaults/multiplatform-android-1-config.md" 17 | 18 | ## Features 19 | 20 | --8<-- "docs/features/base-1-explicit-api.md" 21 | --8<-- "docs/features/base-2-opt-in.md" 22 | --8<-- "docs/features/base-3-compose.md" 23 | --8<-- "docs/features/base-4-serialization.md" 24 | --8<-- "docs/features/base-5-metro.md" 25 | --8<-- "docs/features/base-6-khonshu.md" 26 | --8<-- "docs/features/base-6-poko.md" 27 | --8<-- "docs/features/base-7-kopy.md" 28 | --8<-- "docs/features/base-8-sqldelight.md" 29 | --8<-- "docs/features/base-9-room.md" 30 | --8<-- "docs/features/base-10-burst.md" 31 | --8<-- "docs/features/base-11-oss-publishing.md" 32 | --8<-- "docs/features/base-12-internal-publishing.md" 33 | --8<-- "docs/features/multiplatform-1-targets.md" 34 | --8<-- "docs/features/multiplatform-2-compose-resources.md" 35 | --8<-- "docs/features/multiplatform-3-skie.md" 36 | --8<-- "docs/features/multiplatform-android-1-paparazzi.md" 37 | --8<-- "docs/features/multiplatform-android-2-parcelize.md" 38 | --8<-- "docs/features/multiplatform-android-3-resources.md" 39 | --8<-- "docs/features/multiplatform-android-4-proguard.md" 40 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | publish: 10 | 11 | runs-on: macos-latest 12 | if: github.repository == 'freeletics/freeletics-gradle-plugins' 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v6.0.1 17 | 18 | - name: Install JDK 19 | uses: actions/setup-java@v5.1.0 20 | with: 21 | distribution: zulu 22 | java-version: 24 23 | 24 | - name: Get release notes 25 | run: | 26 | echo "RELEASE_NOTES<> $GITHUB_ENV 27 | echo "$(awk '/^## ${{ github.ref_name }}/{flag=1;next}/^## /{flag=0}flag' CHANGELOG.md)" >> $GITHUB_ENV 28 | echo "EOF" >> $GITHUB_ENV 29 | 30 | - name: Set version for tag 31 | run: | 32 | echo "ORG_GRADLE_PROJECT_VERSION_NAME=${{ github.ref_name }}" >> $GITHUB_ENV 33 | 34 | - uses: gradle/actions/setup-gradle@v5.0.0 35 | 36 | - name: Publish 37 | run: ./gradlew publish 38 | env: 39 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.CENTRAL_PORTAL_USERNAME }} 40 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.CENTRAL_PORTAL_PASSWORD }} 41 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_SIGNING_PRIVATE_KEY }} 42 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_SIGNING_PASSWORD }} 43 | 44 | - name: Create Release 45 | uses: softprops/action-gh-release@v2.5.0 46 | with: 47 | token: ${{ secrets.GITHUB_TOKEN }} 48 | body: ${{ env.RELEASE_NOTES }} 49 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/plugin/NavPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.plugin 2 | 3 | import com.freeletics.gradle.monorepo.setup.applyPlatformConstraints 4 | import com.freeletics.gradle.monorepo.setup.disableMultiplatformLibraryTasks 5 | import com.freeletics.gradle.monorepo.tasks.CheckDependencyRulesTask.Companion.registerCheckDependencyRulesTasks 6 | import com.freeletics.gradle.monorepo.util.ProjectType 7 | import com.freeletics.gradle.plugin.FreeleticsMultiplatformPlugin 8 | import com.freeletics.gradle.util.freeleticsExtension 9 | import com.freeletics.gradle.util.freeleticsMultiplatformExtension 10 | import org.gradle.api.Plugin 11 | import org.gradle.api.Project 12 | 13 | public abstract class NavPlugin : Plugin { 14 | override fun apply(target: Project) { 15 | target.plugins.apply(FreeleticsMultiplatformPlugin::class.java) 16 | target.freeleticsMultiplatformExtension.addDefaultTargets() 17 | 18 | target.freeleticsExtension.useSerialization() 19 | 20 | target.registerCheckDependencyRulesTasks( 21 | allowedProjectTypes = listOf(ProjectType.FEATURE_NAV), 22 | allowedDependencyProjectTypes = listOf( 23 | ProjectType.CORE_API, 24 | ProjectType.CORE_TESTING, 25 | ProjectType.CORE_DEBUG, 26 | ProjectType.DOMAIN_API, 27 | ProjectType.DOMAIN_TESTING, 28 | ProjectType.DOMAIN_DEBUG, 29 | ), 30 | ) 31 | 32 | target.applyPlatformConstraints(multiplatform = true) 33 | target.disableMultiplatformLibraryTasks() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/plugins-monorepo/nav.md: -------------------------------------------------------------------------------- 1 | # Nav plugin 2 | 3 | Add the following to the root `build.gradle` or `build.gradle.kts` to apply the plugin: 4 | 5 | ```groovy 6 | plugins { 7 | id("com.freeletics.gradle.nav").version("") 8 | } 9 | ``` 10 | 11 | ## Features enabled by default 12 | 13 | - `useSerialization()` is applied by default 14 | 15 | ## Default configurations 16 | 17 | --8<-- "docs/defaults/base-1-configuration.md" 18 | --8<-- "docs/defaults/base-2-default-dependencies.md" 19 | --8<-- "docs/defaults/multiplatform-1-kotlin.md" 20 | --8<-- "docs/defaults/multiplatform-android-1-config.md" 21 | 22 | ## Features 23 | 24 | --8<-- "docs/features/base-1-explicit-api.md" 25 | --8<-- "docs/features/base-2-opt-in.md" 26 | --8<-- "docs/features/base-3-compose.md" 27 | --8<-- "docs/features/base-4-serialization.md" 28 | --8<-- "docs/features/base-5-metro.md" 29 | --8<-- "docs/features/base-6-khonshu.md" 30 | --8<-- "docs/features/base-6-poko.md" 31 | --8<-- "docs/features/base-7-kopy.md" 32 | --8<-- "docs/features/base-8-sqldelight.md" 33 | --8<-- "docs/features/base-9-room.md" 34 | --8<-- "docs/features/base-10-burst.md" 35 | --8<-- "docs/features/base-11-oss-publishing.md" 36 | --8<-- "docs/features/base-12-internal-publishing.md" 37 | --8<-- "docs/features/multiplatform-1-targets.md" 38 | --8<-- "docs/features/multiplatform-2-compose-resources.md" 39 | --8<-- "docs/features/multiplatform-3-skie.md" 40 | --8<-- "docs/features/multiplatform-android-1-paparazzi.md" 41 | --8<-- "docs/features/multiplatform-android-2-parcelize.md" 42 | --8<-- "docs/features/multiplatform-android-3-resources.md" 43 | --8<-- "docs/features/multiplatform-android-4-proguard.md" 44 | -------------------------------------------------------------------------------- /scripts-s3/src/jvmMain/kotlin/com/freeletics/gradle/scripts/S3Uploader.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import aws.sdk.kotlin.services.s3.S3Client 4 | import aws.sdk.kotlin.services.s3.model.GetObjectRequest 5 | import aws.sdk.kotlin.services.s3.presigners.presignGetObject 6 | import aws.sdk.kotlin.services.s3.putObject 7 | import aws.smithy.kotlin.runtime.content.ByteStream 8 | import aws.smithy.kotlin.runtime.content.fromFile 9 | import java.io.File 10 | 11 | public suspend fun upload(byteArray: ByteArray, options: BaseS3Options): String { 12 | return upload(ByteStream.fromBytes(byteArray), options) 13 | } 14 | 15 | public suspend fun upload(content: String, options: BaseS3Options): String { 16 | return upload(ByteStream.fromString(content), options) 17 | } 18 | 19 | public suspend fun upload(file: File, options: BaseS3Options): String { 20 | return upload(ByteStream.fromFile(file), options) 21 | } 22 | 23 | private suspend fun upload(stream: ByteStream, options: BaseS3Options): String { 24 | S3Client { 25 | this.region = options.region 26 | }.use { s3 -> 27 | s3.putObject { 28 | bucket = options.bucket 29 | key = options.key 30 | body = stream 31 | if (options.key.endsWith(".html")) { 32 | contentType = "text/html" 33 | } 34 | } 35 | 36 | val unsignedRequest = GetObjectRequest { 37 | bucket = options.bucket 38 | key = options.key 39 | } 40 | val presignedRequest = s3.presignGetObject(unsignedRequest, options.validFor) 41 | return presignedRequest.url.toString() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/plugin/LegacyPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.plugin 2 | 3 | import com.freeletics.gradle.monorepo.setup.applyPlatformConstraints 4 | import com.freeletics.gradle.monorepo.setup.disableMultiplatformLibraryTasks 5 | import com.freeletics.gradle.monorepo.tasks.CheckDependencyRulesTask.Companion.registerCheckDependencyRulesTasks 6 | import com.freeletics.gradle.monorepo.util.ProjectType 7 | import com.freeletics.gradle.plugin.FreeleticsMultiplatformPlugin 8 | import com.freeletics.gradle.util.freeleticsAndroidMultiplatformExtension 9 | import com.freeletics.gradle.util.freeleticsMultiplatformExtension 10 | import org.gradle.api.Plugin 11 | import org.gradle.api.Project 12 | 13 | public abstract class LegacyPlugin : Plugin { 14 | override fun apply(target: Project) { 15 | target.plugins.apply(FreeleticsMultiplatformPlugin::class.java) 16 | target.freeleticsMultiplatformExtension.addDefaultTargets() 17 | 18 | target.freeleticsAndroidMultiplatformExtension.enableAndroidResources() 19 | 20 | target.registerCheckDependencyRulesTasks( 21 | allowedProjectTypes = listOf(ProjectType.LEGACY), 22 | allowedDependencyProjectTypes = listOfNotNull( 23 | ProjectType.CORE_API, 24 | ProjectType.CORE_TESTING, 25 | ProjectType.DOMAIN_API, 26 | ProjectType.DOMAIN_TESTING, 27 | ProjectType.FEATURE_NAV, 28 | ProjectType.LEGACY, 29 | ), 30 | ) 31 | 32 | target.applyPlatformConstraints(multiplatform = true) 33 | target.disableMultiplatformLibraryTasks() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/setup/ComposeSetup.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.setup 2 | 3 | import com.freeletics.gradle.util.booleanProperty 4 | import com.freeletics.gradle.util.compilerOptionsCommon 5 | import com.freeletics.gradle.util.kotlin 6 | import org.gradle.api.Project 7 | 8 | internal fun Project.setupCompose() { 9 | plugins.apply("org.jetbrains.kotlin.plugin.compose") 10 | 11 | val enableMetrics = project.booleanProperty("fgp.compose.enableCompilerMetrics", false) 12 | if (enableMetrics.get()) { 13 | val metricsFolderAbsolutePath = project.layout.buildDirectory 14 | .file("compose-metrics") 15 | .map { it.asFile.absolutePath } 16 | .get() 17 | 18 | kotlin { 19 | compilerOptionsCommon { 20 | freeCompilerArgs.addAll( 21 | "-P", 22 | "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$metricsFolderAbsolutePath", 23 | ) 24 | } 25 | } 26 | } 27 | 28 | val enableReports = project.booleanProperty("fgp.compose.enableCompilerReports", false) 29 | if (enableReports.get()) { 30 | val reportsFolderAbsolutePath = project.layout.buildDirectory 31 | .file("compose-reports") 32 | .map { it.asFile.absolutePath } 33 | .get() 34 | 35 | kotlin { 36 | compilerOptionsCommon { 37 | freeCompilerArgs.addAll( 38 | "-P", 39 | "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$reportsFolderAbsolutePath", 40 | ) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/plugins-monorepo/feature.md: -------------------------------------------------------------------------------- 1 | # Feature plugin 2 | 3 | Add the following to the root `build.gradle` or `build.gradle.kts` to apply the plugin: 4 | 5 | ```groovy 6 | plugins { 7 | id("com.freeletics.gradle.feature").version("") 8 | } 9 | ``` 10 | 11 | ## Features enabled by default 12 | 13 | - `useCompose()` is applied by default 14 | - `enableAndroidResources()` is applied by default 15 | 16 | ## Default configurations 17 | 18 | --8<-- "docs/defaults/base-1-configuration.md" 19 | --8<-- "docs/defaults/base-2-default-dependencies.md" 20 | --8<-- "docs/defaults/multiplatform-1-kotlin.md" 21 | --8<-- "docs/defaults/multiplatform-android-1-config.md" 22 | 23 | ## Features 24 | 25 | --8<-- "docs/features/base-1-explicit-api.md" 26 | --8<-- "docs/features/base-2-opt-in.md" 27 | --8<-- "docs/features/base-3-compose.md" 28 | --8<-- "docs/features/base-4-serialization.md" 29 | --8<-- "docs/features/base-5-metro.md" 30 | --8<-- "docs/features/base-6-khonshu.md" 31 | --8<-- "docs/features/base-6-poko.md" 32 | --8<-- "docs/features/base-7-kopy.md" 33 | --8<-- "docs/features/base-8-sqldelight.md" 34 | --8<-- "docs/features/base-9-room.md" 35 | --8<-- "docs/features/base-10-burst.md" 36 | --8<-- "docs/features/base-11-oss-publishing.md" 37 | --8<-- "docs/features/base-12-internal-publishing.md" 38 | --8<-- "docs/features/multiplatform-1-targets.md" 39 | --8<-- "docs/features/multiplatform-2-compose-resources.md" 40 | --8<-- "docs/features/multiplatform-3-skie.md" 41 | --8<-- "docs/features/multiplatform-android-1-paparazzi.md" 42 | --8<-- "docs/features/multiplatform-android-2-parcelize.md) 43 | --8<-- "docs/features/multiplatform-android-3-resources.md) 44 | --8<-- "docs/features/multiplatform-android-4-proguard.md) 45 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/setup/AndroidLintSetup.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.setup 2 | 3 | import com.android.build.api.dsl.Lint 4 | import com.freeletics.gradle.util.addMaybe 5 | import com.freeletics.gradle.util.getBundleOrNull 6 | import org.gradle.api.Project 7 | import org.gradle.api.file.RegularFile 8 | 9 | internal fun Project.configureStandaloneLint() { 10 | plugins.apply("com.android.lint") 11 | 12 | extensions.configure(Lint::class.java) { 13 | it.configure(project) 14 | } 15 | } 16 | 17 | internal fun Lint.configure(project: Project) { 18 | @Suppress("UnstableApiUsage") 19 | lintConfig = project.layout.settingsDirectory.file("gradle/lint.xml").asFile 20 | 21 | checkReleaseBuilds = false 22 | checkGeneratedSources = false 23 | checkTestSources = false 24 | checkDependencies = true 25 | ignoreTestSources = true 26 | abortOnError = true 27 | warningsAsErrors = true 28 | 29 | htmlReport = true 30 | htmlOutput = project.reportsFile("lint-result.html").asFile 31 | textReport = true 32 | textOutput = project.reportsFile("lint-result.txt").asFile 33 | 34 | disable += "NewerVersionAvailable" 35 | disable += "GradleDependency" 36 | disable += "AndroidGradlePluginVersion" 37 | 38 | project.dependencies.addMaybe("lintChecks", project.getBundleOrNull("default-lint")) 39 | } 40 | 41 | private fun Project.reportsFile(name: String): RegularFile { 42 | val projectName = project.path 43 | .replace("projects", "") 44 | .replaceFirst(":", "") 45 | .replace(":", "/") 46 | 47 | @Suppress("UnstableApiUsage") 48 | return project.layout.settingsDirectory.file("reports/lint/$projectName/$name") 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/publish-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Publish Snapshot 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags: 8 | - '**' 9 | 10 | jobs: 11 | publish: 12 | 13 | runs-on: macos-latest 14 | if: github.repository == 'freeletics/freeletics-gradle-plugins' 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v6.0.1 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Install JDK 23 | uses: actions/setup-java@v5.1.0 24 | with: 25 | distribution: zulu 26 | java-version: 24 27 | 28 | - name: Set version for main 29 | if: ${{ github.ref_name == 'main' || startsWith(github.ref, 'refs/tags/') }} 30 | run: | 31 | echo "ORG_GRADLE_PROJECT_VERSION_NAME=$(git describe --tags --abbrev=0 | awk -F. '/[0-9]+\./{$NF++;print}' OFS=.)-SNAPSHOT" >> $GITHUB_ENV 32 | 33 | - name: Set version for branch 34 | if: ${{ github.ref_name != 'main' && !startsWith(github.ref, 'refs/tags/') }} 35 | run: | 36 | echo "ORG_GRADLE_PROJECT_VERSION_NAME=${{ github.ref_name }}-SNAPSHOT" | sed 's#/#-#g' >> $GITHUB_ENV 37 | 38 | - uses: gradle/actions/setup-gradle@v5.0.0 39 | 40 | - name: Publish 41 | run: ./gradlew publish 42 | env: 43 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.CENTRAL_PORTAL_USERNAME }} 44 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.CENTRAL_PORTAL_PASSWORD }} 45 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_SIGNING_PRIVATE_KEY }} 46 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_SIGNING_PASSWORD }} 47 | if: "${{ env.ORG_GRADLE_PROJECT_mavenCentralUsername != '' }}" 48 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/plugin/CorePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.plugin 2 | 3 | import com.freeletics.gradle.monorepo.setup.applyPlatformConstraints 4 | import com.freeletics.gradle.monorepo.setup.disableMultiplatformLibraryTasks 5 | import com.freeletics.gradle.monorepo.tasks.CheckDependencyRulesTask.Companion.registerCheckDependencyRulesTasks 6 | import com.freeletics.gradle.monorepo.util.ProjectType 7 | import com.freeletics.gradle.monorepo.util.projectType 8 | import com.freeletics.gradle.plugin.FreeleticsMultiplatformPlugin 9 | import com.freeletics.gradle.util.freeleticsMultiplatformExtension 10 | import org.gradle.api.Plugin 11 | import org.gradle.api.Project 12 | 13 | public abstract class CorePlugin : Plugin { 14 | override fun apply(target: Project) { 15 | target.plugins.apply(FreeleticsMultiplatformPlugin::class.java) 16 | target.freeleticsMultiplatformExtension.addDefaultTargets() 17 | 18 | target.registerCheckDependencyRulesTasks( 19 | allowedProjectTypes = listOf( 20 | ProjectType.CORE_API, 21 | ProjectType.CORE_IMPLEMENTATION, 22 | ProjectType.CORE_TESTING, 23 | ProjectType.CORE_DEBUG, 24 | ), 25 | allowedDependencyProjectTypes = listOfNotNull( 26 | ProjectType.CORE_API, 27 | ProjectType.CORE_TESTING, 28 | ProjectType.CORE_DEBUG, 29 | ProjectType.CORE_IMPLEMENTATION.takeIf { target.projectType() == ProjectType.CORE_DEBUG }, 30 | ), 31 | ) 32 | 33 | target.applyPlatformConstraints(multiplatform = true) 34 | target.disableMultiplatformLibraryTasks() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Freeletics Gradle plugins 2 | site_url: https://freeletics.github.io/freeletics-gradle-plugins/ 3 | repo_name: Freeletics Gradle plugins 4 | repo_url: https://github.com/freeletics/freeletics-gradle-plugins 5 | site_description: "Freeletics Gradle plugins" 6 | site_author: Freeletics 7 | 8 | copyright: 'Copyright © 2023 Freeletics.' 9 | 10 | theme: 11 | name: 'material' 12 | favicon: images/icon-freeletics.svg 13 | logo: images/logo-freeletics.svg 14 | features: 15 | - navigation.sections 16 | - navigation.tracking 17 | - navigation.tabs 18 | - content.tabs.link 19 | - toc.integrate 20 | 21 | nav: 22 | - index.md 23 | - 'General plugins': 24 | - plugins-common/multiplatform.md 25 | - plugins-common/android-app.md 26 | - plugins-common/gradle.md 27 | - 'Monorepo': 28 | - 'Overview': monorepo.md 29 | - plugins-monorepo/app.md 30 | - plugins-monorepo/core.md 31 | - plugins-monorepo/domain.md 32 | - plugins-monorepo/feature.md 33 | - plugins-monorepo/nav.md 34 | - plugins-monorepo/root.md 35 | - plugins-monorepo/settings.md 36 | - changelog.md 37 | 38 | extra_css: 39 | - 'css/app.css' 40 | 41 | markdown_extensions: 42 | - smarty 43 | - codehilite: 44 | guess_lang: false 45 | - footnotes 46 | - meta 47 | - toc: 48 | permalink: true 49 | - pymdownx.betterem: 50 | smart_enable: all 51 | - pymdownx.caret 52 | - pymdownx.inlinehilite 53 | - pymdownx.magiclink 54 | - pymdownx.smartsymbols 55 | - pymdownx.superfences 56 | - pymdownx.tilde 57 | - pymdownx.superfences 58 | - admonition 59 | - pymdownx.details 60 | - pymdownx.tabbed: 61 | alternate_style: true 62 | - tables 63 | - pymdownx.snippets 64 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/util/Git.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.util 2 | 3 | import java.io.File 4 | import java.io.IOException 5 | import java.lang.RuntimeException 6 | import java.util.concurrent.TimeUnit 7 | 8 | public interface Git { 9 | public fun branch(): String 10 | 11 | public fun commitSha(): String 12 | 13 | public fun commitTimestamp(): String 14 | 15 | public fun describe(match: String, exactMatch: Boolean): String 16 | } 17 | 18 | public class RealGit( 19 | private val rootDir: File, 20 | ) : Git { 21 | override fun branch(): String { 22 | return run("branch", "--show-current") 23 | } 24 | 25 | override fun commitSha(): String { 26 | return run("describe", "--match", "DO_NOT_MATCH", "--always", "--abbrev=10", "--dirty") 27 | } 28 | 29 | override fun commitTimestamp(): String { 30 | return run("show", "-s", "--format=%ci", commitSha()) 31 | } 32 | 33 | override fun describe(match: String, exactMatch: Boolean): String { 34 | return if (exactMatch) { 35 | run("describe", "--match", match, "--exact-match") 36 | } else { 37 | run("describe", "--match", match) 38 | } 39 | } 40 | 41 | private fun run(vararg command: String): String { 42 | try { 43 | val proc = ProcessBuilder("bash", "-c", "git ${command.joinToString(" ")}") 44 | .directory(rootDir) 45 | .start() 46 | 47 | proc.waitFor(60, TimeUnit.SECONDS) 48 | return proc.inputStream.bufferedReader().readText().trim() 49 | } catch (e: IOException) { 50 | throw RuntimeException("Git command ${command.toList()} failed", e) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/plugin/AppPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.plugin 2 | 3 | import com.freeletics.gradle.monorepo.setup.applyPlatformConstraints 4 | import com.freeletics.gradle.monorepo.setup.disableMultiplatformLibraryTasks 5 | import com.freeletics.gradle.monorepo.tasks.CheckDependencyRulesTask.Companion.registerCheckDependencyRulesTasks 6 | import com.freeletics.gradle.monorepo.util.ProjectType 7 | import com.freeletics.gradle.plugin.FreeleticsMultiplatformPlugin 8 | import com.freeletics.gradle.util.freeleticsMultiplatformExtension 9 | import org.gradle.api.Plugin 10 | import org.gradle.api.Project 11 | 12 | public abstract class AppPlugin : Plugin { 13 | override fun apply(target: Project) { 14 | target.plugins.apply(FreeleticsMultiplatformPlugin::class.java) 15 | target.freeleticsMultiplatformExtension.addDefaultTargets(xcFramework = true) 16 | 17 | target.registerCheckDependencyRulesTasks( 18 | allowedProjectTypes = listOf(ProjectType.APP), 19 | allowedDependencyProjectTypes = listOfNotNull( 20 | ProjectType.CORE_API, 21 | ProjectType.CORE_IMPLEMENTATION, 22 | ProjectType.CORE_TESTING, 23 | ProjectType.CORE_DEBUG, 24 | ProjectType.DOMAIN_API, 25 | ProjectType.DOMAIN_IMPLEMENTATION, 26 | ProjectType.DOMAIN_TESTING, 27 | ProjectType.DOMAIN_DEBUG, 28 | ProjectType.FEATURE_IMPLEMENTATION, 29 | ProjectType.FEATURE_NAV, 30 | ProjectType.FEATURE_DEBUG, 31 | ), 32 | ) 33 | 34 | target.applyPlatformConstraints(multiplatform = true) 35 | target.disableMultiplatformLibraryTasks() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scripts-slack/api/scripts-slack.api: -------------------------------------------------------------------------------- 1 | public final class com/freeletics/gradle/scripts/Platform : java/lang/Enum { 2 | public static final field ANDROID Lcom/freeletics/gradle/scripts/Platform; 3 | public static final field IOS Lcom/freeletics/gradle/scripts/Platform; 4 | public static fun getEntries ()Lkotlin/enums/EnumEntries; 5 | public final fun getValue ()Ljava/lang/String; 6 | public static fun valueOf (Ljava/lang/String;)Lcom/freeletics/gradle/scripts/Platform; 7 | public static fun values ()[Lcom/freeletics/gradle/scripts/Platform; 8 | } 9 | 10 | public final class com/freeletics/gradle/scripts/PlatformOptions : com/github/ajalt/clikt/parameters/groups/OptionGroup { 11 | public fun ()V 12 | public final fun getPlatform ()Lcom/freeletics/gradle/scripts/Platform; 13 | } 14 | 15 | public final class com/freeletics/gradle/scripts/ReleaseTrainOptions : com/github/ajalt/clikt/parameters/groups/OptionGroup { 16 | public fun ()V 17 | public final fun getBranch ()Ljava/lang/String; 18 | public final fun getDisplayName ()Ljava/lang/String; 19 | public final fun getName ()Ljava/lang/String; 20 | public final fun getVersionCode ()Ljava/lang/String; 21 | public final fun getVersionName ()Ljava/lang/String; 22 | } 23 | 24 | public abstract class com/freeletics/gradle/scripts/SlackMessageCli : com/github/ajalt/clikt/core/CliktCommand { 25 | public fun ()V 26 | public fun (Ljava/lang/String;)V 27 | public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 28 | protected abstract fun getWebHookUrl ()Ljava/lang/String; 29 | public fun help (Lcom/github/ajalt/clikt/core/Context;)Ljava/lang/String; 30 | protected final fun send (Ljava/lang/String;)V 31 | protected final fun send (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V 32 | } 33 | 34 | -------------------------------------------------------------------------------- /scripts-slack/src/jvmMain/kotlin/com/freeletics/gradle/scripts/SlackMessageCli.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.github.ajalt.clikt.core.CliktCommand 4 | import com.github.ajalt.clikt.core.Context 5 | import com.slack.api.Slack 6 | import com.slack.api.model.kotlin_extension.block.dsl.LayoutBlockDsl 7 | import com.slack.api.model.kotlin_extension.block.withBlocks 8 | import com.slack.api.webhook.Payload 9 | 10 | /** 11 | * Simple base class with some common setup to send Slack messages using web hooks. 12 | */ 13 | public abstract class SlackMessageCli(name: String? = null) : CliktCommand(name) { 14 | private val slack = Slack.getInstance() 15 | 16 | /** 17 | * The web hook url that the Slack message is sent to. It's recommended to implement this 18 | * using a clikt [com.github.ajalt.clikt.parameters.options.option]. 19 | */ 20 | protected abstract val webHookUrl: String 21 | 22 | /** 23 | * Sends the given message to [webHookUrl]. 24 | */ 25 | protected fun send(message: String) { 26 | val payload = Payload.builder() 27 | .text(message) 28 | .build() 29 | slack.send(webHookUrl, payload) 30 | } 31 | 32 | /** 33 | * Sends the given message to [webHookUrl]. The message is created using [builder], [fallbackMessage] is used 34 | * in case block based messages can't be displayed. 35 | */ 36 | protected fun send(fallbackMessage: String, builder: LayoutBlockDsl.() -> Unit) { 37 | val payload = Payload.builder() 38 | .text(fallbackMessage) 39 | .blocks(withBlocks(builder)) 40 | .build() 41 | slack.send(webHookUrl, payload) 42 | } 43 | 44 | override fun help(context: Context): String { 45 | return "CLI to send Slack messages" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | # TODO: replace with on push tag https://github.com/actions/deploy-pages/issues/76 8 | workflow_run: 9 | workflows: [Publish Release] 10 | types: 11 | - completed 12 | 13 | jobs: 14 | publish: 15 | 16 | runs-on: ubuntu-latest 17 | if: github.repository == 'freeletics/freeletics-gradle-plugins' 18 | 19 | permissions: 20 | contents: read 21 | pages: write 22 | id-token: write 23 | 24 | environment: 25 | name: github-pages 26 | url: ${{ steps.deployment.outputs.page_url }} 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v6.0.1 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Install JDK 35 | uses: actions/setup-java@v5.1.0 36 | with: 37 | distribution: zulu 38 | java-version: 24 39 | 40 | - name: Install Python 41 | uses: actions/setup-python@v6.1.0 42 | with: 43 | python-version: 3.x 44 | 45 | - name: Install MkDocs Material 46 | run: pip install mkdocs-material 47 | 48 | - name: Copy docs 49 | run: | 50 | # copy shared filed 51 | cp README.md docs/index.md 52 | cp CHANGELOG.md docs/changelog.md 53 | # Update links 54 | sed -i 's/docs\/monorepo.md/monorepo.md/' docs/index.md 55 | sed -i 's/docs\/plugins-common/\/general-plugins/' docs/index.md 56 | 57 | - name: Build MkDocs 58 | run: mkdocs build 59 | 60 | - name: Upload artifact 61 | uses: actions/upload-pages-artifact@v4.0.0 62 | with: 63 | path: site 64 | 65 | - name: Deploy to GitHub Pages 66 | id: deployment 67 | uses: actions/deploy-pages@v4.0.5 68 | -------------------------------------------------------------------------------- /plugin-settings/src/main/kotlin/com/freeletics/gradle/plugin/FindProjects.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.plugin 2 | 3 | import java.io.File 4 | import org.gradle.api.initialization.Settings 5 | 6 | internal fun Settings.discoverProjects(extensions: List, directories: List = emptyList()) { 7 | val root = settings.rootDir 8 | val rootPath = root.canonicalPath 9 | directories.ifEmpty { root.listFileNames() }.forEach { 10 | settings.discoverProjects(root.resolve(it), extensions, rootPath) 11 | } 12 | } 13 | 14 | private fun File.listFileNames() = listFiles()!!.map { it.name } 15 | 16 | private val ignoredDirectories = listOf("build", "gradle") 17 | 18 | private fun Settings.discoverProjects( 19 | directory: File, 20 | extensions: List, 21 | rootPath: String, 22 | depth: Int = 1, 23 | ) { 24 | if (!directory.isDirectory || directory.isHidden || ignoredDirectories.contains(directory.name)) { 25 | return 26 | } 27 | 28 | val relativePath = directory.path.substringAfter(rootPath) 29 | if (relativePath.isNotEmpty()) { 30 | val projectName = relativePath.replace(File.separator, ":") 31 | extensions.forEach { extension -> 32 | val expectedBuildFileName = "${projectName.drop(1).replace(":", "-")}.$extension" 33 | if (directory.resolve(expectedBuildFileName).exists()) { 34 | include(projectName) 35 | project(projectName).buildFileName = expectedBuildFileName 36 | return 37 | } 38 | } 39 | } 40 | 41 | if (depth < 3) { 42 | val files = directory.listFiles()!!.toList() 43 | if (files.none { it.name.startsWith("settings.gradle") }) { 44 | files.forEach { 45 | discoverProjects(it, extensions, rootPath, depth + 1) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /minify-common/src/jvmMain/resources/META-INF/proguard/common.pro: -------------------------------------------------------------------------------- 1 | # https://cs.android.com/android-studio/platform/tools/base/+/d732d3188323ed66e1248d06f49bd13de87d0684:build-system/gradle-core/src/main/resources/com/android/build/gradle/proguard-optimizations.txt 2 | -allowaccessmodification 3 | 4 | # https://cs.android.com/android-studio/platform/tools/base/+/710383a66f0637acc9a091f826d45ac7bb6d79cf:build-system/gradle-core/src/main/resources/com/android/build/gradle/proguard-common.txt;l=1-8 5 | # Preserve some attributes that may be required for reflection. 6 | -keepattributes AnnotationDefault, 7 | EnclosingMethod, 8 | InnerClasses, 9 | RuntimeVisibleAnnotations, 10 | RuntimeVisibleParameterAnnotations, 11 | RuntimeVisibleTypeAnnotations, 12 | Signature 13 | 14 | # https://cs.android.com/android-studio/platform/tools/base/+/710383a66f0637acc9a091f826d45ac7bb6d79cf:build-system/gradle-core/src/main/resources/com/android/build/gradle/proguard-common.txt;l=17-20 15 | # For native methods, see https://www.guardsquare.com/manual/configuration/examples#native 16 | -keepclasseswithmembernames,includedescriptorclasses class * { 17 | native ; 18 | } 19 | 20 | # https://cs.android.com/android-studio/platform/tools/base/+/710383a66f0637acc9a091f826d45ac7bb6d79cf:build-system/gradle-core/src/main/resources/com/android/build/gradle/proguard-common.txt;l=33-37 21 | # For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations 22 | -keepclassmembers enum * { 23 | public static **[] values(); 24 | public static ** valueOf(java.lang.String); 25 | } 26 | 27 | # https://cs.android.com/android-studio/platform/tools/base/+/710383a66f0637acc9a091f826d45ac7bb6d79cf:build-system/gradle-core/src/main/resources/com/android/build/gradle/proguard-common.txt;l=39-41 28 | -keepclassmembers class * implements android.os.Parcelable { 29 | public static final ** CREATOR; 30 | } 31 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/plugin/DomainPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.plugin 2 | 3 | import com.freeletics.gradle.monorepo.setup.applyPlatformConstraints 4 | import com.freeletics.gradle.monorepo.setup.disableMultiplatformLibraryTasks 5 | import com.freeletics.gradle.monorepo.tasks.CheckDependencyRulesTask.Companion.registerCheckDependencyRulesTasks 6 | import com.freeletics.gradle.monorepo.util.ProjectType 7 | import com.freeletics.gradle.monorepo.util.projectType 8 | import com.freeletics.gradle.plugin.FreeleticsMultiplatformPlugin 9 | import com.freeletics.gradle.util.freeleticsMultiplatformExtension 10 | import org.gradle.api.Plugin 11 | import org.gradle.api.Project 12 | 13 | public abstract class DomainPlugin : Plugin { 14 | override fun apply(target: Project) { 15 | target.plugins.apply(FreeleticsMultiplatformPlugin::class.java) 16 | target.freeleticsMultiplatformExtension.addDefaultTargets() 17 | 18 | target.registerCheckDependencyRulesTasks( 19 | allowedProjectTypes = listOf( 20 | ProjectType.DOMAIN_API, 21 | ProjectType.DOMAIN_IMPLEMENTATION, 22 | ProjectType.DOMAIN_TESTING, 23 | ProjectType.DOMAIN_DEBUG, 24 | ), 25 | allowedDependencyProjectTypes = listOfNotNull( 26 | ProjectType.CORE_API, 27 | ProjectType.CORE_TESTING, 28 | ProjectType.CORE_DEBUG, 29 | ProjectType.DOMAIN_API, 30 | ProjectType.DOMAIN_TESTING, 31 | ProjectType.DOMAIN_DEBUG, 32 | ProjectType.DOMAIN_IMPLEMENTATION.takeIf { target.projectType() == ProjectType.DOMAIN_DEBUG }, 33 | ProjectType.FEATURE_NAV.takeIf { target.projectType() == ProjectType.DOMAIN_IMPLEMENTATION }, 34 | ), 35 | ) 36 | 37 | target.applyPlatformConstraints(multiplatform = true) 38 | target.disableMultiplatformLibraryTasks() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts-formatting/src/jvmMain/kotlin/com/freeletics/gradle/scripts/FindFiles.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import java.nio.file.FileSystems 4 | import java.nio.file.FileVisitResult 5 | import java.nio.file.Path 6 | import java.nio.file.PathMatcher 7 | import kotlin.io.path.absolute 8 | import kotlin.io.path.isHidden 9 | import kotlin.io.path.name 10 | import kotlin.io.path.visitFileTree 11 | import kotlinx.coroutines.channels.trySendBlocking 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.asFlow 14 | import kotlinx.coroutines.flow.channelFlow 15 | import kotlinx.coroutines.flow.filter 16 | import kotlinx.coroutines.flow.map 17 | 18 | internal val kotlinMatcher = FileSystems.getDefault().getPathMatcher("glob:**/*.{kt,kts}") 19 | internal val gradleMatcher = FileSystems.getDefault().getPathMatcher("glob:**/*.{gradle,gradle.kts}") 20 | 21 | internal fun filesToFormat( 22 | files: List?, 23 | rootDirectory: Path, 24 | matcher: PathMatcher, 25 | ): Flow { 26 | if (files != null) { 27 | return files.asFlow() 28 | .filter { matcher.matches(it) } 29 | // needed for editorconfig to be found 30 | .map { it.absolute() } 31 | } 32 | 33 | return channelFlow { 34 | rootDirectory.visitFileTree { 35 | onPreVisitDirectory { it, _ -> 36 | val name = it.name 37 | if (it.isHidden() && name != ".") { 38 | FileVisitResult.SKIP_SUBTREE 39 | } else if (name == "build" || name == "res") { 40 | FileVisitResult.SKIP_SUBTREE 41 | } else { 42 | FileVisitResult.CONTINUE 43 | } 44 | } 45 | 46 | onVisitFile { file, _ -> 47 | if (matcher.matches(file)) { 48 | check(trySendBlocking(file).isSuccess) 49 | } 50 | FileVisitResult.CONTINUE 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/setup/PaparazziSetup.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.setup 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.attributes.java.TargetJvmEnvironment 5 | import org.gradle.api.tasks.Copy 6 | import org.gradle.api.tasks.testing.Test 7 | import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.Companion.attribute 8 | 9 | internal fun Project.configurePaparazzi() { 10 | plugins.apply("com.freeletics.fork.paparazzi") 11 | 12 | val copyFailures = tasks.register("copyPaparazziFailures", Copy::class.java) { 13 | it.from(layout.buildDirectory.dir("paparazzi/failures")) 14 | @Suppress("UnstableApiUsage") 15 | it.into(layout.settingsDirectory.dir("reports/paparazzi")) 16 | it.include("**/delta-*") 17 | } 18 | 19 | // run the copy task after the test task, finalizedBy will even run if the test task fails 20 | tasks.withType(Test::class.java).configureEach { 21 | it.finalizedBy(copyFailures) 22 | } 23 | 24 | val verify = tasks.named("verifyPaparazzi") { 25 | it.dependsOn(copyFailures) 26 | } 27 | tasks.named("check").configure { 28 | it.dependsOn(verify) 29 | } 30 | 31 | addConstraint("androidHostTestImplementation") 32 | } 33 | 34 | private fun Project.addConstraint(configuration: String) { 35 | dependencies.constraints { constraints -> 36 | constraints.add(configuration, "com.google.guava:guava") { constraint -> 37 | constraint.attributes { 38 | it.attribute( 39 | TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, 40 | objects.named(TargetJvmEnvironment::class.java, TargetJvmEnvironment.STANDARD_JVM), 41 | ) 42 | } 43 | constraint.because( 44 | "LayoutLib and sdk-common depend on Guava's -jre published variant." + 45 | "See https://github.com/cashapp/paparazzi/issues/906.", 46 | ) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/plugin/FreeleticsGradlePluginPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.plugin 2 | 3 | import com.freeletics.gradle.setup.configureStandaloneLint 4 | import com.freeletics.gradle.util.compilerOptionsJvm 5 | import com.freeletics.gradle.util.getDependency 6 | import com.freeletics.gradle.util.java 7 | import com.freeletics.gradle.util.javaTargetVersion 8 | import com.freeletics.gradle.util.kotlin 9 | import org.gradle.api.Plugin 10 | import org.gradle.api.Project 11 | import org.gradle.api.tasks.compile.JavaCompile 12 | import org.gradle.plugin.devel.tasks.ValidatePlugins 13 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 14 | 15 | public abstract class FreeleticsGradlePluginPlugin : Plugin { 16 | override fun apply(target: Project) { 17 | target.plugins.apply("java-gradle-plugin") 18 | target.plugins.apply("org.jetbrains.kotlin.jvm") 19 | target.plugins.apply(FreeleticsBasePlugin::class.java) 20 | 21 | target.java { 22 | sourceCompatibility = target.javaTargetVersion 23 | targetCompatibility = target.javaTargetVersion 24 | } 25 | 26 | target.tasks.withType(JavaCompile::class.java).configureEach { 27 | it.options.release.set(target.javaTargetVersion.majorVersion.toInt()) 28 | } 29 | 30 | target.kotlin { 31 | compilerOptionsJvm { 32 | // https://docs.gradle.org/current/userguide/compatibility.html#kotlin 33 | apiVersion.set(KotlinVersion.KOTLIN_2_2) 34 | languageVersion.set(KotlinVersion.KOTLIN_2_2) 35 | 36 | // https://github.com/gradle/gradle/issues/24871 37 | freeCompilerArgs.addAll("-Xsam-conversions=class", "-Xlambdas=class") 38 | } 39 | } 40 | 41 | target.tasks.withType(ValidatePlugins::class.java).configureEach { 42 | it.enableStricterValidation.set(true) 43 | } 44 | 45 | target.configureStandaloneLint() 46 | target.dependencies.add("lintChecks", target.getDependency("androidx-lint-gradle")) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/setup/KmpJsWasmRepositories.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.setup 2 | 3 | import org.gradle.api.Project 4 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 5 | import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnvSpec 6 | import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin 7 | import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin 8 | import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootEnvSpec 9 | import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenEnvSpec 10 | import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenPlugin 11 | import org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsEnvSpec 12 | import org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsPlugin 13 | import org.jetbrains.kotlin.gradle.targets.wasm.yarn.WasmYarnPlugin 14 | import org.jetbrains.kotlin.gradle.targets.wasm.yarn.WasmYarnRootEnvSpec 15 | 16 | // TODO remove after https://youtrack.jetbrains.com/issue/KT-68533 17 | internal fun Project.disableDefaultJsRepositories() { 18 | plugins.withType(NodeJsPlugin::class.java).configureEach { 19 | extensions.configure(NodeJsEnvSpec::class.java) { 20 | it.downloadBaseUrl.set(null) 21 | } 22 | } 23 | plugins.withType(YarnPlugin::class.java).configureEach { 24 | extensions.configure(YarnRootEnvSpec::class.java) { 25 | it.downloadBaseUrl.set(null) 26 | } 27 | } 28 | plugins.withType(WasmNodeJsPlugin::class.java).configureEach { 29 | extensions.configure(WasmNodeJsEnvSpec::class.java) { 30 | it.downloadBaseUrl.set(null) 31 | } 32 | } 33 | plugins.withType(WasmYarnPlugin::class.java).configureEach { 34 | extensions.configure(WasmYarnRootEnvSpec::class.java) { 35 | it.downloadBaseUrl.set(null) 36 | } 37 | } 38 | @OptIn(ExperimentalWasmDsl::class) 39 | plugins.withType(BinaryenPlugin::class.java).configureEach { 40 | extensions.configure(BinaryenEnvSpec::class.java) { 41 | it.downloadBaseUrl.set(null) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts-github/src/jvmMain/kotlin/com/freeletics/gradle/scripts/GithubOptions.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.github.ajalt.clikt.parameters.groups.OptionGroup 4 | import com.github.ajalt.clikt.parameters.options.option 5 | import com.github.ajalt.clikt.parameters.options.required 6 | 7 | public class GithubOptions : OptionGroup("Github options") { 8 | public val userName: String by option( 9 | "--project-username", 10 | envvar = "GITHUB_REPOSITORY_OWNER", 11 | help = "The username that this project belongs to", 12 | ).required() 13 | 14 | public val repoSlug: String by option( 15 | "--project-repository", 16 | envvar = "GITHUB_REPOSITORY", 17 | help = "The repository for this project", 18 | ).required() 19 | 20 | public val repoName: String 21 | get() = repoSlug.split("/")[1] 22 | 23 | private val serverUrl: String by option( 24 | "--server-url", 25 | envvar = "GITHUB_SERVER_URL", 26 | help = "The repository for this project", 27 | ).required() 28 | 29 | public val runId: String by option( 30 | "--run-id", 31 | envvar = "GITHUB_RUN_ID", 32 | help = "The repository for this project", 33 | ).required() 34 | 35 | public val jobUrl: String 36 | get() = "$serverUrl/$repoSlug/actions/runs/$runId" 37 | 38 | public val jobName: String by option( 39 | "--job-name", 40 | envvar = "GITHUB_JOB", 41 | help = "The name of the job", 42 | ).required() 43 | 44 | public val buildNumber: String by option( 45 | "--build-number", 46 | envvar = "GITHUB_RUN_ID", 47 | help = "The number of the job", 48 | ).required() 49 | 50 | public val branch: String by option( 51 | "--branch", 52 | envvar = "GITHUB_REF_NAME", 53 | help = "The branch that was built", 54 | ).required() 55 | 56 | public val commitSha1: String by option( 57 | "--commit-sha1", 58 | envvar = "GITHUB_SHA", 59 | help = "The sha1 of the commit that was built", 60 | ).required() 61 | } 62 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/setup/CrashlyticsSetup.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.setup 2 | 3 | import com.freeletics.gradle.monorepo.util.computeInfoFromGit 4 | import com.freeletics.gradle.util.androidApp 5 | import com.freeletics.gradle.util.androidComponentsApp 6 | import com.freeletics.gradle.util.getVersion 7 | import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension 8 | import com.google.gms.googleservices.GoogleServicesTask.Companion.registerProcessGoogleResourcesTask 9 | import org.gradle.api.Project 10 | import org.gradle.api.plugins.ExtensionAware 11 | 12 | internal fun Project.configureCrashlytics(uploadNativeSymbols: Boolean) { 13 | plugins.apply("com.google.firebase.crashlytics") 14 | 15 | androidApp { 16 | buildTypes { 17 | named("debug") { debugType -> 18 | debugType.buildConfigField("boolean", "CRASHLYTICS_ENABLED", "false") 19 | (debugType as ExtensionAware).extensions.configure(CrashlyticsExtension::class.java) { 20 | it.mappingFileUploadEnabled = false 21 | it.nativeSymbolUploadEnabled = false 22 | } 23 | } 24 | 25 | named("release") { releaseType -> 26 | val enabled = computeInfoFromGit.get() 27 | releaseType.buildConfigField("boolean", "CRASHLYTICS_ENABLED", "$enabled") 28 | (releaseType as ExtensionAware).extensions.configure(CrashlyticsExtension::class.java) { 29 | it.mappingFileUploadEnabled = enabled 30 | it.nativeSymbolUploadEnabled = enabled && uploadNativeSymbols 31 | } 32 | } 33 | } 34 | } 35 | 36 | project.dependencies.add("releaseApi", "com.freeletics.gradle:minify-crashlytics:${project.getVersion("fgp")}") 37 | 38 | androidComponentsApp { 39 | beforeVariants { variant -> 40 | // dummy task to make the crashlytics plugin work without the google-services plugin 41 | registerProcessGoogleResourcesTask(variant) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /plugins/src/test/kotlin/com/freeletics/gradle/monorepo/util/AppTypeTest.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.util 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import org.junit.Test 5 | 6 | class AppTypeTest { 7 | @Test 8 | fun `AppType for app module`() { 9 | assertThat(":app:freeletics".toAppType()).isEqualTo(AppType("freeletics")) 10 | } 11 | 12 | @Test 13 | fun `AppType for app module with dash`() { 14 | assertThat(":app:movement-tracking".toAppType()).isEqualTo(AppType("movement-tracking")) 15 | } 16 | 17 | @Test 18 | fun `AppType for app module with platform suffix`() { 19 | assertThat(":app:freeletics-android".toAppType()).isEqualTo(AppType("freeletics")) 20 | } 21 | 22 | @Test 23 | fun `AppType for app module with dash and platform suffix`() { 24 | assertThat(":app:movement-tracking-android".toAppType()).isEqualTo(AppType("movement-tracking")) 25 | } 26 | 27 | @Test 28 | fun `AppType for generic feature module`() { 29 | assertThat(":feature:foo:bar".toAppType()).isEqualTo(null) 30 | } 31 | 32 | @Test 33 | fun `AppType for feature module`() { 34 | assertThat(":feature-freeletics:foo:bar".toAppType()).isEqualTo(AppType("freeletics")) 35 | } 36 | 37 | @Test 38 | fun `AppType for feature module with dash`() { 39 | assertThat(":feature-movement-tracking:foo:bar".toAppType()).isEqualTo(AppType("movement-tracking")) 40 | } 41 | 42 | @Test 43 | fun `AppType for generic domain module`() { 44 | assertThat(":domain:foo:bar".toAppType()).isEqualTo(null) 45 | } 46 | 47 | @Test 48 | fun `AppType for domain module`() { 49 | assertThat(":domain-freeletics:foo:bar".toAppType()).isEqualTo(AppType("freeletics")) 50 | } 51 | 52 | @Test 53 | fun `AppType for domain module with dash`() { 54 | assertThat(":domain-movement-tracking:foo:bar".toAppType()).isEqualTo(AppType("movement-tracking")) 55 | } 56 | 57 | @Test 58 | fun `AppType for core module`() { 59 | assertThat(":core:foo:bar".toAppType()).isEqualTo(null) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/util/VersionCatalog.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.util 2 | 3 | import org.gradle.api.JavaVersion 4 | import org.gradle.api.Project 5 | import org.gradle.api.artifacts.ExternalModuleDependencyBundle 6 | import org.gradle.api.artifacts.MinimalExternalModuleDependency 7 | import org.gradle.api.artifacts.VersionCatalog 8 | import org.gradle.api.artifacts.VersionCatalogsExtension 9 | import org.gradle.api.artifacts.dsl.DependencyHandler 10 | import org.gradle.api.provider.Provider 11 | import org.gradle.jvm.toolchain.JavaLanguageVersion 12 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 13 | 14 | private val Project.libs: VersionCatalog 15 | get() = extensions.getByType(VersionCatalogsExtension::class.java).named("libs") 16 | 17 | internal fun Project.getDependency(name: String): Provider { 18 | return libs.findLibrary(name).orElseThrow { NoSuchElementException("Could not find library $name") } 19 | } 20 | 21 | internal fun Project.getDependencyOrNull(name: String): Provider? { 22 | return libs.findLibrary(name).orElseGet { null } 23 | } 24 | 25 | internal fun Project.getVersion(name: String): String { 26 | return getVersionOrNull(name) ?: throw NoSuchElementException("Could not find version $name") 27 | } 28 | 29 | internal fun Project.getVersionOrNull(name: String): String? { 30 | return libs.findVersion(name).orElseGet { null }?.requiredVersion 31 | } 32 | 33 | internal fun Project.getBundleOrNull(name: String): Provider? { 34 | return libs.findBundle(name).orElseGet { null } 35 | } 36 | 37 | internal fun DependencyHandler.addMaybe(name: String, dependency: Any?) { 38 | if (dependency != null) { 39 | add(name, dependency) 40 | } 41 | } 42 | 43 | internal val Project.javaTarget: String 44 | get() = getVersion("java-target") 45 | 46 | internal val Project.javaTargetVersion: JavaVersion 47 | get() = JavaVersion.toVersion(javaTarget) 48 | 49 | internal val Project.jvmTarget: JvmTarget 50 | get() = JvmTarget.fromTarget(javaTarget) 51 | 52 | internal val Project.javaToolchainVersion: JavaLanguageVersion 53 | get() = JavaLanguageVersion.of(getVersion("java-toolchain")) 54 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/tasks/VerifyLicensesTask.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.tasks 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.Project 5 | import org.gradle.api.file.RegularFileProperty 6 | import org.gradle.api.tasks.InputFile 7 | import org.gradle.api.tasks.PathSensitive 8 | import org.gradle.api.tasks.PathSensitivity 9 | import org.gradle.api.tasks.TaskAction 10 | import org.gradle.api.tasks.VerificationException 11 | import org.gradle.work.DisableCachingByDefault 12 | 13 | @DisableCachingByDefault(because = "Verification task without output") 14 | public abstract class VerifyLicensesTask : DefaultTask() { 15 | @get:InputFile 16 | @get:PathSensitive(PathSensitivity.NONE) 17 | public abstract val generatedJson: RegularFileProperty 18 | 19 | @get:InputFile 20 | @get:PathSensitive(PathSensitivity.NONE) 21 | public abstract val existingJson: RegularFileProperty 22 | 23 | @TaskAction 24 | public fun verify() { 25 | val generated = generatedJson.get().asFile 26 | .readLines() 27 | .joinToString(separator = "\n") { line -> if (line.contains("\"version\": \"")) "" else line } 28 | .trim() 29 | val existing = existingJson.get().asFile 30 | .readText() 31 | .trim() 32 | if (generated != existing) { 33 | throw VerificationException("Generated licenses changed. Run ./gradlew updateLicenses to update") 34 | } 35 | } 36 | 37 | internal companion object { 38 | fun Project.registerVerifyLicensesTask() { 39 | val task = tasks.register("verifyLicenses", VerifyLicensesTask::class.java) { task -> 40 | task.generatedJson.set( 41 | project.layout.buildDirectory.file("reports/licensee/androidRelease/artifacts.json"), 42 | ) 43 | task.existingJson.set( 44 | project.layout.projectDirectory.file("src/main/assets/license_acknowledgements.json"), 45 | ) 46 | 47 | task.dependsOn("licenseeAndroidRelease") 48 | } 49 | tasks.named("check").configure { 50 | it.dependsOn(task) 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/setup/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.setup 2 | 3 | import org.gradle.api.Project 4 | 5 | internal fun Project.applyPlatformConstraints(multiplatform: Boolean = false) { 6 | val platformDependency = dependencies.enforcedPlatform(project(":")) 7 | configurations.configureEach { config -> 8 | if (isPlatformConfigurationName(config.name, multiplatform)) { 9 | config.dependencies.add(platformDependency) 10 | } 11 | } 12 | } 13 | 14 | // adapted from https://github.com/ZacSweers/CatchUp/blob/347db46d82497990ff10c441ecc75c0c9eedf7c4/buildSrc/src/main/kotlin/dev/zacsweers/catchup/gradle/CatchUpPlugin.kt#L68-L80 15 | private fun isPlatformConfigurationName(name: String, multiplatform: Boolean): Boolean { 16 | // TODO: KT-61653 KMP fails on androidTest*Api dependencies 17 | if (multiplatform) { 18 | MULTIPLATFORM_API_IGNORE_CONFIGURATION.forEach { 19 | if (name.contains(it, ignoreCase = true) && name.endsWith("api", ignoreCase = true)) { 20 | return false 21 | } 22 | } 23 | } 24 | 25 | // Try trimming the flavor by just matching the prefix 26 | PLATFORM_CONFIGURATION_PREFIX.forEach { platformConfig -> 27 | if (name.startsWith(platformConfig, ignoreCase = true)) { 28 | return true 29 | } 30 | } 31 | // Try trimming the flavor by just matching the suffix 32 | PLATFORM_CONFIGURATION_SUFFIX.forEach { platformConfig -> 33 | if (name.endsWith(platformConfig, ignoreCase = true)) { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | 40 | private val PLATFORM_CONFIGURATION_PREFIX = setOf( 41 | "kapt", 42 | "ksp", 43 | "layoutlib", 44 | ) 45 | 46 | private val PLATFORM_CONFIGURATION_SUFFIX = setOf( 47 | "api", 48 | "coreLibraryDesugaring", 49 | "compileOnly", 50 | "implementation", 51 | "kapt", 52 | "ksp", 53 | "runtimeOnly", 54 | "androidTestUtil", 55 | "lintChecks", 56 | "lintRelease", 57 | ) 58 | 59 | private val MULTIPLATFORM_API_IGNORE_CONFIGURATION = setOf( 60 | "androidTest", 61 | "androidUnitTest", 62 | "androidDebugUnitTest", 63 | "androidReleaseUnitTest", 64 | "androidInstrumentedTest", 65 | ) 66 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/tasks/GoogleServicesTask.kt: -------------------------------------------------------------------------------- 1 | package com.google.gms.googleservices 2 | 3 | import com.android.build.api.variant.ApplicationVariantBuilder 4 | import com.freeletics.gradle.monorepo.util.capitalize 5 | import org.gradle.api.DefaultTask 6 | import org.gradle.api.GradleException 7 | import org.gradle.api.Project 8 | import org.gradle.api.file.RegularFileProperty 9 | import org.gradle.api.tasks.InputFile 10 | import org.gradle.api.tasks.OutputFile 11 | import org.gradle.api.tasks.PathSensitive 12 | import org.gradle.api.tasks.PathSensitivity 13 | import org.gradle.api.tasks.TaskAction 14 | import org.gradle.work.DisableCachingByDefault 15 | 16 | @DisableCachingByDefault(because = "Simple task") 17 | public abstract class GoogleServicesTask : DefaultTask() { 18 | @get:InputFile 19 | @get:PathSensitive(PathSensitivity.NONE) 20 | public abstract val googleServicesConfigFile: RegularFileProperty 21 | 22 | @get:OutputFile 23 | public abstract val gmpAppId: RegularFileProperty 24 | 25 | @TaskAction 26 | public fun extractAppId() { 27 | val regex = Regex("([A-z0-9:]+)<\\/string>") 28 | val googleServicesConfig = googleServicesConfigFile.get().asFile.readText() 29 | val appIdMatch = regex.find(googleServicesConfig) 30 | if (appIdMatch != null) { 31 | gmpAppId.get().asFile.writeText(appIdMatch.groupValues[1]) 32 | } else { 33 | throw GradleException("google_app_id not found in ${googleServicesConfigFile.get()}") 34 | } 35 | } 36 | 37 | internal companion object { 38 | fun Project.registerProcessGoogleResourcesTask(variant: ApplicationVariantBuilder) { 39 | val variantName = variant.name.capitalize() 40 | val googleServicesConfigFile = "src/${variant.buildType}/res/values/google-services.xml" 41 | val appIdFile = "gmpAppId/${variant.name}.txt" 42 | tasks.register("process${variantName}GoogleServices", GoogleServicesTask::class.java) { task -> 43 | task.googleServicesConfigFile.set(project.layout.projectDirectory.file(googleServicesConfigFile)) 44 | task.gmpAppId.set(project.layout.buildDirectory.file(appIdFile)) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/tasks/ComputeVersionNameTask.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.tasks 2 | 3 | import com.freeletics.gradle.monorepo.util.RealGit 4 | import com.freeletics.gradle.monorepo.util.computeInfoFromGit 5 | import org.gradle.api.DefaultTask 6 | import org.gradle.api.Project 7 | import org.gradle.api.file.RegularFileProperty 8 | import org.gradle.api.provider.Property 9 | import org.gradle.api.provider.Provider 10 | import org.gradle.api.tasks.Input 11 | import org.gradle.api.tasks.Internal 12 | import org.gradle.api.tasks.OutputFile 13 | import org.gradle.api.tasks.TaskAction 14 | import org.gradle.api.tasks.TaskProvider 15 | import org.gradle.api.tasks.UntrackedTask 16 | 17 | @UntrackedTask(because = "Relies on local git state") 18 | public abstract class ComputeVersionNameTask : DefaultTask() { 19 | @get:Input 20 | public abstract val computeFromGit: Property 21 | 22 | @get:Input 23 | public abstract val gitTagName: Property 24 | 25 | @get:Internal 26 | public abstract val gitRootDirectory: RegularFileProperty 27 | 28 | @get:OutputFile 29 | public abstract val outputFile: RegularFileProperty 30 | 31 | @TaskAction 32 | public fun action() { 33 | val versionName = if (computeFromGit.get()) { 34 | val git = RealGit(gitRootDirectory.get().asFile) 35 | computeVersionName(git, gitTagName.get()) 36 | } else { 37 | "99.0.0" 38 | } 39 | outputFile.get().asFile.writeText(versionName) 40 | } 41 | 42 | internal companion object { 43 | fun Project.registerComputeVersionNameTask(gitTagName: String): TaskProvider { 44 | return tasks.register("computeVersionName", ComputeVersionNameTask::class.java) { task -> 45 | task.computeFromGit.set(computeInfoFromGit) 46 | task.gitTagName.set(gitTagName) 47 | task.gitRootDirectory.set(rootDir) 48 | task.outputFile.set(layout.buildDirectory.file("intermediates/git/version-name.txt")) 49 | } 50 | } 51 | 52 | fun TaskProvider.mapOutput(): Provider { 53 | return flatMap { task -> 54 | task.outputFile.map { it.asFile.readText() } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/tasks/CheckDependencyRules.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.tasks 2 | 3 | import com.freeletics.gradle.monorepo.util.ProjectType 4 | import com.freeletics.gradle.monorepo.util.toAppType 5 | import com.freeletics.gradle.monorepo.util.toProjectType 6 | 7 | internal fun checkDependencyRules( 8 | projectPath: String, 9 | configurationName: String, 10 | dependencyPath: String, 11 | allowedProjectTypes: List, 12 | allowedDependencyProjectTypes: List, 13 | ): List { 14 | val errors = mutableListOf() 15 | 16 | val projectType = projectPath.toProjectType() 17 | val projectAppType = projectPath.toAppType() 18 | 19 | if (!allowedProjectTypes.contains(projectType)) { 20 | errors += "$projectPath is a ${projectType.fullName} project but the current plugin only allows " + 21 | allowedProjectTypes.joinToString(separator = ", ") { it.fullName } 22 | } 23 | 24 | val dependencyProjectType = dependencyPath.toProjectType() 25 | val dependencyAppType = dependencyPath.toAppType() 26 | 27 | if (!allowedDependencyProjectTypes.contains(dependencyProjectType)) { 28 | errors += "$projectPath is not allowed to depend on ${dependencyProjectType.fullName} module $dependencyPath" 29 | } 30 | 31 | if (dependencyAppType != null && dependencyAppType != projectAppType) { 32 | errors += "$projectPath is not allowed to depend on ${dependencyAppType.name} module $dependencyPath" 33 | } 34 | 35 | if (dependencyProjectType.suffix == "testing" && projectType.suffix != "testing") { 36 | if (!configurationName.contains("test", ignoreCase = true)) { 37 | errors += "$projectPath is not allowed to depend on testing module $dependencyPath " + 38 | "in configuration $configurationName" 39 | } 40 | } 41 | if (dependencyProjectType.suffix == "debug" && projectType.suffix != "debug" && projectType.suffix != "testing") { 42 | if (!configurationName.contains("debug", ignoreCase = true) && 43 | !configurationName.contains("test", ignoreCase = true) 44 | ) { 45 | errors += "$projectPath is not allowed to depend on debug module $dependencyPath " + 46 | "in configuration $configurationName" 47 | } 48 | } 49 | 50 | return errors 51 | } 52 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/tasks/ComputeGitShaTask.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.tasks 2 | 3 | import com.android.build.api.variant.BuildConfigField 4 | import com.freeletics.gradle.monorepo.util.RealGit 5 | import com.freeletics.gradle.monorepo.util.computeInfoFromGit 6 | import org.gradle.api.DefaultTask 7 | import org.gradle.api.Project 8 | import org.gradle.api.file.RegularFileProperty 9 | import org.gradle.api.provider.Property 10 | import org.gradle.api.provider.Provider 11 | import org.gradle.api.tasks.Input 12 | import org.gradle.api.tasks.Internal 13 | import org.gradle.api.tasks.OutputFile 14 | import org.gradle.api.tasks.TaskAction 15 | import org.gradle.api.tasks.TaskProvider 16 | import org.gradle.api.tasks.UntrackedTask 17 | 18 | @UntrackedTask(because = "Relies on local git state") 19 | public abstract class ComputeGitShaTask : DefaultTask() { 20 | @get:Input 21 | public abstract val computeFromGit: Property 22 | 23 | @get:Internal 24 | public abstract val gitRootDirectory: RegularFileProperty 25 | 26 | @get:OutputFile 27 | public abstract val outputFile: RegularFileProperty 28 | 29 | @TaskAction 30 | public fun action() { 31 | val gitSha = if (computeFromGit.get()) { 32 | val git = RealGit(gitRootDirectory.get().asFile) 33 | git.commitSha() 34 | } else { 35 | "sha-not-computed" 36 | } 37 | outputFile.get().asFile.writeText(gitSha) 38 | } 39 | 40 | internal companion object { 41 | fun Project.registerComputeGitShaTask(): TaskProvider { 42 | return tasks.register("computeGitSha", ComputeGitShaTask::class.java) { task -> 43 | task.computeFromGit.set(computeInfoFromGit) 44 | task.gitRootDirectory.set(rootDir) 45 | task.outputFile.set(layout.buildDirectory.file("intermediates/git/sha.txt")) 46 | } 47 | } 48 | 49 | fun TaskProvider.mapOutput(): Provider> { 50 | return flatMap { task -> 51 | task.outputFile.map { 52 | BuildConfigField( 53 | type = "String", 54 | value = "\"${it.asFile.readText()}\"", 55 | comment = null, 56 | ) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/tasks/ComputeVersionCodeTask.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.tasks 2 | 3 | import com.freeletics.gradle.monorepo.util.RealGit 4 | import com.freeletics.gradle.monorepo.util.computeInfoFromGit 5 | import java.time.LocalDateTime 6 | import org.gradle.api.DefaultTask 7 | import org.gradle.api.Project 8 | import org.gradle.api.file.RegularFileProperty 9 | import org.gradle.api.provider.Property 10 | import org.gradle.api.provider.Provider 11 | import org.gradle.api.tasks.Input 12 | import org.gradle.api.tasks.Internal 13 | import org.gradle.api.tasks.OutputFile 14 | import org.gradle.api.tasks.TaskAction 15 | import org.gradle.api.tasks.TaskProvider 16 | import org.gradle.api.tasks.UntrackedTask 17 | 18 | @UntrackedTask(because = "Relies on local git state") 19 | public abstract class ComputeVersionCodeTask : DefaultTask() { 20 | @get:Input 21 | public abstract val computeFromGit: Property 22 | 23 | @get:Input 24 | public abstract val gitTagName: Property 25 | 26 | @get:Internal 27 | public abstract val gitRootDirectory: RegularFileProperty 28 | 29 | @get:OutputFile 30 | public abstract val outputFile: RegularFileProperty 31 | 32 | @TaskAction 33 | public fun action() { 34 | val versionCode = if (computeFromGit.get()) { 35 | val git = RealGit(gitRootDirectory.get().asFile) 36 | computeVersionCode(git, gitTagName.get(), LocalDateTime.now()) 37 | } else { 38 | Int.MAX_VALUE 39 | } 40 | outputFile.get().asFile.writeText(versionCode.toString()) 41 | } 42 | 43 | internal companion object { 44 | fun Project.registerComputeVersionCodeTask(gitTagName: String): TaskProvider { 45 | return tasks.register("computeVersionCode", ComputeVersionCodeTask::class.java) { task -> 46 | task.computeFromGit.set(computeInfoFromGit) 47 | task.gitTagName.set(gitTagName) 48 | task.gitRootDirectory.set(rootDir) 49 | task.outputFile.set(layout.buildDirectory.file("intermediates/git/version-code.txt")) 50 | } 51 | } 52 | 53 | fun TaskProvider.mapOutput(): Provider { 54 | return flatMap { task -> 55 | task.outputFile.map { it.asFile.readText().toInt() } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/setup/AndroidTargetSetup.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.setup 2 | 3 | import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget 4 | import com.freeletics.gradle.plugin.FreeleticsMultiplatformAndroidExtension 5 | import com.freeletics.gradle.util.addCompileOnlyDependency 6 | import com.freeletics.gradle.util.addImplementationDependency 7 | import com.freeletics.gradle.util.addMaybe 8 | import com.freeletics.gradle.util.defaultPackageName 9 | import com.freeletics.gradle.util.freeleticsExtension 10 | import com.freeletics.gradle.util.getBundleOrNull 11 | import com.freeletics.gradle.util.getDependencyOrNull 12 | import com.freeletics.gradle.util.getVersion 13 | import com.freeletics.gradle.util.getVersionOrNull 14 | import kotlin.text.toInt 15 | import org.gradle.api.Project 16 | 17 | internal fun KotlinMultiplatformAndroidLibraryTarget.setupAndroidTarget( 18 | target: Project, 19 | configure: FreeleticsMultiplatformAndroidExtension.() -> Unit, 20 | ) { 21 | namespace = target.defaultPackageName() 22 | 23 | val buildTools = target.getVersionOrNull("android.buildTools") 24 | if (buildTools != null) { 25 | buildToolsVersion = buildTools 26 | } 27 | 28 | compileSdk = target.getVersion("android.compile").toInt() 29 | minSdk = target.getVersion("android.min").toInt() 30 | 31 | // default all features to false, they will be enabled through FreeleticsAndroidExtension 32 | androidResources.enable = false 33 | 34 | val desugarLibrary = target.getDependencyOrNull("android.desugarjdklibs") 35 | enableCoreLibraryDesugaring = desugarLibrary != null 36 | target.dependencies.addMaybe("coreLibraryDesugaring", desugarLibrary) 37 | 38 | lint.configure(target) 39 | 40 | // enable tests 41 | withHostTestBuilder {}.configure { 42 | isIncludeAndroidResources = true 43 | } 44 | 45 | // add default dependencies 46 | val bundle = target.getBundleOrNull("default-android") 47 | if (bundle != null) { 48 | target.addImplementationDependency(bundle, setOf("android")) 49 | } 50 | val compileBundle = target.getBundleOrNull("default-android-compile") 51 | if (compileBundle != null) { 52 | target.addCompileOnlyDependency(compileBundle, setOf("android")) 53 | } 54 | 55 | project.freeleticsExtension.extensions 56 | .create("android", FreeleticsMultiplatformAndroidExtension::class.java) 57 | .configure() 58 | } 59 | -------------------------------------------------------------------------------- /scripts-s3/api/scripts-s3.api: -------------------------------------------------------------------------------- 1 | public abstract interface class com/freeletics/gradle/scripts/BaseS3Options { 2 | public abstract fun getBucket ()Ljava/lang/String; 3 | public abstract fun getKey ()Ljava/lang/String; 4 | public abstract fun getRegion ()Ljava/lang/String; 5 | public abstract fun getValidFor-UwyO8pc ()J 6 | } 7 | 8 | public final class com/freeletics/gradle/scripts/QrCodeKt { 9 | public static final fun createQrCode (Ljava/lang/String;)[B 10 | } 11 | 12 | public final class com/freeletics/gradle/scripts/S3Options : com/github/ajalt/clikt/parameters/groups/OptionGroup, com/freeletics/gradle/scripts/BaseS3Options { 13 | public fun ()V 14 | public fun getBucket ()Ljava/lang/String; 15 | public fun getKey ()Ljava/lang/String; 16 | public fun getRegion ()Ljava/lang/String; 17 | public fun getValidFor-UwyO8pc ()J 18 | } 19 | 20 | public final class com/freeletics/gradle/scripts/S3UploaderKt { 21 | public static final fun upload (Ljava/io/File;Lcom/freeletics/gradle/scripts/BaseS3Options;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 22 | public static final fun upload (Ljava/lang/String;Lcom/freeletics/gradle/scripts/BaseS3Options;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 23 | public static final fun upload ([BLcom/freeletics/gradle/scripts/BaseS3Options;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 24 | } 25 | 26 | public final class com/freeletics/gradle/scripts/SimpleS3Options : com/freeletics/gradle/scripts/BaseS3Options { 27 | public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLkotlin/jvm/internal/DefaultConstructorMarker;)V 28 | public final fun component1 ()Ljava/lang/String; 29 | public final fun component2 ()Ljava/lang/String; 30 | public final fun component3 ()Ljava/lang/String; 31 | public final fun component4-UwyO8pc ()J 32 | public final fun copy-Wn2Vu4Y (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;J)Lcom/freeletics/gradle/scripts/SimpleS3Options; 33 | public static synthetic fun copy-Wn2Vu4Y$default (Lcom/freeletics/gradle/scripts/SimpleS3Options;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JILjava/lang/Object;)Lcom/freeletics/gradle/scripts/SimpleS3Options; 34 | public fun equals (Ljava/lang/Object;)Z 35 | public fun getBucket ()Ljava/lang/String; 36 | public fun getKey ()Ljava/lang/String; 37 | public fun getRegion ()Ljava/lang/String; 38 | public fun getValidFor-UwyO8pc ()J 39 | public fun hashCode ()I 40 | public fun toString ()Ljava/lang/String; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/setup/DisableTasks.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.setup 2 | 3 | import com.android.build.api.variant.AndroidComponentsExtension 4 | import com.freeletics.gradle.monorepo.util.capitalize 5 | import org.gradle.api.Project 6 | 7 | internal fun Project.disableAndroidApplicationTasks() { 8 | disableAndroidTasks(androidAppLintTasksToDisableExceptOneVariant, "debug") 9 | } 10 | 11 | internal fun Project.disableMultiplatformApplicationTasks() {} 12 | 13 | internal fun Project.disableMultiplatformLibraryTasks() { 14 | disableTasks( 15 | listOf( 16 | "assemble", 17 | "lint", 18 | "lintAndroidMain", 19 | "lintJvm", 20 | "lintReportAndroidMain", 21 | "lintReportJvm", 22 | "copyAndroidMainLintReports", 23 | "copyJvmLintReports", 24 | "lintFix", 25 | "updateLintBaseline", 26 | ), 27 | ) 28 | } 29 | 30 | private fun Project.disableAndroidTasks(names: List, variantToKeep: String = "") { 31 | extensions.configure>("androidComponents") { components -> 32 | components.onVariants { variant -> 33 | if (variant.name != variantToKeep) { 34 | val variantAwareNames = names.map { it.replace("{VARIANT}", variant.name.capitalize()) } 35 | disableTasks(variantAwareNames) 36 | } 37 | } 38 | } 39 | } 40 | 41 | private fun Project.disableTasks(names: List) { 42 | // since AGP 8.3 the tasks.named will fail during project sync 43 | if (providers.systemProperty("idea.sync.active").getOrElse("false").toBoolean()) { 44 | return 45 | } 46 | 47 | afterEvaluate { 48 | names.forEach { name -> 49 | tasks.named(name).configure { 50 | it.enabled = false 51 | it.description = "DISABLED" 52 | it.setDependsOn(mutableListOf()) 53 | } 54 | } 55 | } 56 | } 57 | 58 | // disable debug variant of these tasks, we're only running on release 59 | private val androidAppLintTasksToDisableExceptOneVariant get() = listOf( 60 | // analyze 61 | "lintAnalyze{VARIANT}", 62 | // report 63 | "lint{VARIANT}", 64 | "lintReport{VARIANT}", 65 | "copy{VARIANT}LintReports", 66 | // fix 67 | "lintFix{VARIANT}", 68 | // baseline 69 | "updateLintBaseline{VARIANT}", 70 | ) 71 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/plugin/FeaturePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.plugin 2 | 3 | import com.freeletics.gradle.monorepo.setup.applyPlatformConstraints 4 | import com.freeletics.gradle.monorepo.setup.disableMultiplatformLibraryTasks 5 | import com.freeletics.gradle.monorepo.tasks.CheckDependencyRulesTask.Companion.registerCheckDependencyRulesTasks 6 | import com.freeletics.gradle.monorepo.util.ProjectType 7 | import com.freeletics.gradle.monorepo.util.projectType 8 | import com.freeletics.gradle.plugin.FreeleticsMultiplatformPlugin 9 | import com.freeletics.gradle.util.freeleticsAndroidMultiplatformExtension 10 | import com.freeletics.gradle.util.freeleticsExtension 11 | import com.freeletics.gradle.util.freeleticsMultiplatformExtension 12 | import org.gradle.api.Plugin 13 | import org.gradle.api.Project 14 | 15 | public abstract class FeaturePlugin : Plugin { 16 | override fun apply(target: Project) { 17 | target.plugins.apply(FreeleticsMultiplatformPlugin::class.java) 18 | target.freeleticsMultiplatformExtension.addDefaultTargets() 19 | 20 | target.freeleticsExtension.useCompose() 21 | target.freeleticsAndroidMultiplatformExtension.enableAndroidResources() 22 | 23 | val legacy = target.freeleticsExtension.extensions.create("legacy", LegacyExtension::class.java) 24 | 25 | target.afterEvaluate { 26 | target.registerCheckDependencyRulesTasks( 27 | allowedProjectTypes = listOf( 28 | ProjectType.FEATURE_IMPLEMENTATION, 29 | ProjectType.FEATURE_DEBUG, 30 | ), 31 | allowedDependencyProjectTypes = listOfNotNull( 32 | ProjectType.CORE_API, 33 | ProjectType.CORE_TESTING, 34 | ProjectType.CORE_DEBUG, 35 | ProjectType.DOMAIN_API, 36 | ProjectType.DOMAIN_TESTING, 37 | ProjectType.DOMAIN_DEBUG, 38 | ProjectType.FEATURE_NAV, 39 | ProjectType.FEATURE_IMPLEMENTATION.takeIf { target.projectType() == ProjectType.FEATURE_DEBUG }, 40 | ProjectType.FEATURE_DEBUG, 41 | ProjectType.LEGACY.takeIf { legacy.allowLegacyDependencies }, 42 | ), 43 | ) 44 | } 45 | 46 | target.applyPlatformConstraints(multiplatform = true) 47 | target.disableMultiplatformLibraryTasks() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/tasks/ComputeGitTimestampTask.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.tasks 2 | 3 | import com.android.build.api.variant.BuildConfigField 4 | import com.freeletics.gradle.monorepo.util.RealGit 5 | import com.freeletics.gradle.monorepo.util.computeInfoFromGit 6 | import org.gradle.api.DefaultTask 7 | import org.gradle.api.Project 8 | import org.gradle.api.file.RegularFileProperty 9 | import org.gradle.api.provider.Property 10 | import org.gradle.api.provider.Provider 11 | import org.gradle.api.tasks.Input 12 | import org.gradle.api.tasks.Internal 13 | import org.gradle.api.tasks.OutputFile 14 | import org.gradle.api.tasks.TaskAction 15 | import org.gradle.api.tasks.TaskProvider 16 | import org.gradle.api.tasks.UntrackedTask 17 | 18 | @UntrackedTask(because = "Relies on local git state") 19 | public abstract class ComputeGitTimestampTask : DefaultTask() { 20 | @get:Input 21 | public abstract val computeFromGit: Property 22 | 23 | @get:Internal 24 | public abstract val gitRootDirectory: RegularFileProperty 25 | 26 | @get:OutputFile 27 | public abstract val outputFile: RegularFileProperty 28 | 29 | @TaskAction 30 | public fun action() { 31 | val timestamp = if (computeFromGit.get()) { 32 | val git = RealGit(gitRootDirectory.get().asFile) 33 | git.commitTimestamp() 34 | } else { 35 | "timestamp-not-computed" 36 | } 37 | outputFile.get().asFile.writeText(timestamp) 38 | } 39 | 40 | internal companion object { 41 | fun Project.registerComputeGitTimestampTask(): TaskProvider { 42 | return tasks.register("computeGitTimestamp", ComputeGitTimestampTask::class.java) { task -> 43 | task.computeFromGit.set(computeInfoFromGit) 44 | task.gitRootDirectory.set(rootDir) 45 | task.outputFile.set(layout.buildDirectory.file("intermediates/git/timestamp.txt")) 46 | } 47 | } 48 | 49 | fun TaskProvider.mapOutput(): Provider> { 50 | return flatMap { task -> 51 | task.outputFile.map { 52 | BuildConfigField( 53 | type = "String", 54 | value = "\"${it.asFile.readText()}\"", 55 | comment = null, 56 | ) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /scripts-google/src/jvmMain/kotlin/com/freeletics/gradle/scripts/GooglePlayPublisher.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport 4 | import com.google.api.client.json.gson.GsonFactory 5 | import com.google.api.services.androidpublisher.AndroidPublisher 6 | import com.google.api.services.androidpublisher.AndroidPublisherScopes.ANDROIDPUBLISHER 7 | import com.google.api.services.androidpublisher.model.AppEdit 8 | import com.google.auth.oauth2.GoogleCredentials 9 | import java.io.ByteArrayInputStream 10 | import java.io.File 11 | import kotlin.time.Duration.Companion.minutes 12 | 13 | public class GooglePlayPublisher( 14 | jsonKey: String, 15 | application: String, 16 | private val appId: String, 17 | ) { 18 | private val credentials = GoogleCredentials.fromStream(ByteArrayInputStream(jsonKey.toByteArray())) 19 | .createScoped(listOf(ANDROIDPUBLISHER)) 20 | 21 | private val androidPublisher = AndroidPublisher.Builder( 22 | GoogleNetHttpTransport.newTrustedTransport(), 23 | GsonFactory.getDefaultInstance(), 24 | // long timeout because uploading an aab can take a while 25 | TimeoutHttpCredentialsAdapter(credentials, timeout = 3.minutes), 26 | ).setApplicationName(application).build() 27 | 28 | public fun edit(block: GooglePlayEdit.() -> Boolean) { 29 | val edit = androidPublisher.edits().insert(appId, AppEdit()).execute() 30 | try { 31 | val editManager = GooglePlayEdit(androidPublisher.edits(), appId, edit.id) 32 | val result = editManager.block() 33 | if (result) { 34 | androidPublisher.edits().commit(appId, edit.id) 35 | .setChangesNotSentForReview(false) 36 | .execute() 37 | } else { 38 | androidPublisher.edits().delete(appId, edit.id).execute() 39 | } 40 | } catch (t: Throwable) { 41 | androidPublisher.edits().delete(appId, edit.id).execute() 42 | throw t 43 | } 44 | } 45 | 46 | public fun downloadApkTo(file: File, versionCode: Int) { 47 | val result = androidPublisher.generatedapks().list(appId, versionCode).execute() 48 | val universalApk = result.generatedApks.single().generatedUniversalApk 49 | val request = androidPublisher.generatedapks().download(appId, versionCode, universalApk.downloadId) 50 | file.outputStream().use { 51 | request.executeMediaAndDownloadTo(it) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/util/ProjectType.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.util 2 | 3 | import org.gradle.api.Project 4 | 5 | internal enum class ProjectType( 6 | val fullName: String, 7 | val prefix: String, 8 | val suffix: String, 9 | ) { 10 | APP(":app:*", "app", ""), 11 | CORE_API(":core:*:api", "core", "api"), 12 | CORE_IMPLEMENTATION(":core:*:implementation", "core", "implementation"), 13 | CORE_TESTING(":core:*:testing", "core", "testing"), 14 | CORE_DEBUG(":core:*:debug", "core", "debug"), 15 | DOMAIN_API(":domain:*:api", "domain", "api"), 16 | DOMAIN_IMPLEMENTATION(":domain:*:implementation", "domain", "implementation"), 17 | DOMAIN_DEBUG(":domain:*:debug", "domain", "debug"), 18 | DOMAIN_TESTING(":domain:*:testing", "domain", "testing"), 19 | FEATURE_NAV(":feature:*:nav", "domain", "nav"), 20 | FEATURE_IMPLEMENTATION(":feature:*:implementation", "domain", "implementation"), 21 | FEATURE_DEBUG(":feature:*:debug", "domain", "debug"), 22 | 23 | // TODO phase out 24 | LEGACY(":legacy-freeletics:*", "legacy", ""), 25 | } 26 | 27 | internal fun Project.projectType(): ProjectType { 28 | return path.toProjectType() 29 | } 30 | 31 | internal fun String.toProjectType(): ProjectType { 32 | return toProjectTypeOrNull() ?: throw IllegalStateException("Unknown project type $this") 33 | } 34 | 35 | internal fun String.toProjectTypeOrNull(): ProjectType? { 36 | return when { 37 | startsWith(":app:") -> ProjectType.APP 38 | startsWith(":core:") && endsWith(":api") -> ProjectType.CORE_API 39 | startsWith(":core:") && endsWith(":implementation") -> ProjectType.CORE_IMPLEMENTATION 40 | startsWith(":core:") && endsWith(":testing") -> ProjectType.CORE_TESTING 41 | startsWith(":core:") && endsWith(":debug") -> ProjectType.CORE_DEBUG 42 | startsWith(":domain") && endsWith(":api") -> ProjectType.DOMAIN_API 43 | startsWith(":domain") && endsWith(":implementation") -> ProjectType.DOMAIN_IMPLEMENTATION 44 | startsWith(":domain") && endsWith(":testing") -> ProjectType.DOMAIN_TESTING 45 | startsWith(":domain") && endsWith(":debug") -> ProjectType.DOMAIN_DEBUG 46 | startsWith(":feature") && endsWith(":implementation") -> ProjectType.FEATURE_IMPLEMENTATION 47 | startsWith(":feature") && endsWith(":nav") -> ProjectType.FEATURE_NAV 48 | startsWith(":feature") && endsWith(":debug") -> ProjectType.FEATURE_DEBUG 49 | startsWith(":legacy-freeletics:") -> ProjectType.LEGACY 50 | else -> null 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scripts-formatting/README.md: -------------------------------------------------------------------------------- 1 | # Formatting Scripts 2 | 3 | This can be added as a dependency to `.main.kts` script to share 4 | code for scripts between repositories. 5 | 6 | ## ktlint 7 | 8 | The published artifact contains a wrapper to run ktlint on the repository or on 9 | given files. To use it create `ktlint.main.kts` and add the following content: 10 | 11 | ```kts 12 | #!/usr/bin/env kotlin 13 | 14 | @file:DependsOn("com.freeletics.gradle:scripts-formatting:") 15 | 16 | import com.freeletics.gradle.scripts.KtLintCli 17 | import com.github.ajalt.clikt.core.main 18 | 19 | KtLintCli().main(args) 20 | ``` 21 | 22 | This can then be executed using `./kotlinw ktlint.main.kts --help` 23 | 24 | 25 | ## Kotlin wrapper 26 | 27 | It is recommended to create an executable file called `kotlinw` in the root of the 28 | repository with the following content. This will allow to run kts scripts with 29 | `./kotlinw foo.main.kts` and automatically takes care of downloading and updating 30 | the Kotlin compiler for the execution. For this to work the `libs.versions.toml` 31 | version catalog needs to include a version called `kotlin`. 32 | 33 | ```shell 34 | #!/usr/bin/env bash 35 | set -e 36 | 37 | ROOT_DIR="$(dirname $0)" 38 | KOTLIN_VERSION="$(grep -m 1 '^kotlin \?=' $ROOT_DIR/gradle/libs.versions.toml | cut -d'"' -f2)" 39 | INSTALLATION_DIR="${HOME}/.kotlinw" 40 | BINARY_DIR="${INSTALLATION_DIR}/kotlinc/bin" 41 | 42 | function internal_kotlinw() { 43 | if [ ! -f "${BINARY_DIR}/kotlin" ]; then 44 | internal_install_and_link_kotlin_compiler 45 | fi 46 | 47 | local current_version 48 | current_version="$("${BINARY_DIR}/kotlin" -version 2> /dev/null)" 49 | local expected_version_regex 50 | expected_version_regex="Kotlin version ${KOTLIN_VERSION}.*" 51 | if ! [[ "${current_version}" =~ ${expected_version_regex} ]]; then 52 | internal_install_and_link_kotlin_compiler 53 | fi 54 | 55 | # this works around an issue where the Kotlin compiler used by ktlint accesses code that JDK 12+ don't allow access to 56 | export JAVA_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED" 57 | 58 | "${BINARY_DIR}/kotlin" "$@" 59 | } 60 | 61 | function internal_install_and_link_kotlin_compiler() { 62 | echo "Downloading Kotlin ${KOTLIN_VERSION}" 63 | rm -rf "${INSTALLATION_DIR}" 64 | local temp_file 65 | temp_file=$(mktemp /tmp/kotlin.zip.XXXXXX) 66 | curl -sLo "${temp_file}" "https://github.com/JetBrains/kotlin/releases/download/v${KOTLIN_VERSION}/kotlin-compiler-${KOTLIN_VERSION}.zip" 67 | unzip -q "${temp_file}" -d "${INSTALLATION_DIR}" 68 | rm -f "${temp_file}" 69 | } 70 | 71 | internal_kotlinw "$@" 72 | ``` 73 | -------------------------------------------------------------------------------- /scripts-formatting/api/scripts-formatting.api: -------------------------------------------------------------------------------- 1 | public abstract interface class com/freeletics/gradle/scripts/Formatter { 2 | public abstract fun format (Ljava/nio/file/Path;)Lcom/freeletics/gradle/scripts/FormattingResult; 3 | } 4 | 5 | public abstract class com/freeletics/gradle/scripts/FormattingCli : com/github/ajalt/clikt/core/CliktCommand { 6 | public fun ()V 7 | protected abstract fun createFormatter ()Lcom/freeletics/gradle/scripts/Formatter; 8 | protected abstract fun createPathMatcher ()Ljava/nio/file/PathMatcher; 9 | public fun run ()V 10 | } 11 | 12 | public abstract interface class com/freeletics/gradle/scripts/FormattingResult { 13 | } 14 | 15 | public final class com/freeletics/gradle/scripts/FormattingResult$AlreadyFormatted : com/freeletics/gradle/scripts/FormattingResult { 16 | public static final field INSTANCE Lcom/freeletics/gradle/scripts/FormattingResult$AlreadyFormatted; 17 | public fun equals (Ljava/lang/Object;)Z 18 | public fun hashCode ()I 19 | public fun toString ()Ljava/lang/String; 20 | } 21 | 22 | public final class com/freeletics/gradle/scripts/FormattingResult$Error : com/freeletics/gradle/scripts/FormattingResult { 23 | public fun (Ljava/lang/String;)V 24 | public final fun component1 ()Ljava/lang/String; 25 | public final fun copy (Ljava/lang/String;)Lcom/freeletics/gradle/scripts/FormattingResult$Error; 26 | public static synthetic fun copy$default (Lcom/freeletics/gradle/scripts/FormattingResult$Error;Ljava/lang/String;ILjava/lang/Object;)Lcom/freeletics/gradle/scripts/FormattingResult$Error; 27 | public fun equals (Ljava/lang/Object;)Z 28 | public final fun getMessage ()Ljava/lang/String; 29 | public fun hashCode ()I 30 | public fun toString ()Ljava/lang/String; 31 | } 32 | 33 | public final class com/freeletics/gradle/scripts/FormattingResult$Formatted : com/freeletics/gradle/scripts/FormattingResult { 34 | public static final field INSTANCE Lcom/freeletics/gradle/scripts/FormattingResult$Formatted; 35 | public fun equals (Ljava/lang/Object;)Z 36 | public fun hashCode ()I 37 | public fun toString ()Ljava/lang/String; 38 | } 39 | 40 | public final class com/freeletics/gradle/scripts/GradleDependenciesCli : com/freeletics/gradle/scripts/FormattingCli { 41 | public fun ()V 42 | public fun help (Lcom/github/ajalt/clikt/core/Context;)Ljava/lang/String; 43 | } 44 | 45 | public final class com/freeletics/gradle/scripts/KtFmtCli : com/freeletics/gradle/scripts/FormattingCli { 46 | public fun ()V 47 | public fun help (Lcom/github/ajalt/clikt/core/Context;)Ljava/lang/String; 48 | } 49 | 50 | public final class com/freeletics/gradle/scripts/KtLintCli : com/freeletics/gradle/scripts/FormattingCli { 51 | public fun ()V 52 | public fun help (Lcom/github/ajalt/clikt/core/Context;)Ljava/lang/String; 53 | } 54 | 55 | -------------------------------------------------------------------------------- /docs/images/logo-freeletics.svg: -------------------------------------------------------------------------------- 1 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/monorepo/plugin/AppDesktopPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.monorepo.plugin 2 | 3 | import com.android.build.api.dsl.Lint 4 | import com.freeletics.gradle.monorepo.setup.applyPlatformConstraints 5 | import com.freeletics.gradle.monorepo.setup.disableMultiplatformApplicationTasks 6 | import com.freeletics.gradle.monorepo.tasks.CheckDependencyRulesTask.Companion.registerCheckDependencyRulesTasks 7 | import com.freeletics.gradle.monorepo.util.ProjectType 8 | import com.freeletics.gradle.plugin.FreeleticsMultiplatformPlugin 9 | import com.freeletics.gradle.util.defaultPackageName 10 | import com.freeletics.gradle.util.freeleticsMultiplatformExtension 11 | import org.gradle.api.Plugin 12 | import org.gradle.api.Project 13 | import org.jetbrains.compose.ComposeExtension 14 | import org.jetbrains.compose.desktop.DesktopExtension 15 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 16 | 17 | public abstract class AppDesktopPlugin : Plugin { 18 | override fun apply(target: Project) { 19 | target.plugins.apply(FreeleticsMultiplatformPlugin::class.java) 20 | target.plugins.apply("org.jetbrains.compose") 21 | 22 | target.freeleticsMultiplatformExtension.addJvmTarget() 23 | 24 | target.extensions.configure(ComposeExtension::class.java) { compose -> 25 | compose.extensions.configure(DesktopExtension::class.java) { desktop -> 26 | desktop.application { app -> 27 | app.mainClass = "${target.defaultPackageName()}.AppKt" 28 | app.nativeDistributions { 29 | it.targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 30 | it.packageName = target.defaultPackageName() 31 | // TODO compute version from task 32 | it.packageVersion = "1.0.0" 33 | } 34 | } 35 | } 36 | } 37 | 38 | target.extensions.configure(Lint::class.java) { 39 | it.baseline = target.file("lint-baseline.xml") 40 | } 41 | 42 | target.registerCheckDependencyRulesTasks( 43 | allowedProjectTypes = listOf(ProjectType.APP), 44 | allowedDependencyProjectTypes = listOfNotNull( 45 | ProjectType.CORE_API, 46 | ProjectType.CORE_IMPLEMENTATION, 47 | ProjectType.CORE_TESTING, 48 | ProjectType.DOMAIN_API, 49 | ProjectType.DOMAIN_IMPLEMENTATION, 50 | ProjectType.DOMAIN_TESTING, 51 | ProjectType.FEATURE_IMPLEMENTATION, 52 | ProjectType.FEATURE_NAV, 53 | ProjectType.APP, 54 | ), 55 | ) 56 | 57 | target.applyPlatformConstraints(multiplatform = true) 58 | target.disableMultiplatformApplicationTasks() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/plugin/CodegenTask.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.codegen 2 | 3 | import com.freeletics.codegen.CodeGenerator 4 | import java.util.ServiceLoader 5 | import javax.inject.Inject 6 | import org.gradle.api.DefaultTask 7 | import org.gradle.api.file.ConfigurableFileCollection 8 | import org.gradle.api.file.DirectoryProperty 9 | import org.gradle.api.provider.ListProperty 10 | import org.gradle.api.tasks.CacheableTask 11 | import org.gradle.api.tasks.Classpath 12 | import org.gradle.api.tasks.Input 13 | import org.gradle.api.tasks.InputDirectory 14 | import org.gradle.api.tasks.OutputDirectory 15 | import org.gradle.api.tasks.PathSensitive 16 | import org.gradle.api.tasks.PathSensitivity 17 | import org.gradle.api.tasks.TaskAction 18 | import org.gradle.workers.WorkAction 19 | import org.gradle.workers.WorkParameters 20 | import org.gradle.workers.WorkQueue 21 | import org.gradle.workers.WorkerExecutor 22 | 23 | @CacheableTask 24 | public abstract class CodegenTask : DefaultTask() { 25 | @get:Classpath 26 | internal abstract val generatorClasspath: ConfigurableFileCollection 27 | 28 | @get:Input 29 | public abstract val arguments: ListProperty 30 | 31 | @get:InputDirectory 32 | @get:PathSensitive(PathSensitivity.NAME_ONLY) 33 | public abstract val sourceDirectory: DirectoryProperty 34 | 35 | @get:OutputDirectory 36 | public abstract val outputDirectory: DirectoryProperty 37 | 38 | @get:Inject 39 | public abstract val workerExecutor: WorkerExecutor 40 | 41 | @TaskAction 42 | public fun run() { 43 | val output = outputDirectory.asFile.get() 44 | if (output.exists()) { 45 | output.walkBottomUp().forEach { 46 | if (it != output) { 47 | it.delete() 48 | } 49 | } 50 | } 51 | 52 | val workQueue: WorkQueue = workerExecutor.classLoaderIsolation { 53 | it.classpath.from(generatorClasspath) 54 | } 55 | 56 | workQueue.submit(GenerateCode::class.java) { 57 | it.arguments.set(arguments) 58 | it.sourceDirectory.set(sourceDirectory) 59 | it.outputDirectory.set(outputDirectory) 60 | } 61 | } 62 | 63 | public abstract class GenerateCode : WorkAction { 64 | override fun execute() { 65 | val generator = ServiceLoader.load(CodeGenerator::class.java).single() 66 | generator.generate( 67 | parameters.arguments.get(), 68 | parameters.sourceDirectory.asFile.get(), 69 | parameters.outputDirectory.asFile.get(), 70 | ) 71 | } 72 | 73 | public interface Parameters : WorkParameters { 74 | public val arguments: ListProperty 75 | public val sourceDirectory: DirectoryProperty 76 | public val outputDirectory: DirectoryProperty 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /kotlinw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | ROOT_DIR="$(dirname "$0")" 5 | 6 | KOTLIN_VERSION="$(grep -m 1 '^kotlin \?=' "$ROOT_DIR/gradle/libs.versions.toml" | cut -d'"' -f2)" 7 | JDK_VERSION="$(grep -s 'toolchainVersion' "$ROOT_DIR/gradle/gradle-daemon-jvm.properties" | cut -d'=' -f2)" 8 | 9 | if [ "$JDK_VERSION" = "24" ]; then 10 | JDK_TARGET_VERSION="23" 11 | else 12 | JDK_TARGET_VERSION="$JDK_VERSION" 13 | fi 14 | 15 | # OS specific support (must be 'true' or 'false'). 16 | case "$(uname -s)" in 17 | Linux*) MACHINE=Linux;; 18 | Darwin*) MACHINE=Mac;; 19 | *) MACHINE="UNSUPPORTED" 20 | esac 21 | 22 | if [ "$MACHINE" = "Linux" ]; then 23 | export JAVA_HOME="/usr/lib/jvm/zulu-$JDK_VERSION-amd64/" 24 | if [ ! -d "$JAVA_HOME" ]; then 25 | export JAVA_HOME="/usr/lib/jvm/zulu$JDK_VERSION-ca-amd64/" 26 | if [ ! -d "$JAVA_HOME" ]; then 27 | echo "Installing JDK ${JDK_VERSION} (you may be prompted for your password)..." 28 | curl -s https://repos.azul.com/azul-repo.key | sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/azul.gpg 29 | echo "deb [signed-by=/usr/share/keyrings/azul.gpg] https://repos.azul.com/zulu/deb stable main" | sudo tee /etc/apt/sources.list.d/zulu.list 30 | sudo apt update 31 | sudo apt install -y "zulu$JDK_VERSION-jdk" 32 | fi 33 | fi 34 | elif [ "$MACHINE" = "Mac" ]; then 35 | export JAVA_HOME="/Library/Java/JavaVirtualMachines/zulu-$JDK_VERSION.jdk/Contents/Home" 36 | if [ ! -d "$JAVA_HOME" ]; then 37 | echo "Installing JDK ${JDK_VERSION} (you may be prompted for your password)..." 38 | brew tap mdogan/zulu 39 | brew install --cask "zulu-jdk${JDK_VERSION}" 40 | fi 41 | else 42 | echo "Only macOS and Linux are supported." 43 | exit 1 44 | fi 45 | 46 | INSTALLATION_DIR="${HOME}/.kotlinw/${KOTLIN_VERSION}" 47 | BINARY_DIR="${INSTALLATION_DIR}/kotlinc/bin" 48 | 49 | if [ ! -f "${BINARY_DIR}/kotlin" ]; then 50 | echo "Downloading Kotlin ${KOTLIN_VERSION}" 51 | mkdir -p "${INSTALLATION_DIR}" 52 | temp_file=$(mktemp /tmp/kotlin.zip.XXXXXX) 53 | curl -sLo "${temp_file}" "https://github.com/JetBrains/kotlin/releases/download/v${KOTLIN_VERSION}/kotlin-compiler-${KOTLIN_VERSION}.zip" 54 | unzip -q "${temp_file}" -d "${INSTALLATION_DIR}" 55 | rm -f "${temp_file}" 56 | fi 57 | 58 | # this works around an issue where the Kotlin compiler used by ktlint accesses code that JDK 12+ don't allow access to 59 | export JAVA_OPTS="--enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow" 60 | 61 | SCRIPT_FILE="$1" 62 | # will remove the first element of the $@ arguments array since we read it already above 63 | shift 64 | # uses kotlinc instead of kotlin because the latter doesn't allow specifying a jvm target and defaults to Java 8 65 | # the -- between SCRIPT_FILE and the other arguments is there so that the arguments are treated as arguments to the 66 | # script and not to kotlinc 67 | "${BINARY_DIR}/kotlinc" "-jvm-target" "$JDK_TARGET_VERSION" "-script" "$SCRIPT_FILE" "--" "${@}" 68 | -------------------------------------------------------------------------------- /scripts-formatting/src/jvmMain/kotlin/com/freeletics/gradle/scripts/FormattingCli.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.github.ajalt.clikt.core.CliktCommand 4 | import com.github.ajalt.clikt.parameters.options.default 5 | import com.github.ajalt.clikt.parameters.options.flag 6 | import com.github.ajalt.clikt.parameters.options.option 7 | import com.github.ajalt.clikt.parameters.options.split 8 | import com.github.ajalt.clikt.parameters.types.path 9 | import java.nio.file.Path 10 | import java.nio.file.PathMatcher 11 | import java.nio.file.Paths 12 | import kotlin.io.path.absolute 13 | import kotlin.system.exitProcess 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.ExperimentalCoroutinesApi 16 | import kotlinx.coroutines.flow.flatMapMerge 17 | import kotlinx.coroutines.flow.flowOf 18 | import kotlinx.coroutines.flow.onEach 19 | import kotlinx.coroutines.runBlocking 20 | 21 | public abstract class FormattingCli : CliktCommand() { 22 | private val rootDirectory: Path by option("--root", help = ROOT_HELP) 23 | .path() 24 | .default(Paths.get(".").absolute()) 25 | 26 | private val files: List? by option("--files", help = FILES_HELP) 27 | .path() 28 | .split(Regex("[\n ,]")) 29 | 30 | private val verify: Boolean by option("--fail-on-changes", help = VERIFY_HELP) 31 | .flag() 32 | 33 | protected abstract fun createPathMatcher(): PathMatcher 34 | 35 | protected abstract fun createFormatter(): Formatter 36 | 37 | @OptIn(ExperimentalCoroutinesApi::class) 38 | override fun run(): Unit = runBlocking(Dispatchers.IO) { 39 | val formatter = createFormatter() 40 | 41 | var count = 0 42 | var hadChanges = false 43 | var hadErrors = false 44 | 45 | filesToFormat(files, rootDirectory, createPathMatcher()) 46 | .onEach { count++ } 47 | .flatMapMerge { flowOf(formatter.format(it)) } 48 | .collect { result -> 49 | when (result) { 50 | FormattingResult.AlreadyFormatted -> {} 51 | FormattingResult.Formatted -> { 52 | hadChanges = true 53 | } 54 | is FormattingResult.Error -> { 55 | hadErrors = true 56 | println(result.message) 57 | } 58 | } 59 | } 60 | 61 | println("Formatted $count files") 62 | if (hadErrors || (verify && hadChanges)) { 63 | exitProcess(1) 64 | } 65 | } 66 | 67 | private companion object { 68 | private const val ROOT_HELP = "The root directory of the project, used as starting point to search files. " + 69 | "Uses current directory if not specified." 70 | private const val FILES_HELP = "The files to format, if not specified the root option will be used to" + 71 | "automatically find files." 72 | private const val VERIFY_HELP = "When this flag is passed, the script will fail if any files changed during " + 73 | "formatting." 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/plugin/CodegenPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.codegen 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | import org.gradle.api.attributes.java.TargetJvmEnvironment 6 | import org.gradle.api.attributes.java.TargetJvmEnvironment.STANDARD_JVM 7 | import org.gradle.api.attributes.java.TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE 8 | import org.gradle.api.tasks.TaskProvider 9 | import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension 10 | 11 | public abstract class CodegenPlugin : Plugin { 12 | override fun apply(project: Project) { 13 | // The DSL that is used from the build script to configure the 14 | // options of this plugin. 15 | val extension = project.extensions.create("codegen", CodegenExtension::class.java) 16 | 17 | // The code generator will be added to this as a dependency 18 | val configuration = project.configurations.register("codegen") { configuration -> 19 | configuration.isTransitive = true 20 | 21 | configuration.attributes { it -> 22 | it.attribute( 23 | TARGET_JVM_ENVIRONMENT_ATTRIBUTE, 24 | project.objects.named(TargetJvmEnvironment::class.java, STANDARD_JVM), 25 | ) 26 | } 27 | } 28 | 29 | val generateTask = project.tasks.register("generateCode", CodegenTask::class.java) { 30 | it.generatorClasspath.setFrom(configuration) 31 | it.arguments.set(extension.arguments) 32 | it.sourceDirectory.set(extension.sourceDirectory) 33 | it.outputDirectory.set(project.layout.buildDirectory.dir("generated/sources")) 34 | } 35 | 36 | // This does 2 things: 37 | // - the output directory of the task is registered as source directory so that the 38 | // generated code will be compiled and included in the built artifacts 39 | // - it automatically sets up task dependencies so that generate is run before the 40 | // compile task of the specified source set runs 41 | // 42 | // Depending on the module type based on which Kotlin plugin is applied a different source set is used. 43 | registerTaskWithSourceSet(project, generateTask, "org.jetbrains.kotlin.multiplatform", "commonMain") 44 | registerTaskWithSourceSet(project, generateTask, "org.jetbrains.kotlin.multiplatform", "jvmMain") 45 | registerTaskWithSourceSet(project, generateTask, "org.jetbrains.kotlin.multiplatform", "iosMain") 46 | registerTaskWithSourceSet(project, generateTask, "org.jetbrains.kotlin.jvm", "main") 47 | } 48 | 49 | private fun registerTaskWithSourceSet( 50 | project: Project, 51 | task: TaskProvider, 52 | plugin: String, 53 | sourceSet: String, 54 | ) { 55 | project.plugins.withId(plugin) { 56 | project.extensions.configure(KotlinProjectExtension::class.java) { extension -> 57 | extension.sourceSets.configureEach { kotlinSourceSet -> 58 | if (kotlinSourceSet.name == sourceSet) { 59 | kotlinSourceSet.kotlin.srcDir(task.flatMap { it.outputDirectory.dir("kotlin/$sourceSet") }) 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: [ 4 | "config:base", 5 | ":disableRateLimiting", 6 | ":semanticCommitsDisabled" 7 | ], 8 | automerge: true, 9 | platformAutomerge: true, 10 | platformCommit: true, 11 | assignAutomerge: true, 12 | reviewers: ["team:android-oss"], 13 | rebaseWhen: "conflicted", 14 | prBodyTemplate: "{{{header}}}{{{table}}}{{{notes}}}{{{changelogs}}}{{{controls}}}{{{footer}}}", 15 | prBodyColumns: ["Package", "Change", "Age"], 16 | prFooter: "🔕 Ignore: Close this PR and you won't be reminded about these updates again.", 17 | ignoreDeps: [ 18 | ], 19 | packageRules: [ 20 | { 21 | matchDatasources: ["maven"], 22 | registryUrls: [ 23 | "https://repo.maven.apache.org/maven2", 24 | "https://dl.google.com/android/maven2", 25 | "https://plugins.gradle.org/m2", 26 | ] 27 | }, 28 | { 29 | matchPackageNames: [ 30 | "gradle", 31 | ], 32 | prBodyNotes: "[Changelog](https://docs.gradle.org/{{{newVersion}}}/release-notes.html)" 33 | }, 34 | { 35 | matchPackageNames: [ 36 | "com.gradle:develocity-gradle-plugin" 37 | ], 38 | registryUrls: ["https://plugins.gradle.org/m2/"], 39 | prBodyNotes: "[Changelog](https://plugins.gradle.org/plugin/com.gradle.enterprise)" 40 | }, 41 | { 42 | matchPackageNames: [ 43 | "org.gradle.toolchains:foojay-resolver" 44 | ], 45 | registryUrls: ["https://plugins.gradle.org/m2/"], 46 | prBodyNotes: "[Changelog](https://plugins.gradle.org/plugin/org.gradle.toolchains.foojay-resolver)" 47 | }, 48 | { 49 | matchPackageNames: [ 50 | "com.google.firebase:firebase-crashlytics-gradle" 51 | ], 52 | registryUrls: ["https://dl.google.com/android/maven2"], 53 | prBodyNotes: "[Changelog](https://firebase.google.com/support/release-notes/android)" 54 | }, 55 | { 56 | matchPackagePatterns: [ 57 | "com.freeletics.gradle", 58 | ], 59 | groupName: "Freeletics Gradle Plugin" 60 | }, 61 | { 62 | matchPackagePatterns: [ 63 | "org.jetbrains.kotlin", 64 | "com.javiersc.kotlin", 65 | "dev.drewhamilton.poko", 66 | "co.touchlab.skie", 67 | ], 68 | excludePackagePatterns: [ 69 | "org.jetbrains.kotlinx", 70 | ], 71 | groupName: "Kotlin and compiler plugins" 72 | }, 73 | { 74 | matchPackagePatterns: [ 75 | "androidx.compose", 76 | "org.jetbrains.compose", 77 | ], 78 | groupName: "Compose" 79 | }, 80 | { 81 | matchPackagePatterns: [ 82 | "^com.google.http-client", 83 | "^com.google.auth", 84 | "^com.google.apis", 85 | "^com.google.api-client", 86 | "^com.google.http-client", 87 | ], 88 | groupName: "Google APIs" 89 | }, 90 | // the AWS SDK and Google APIs are updated multiple times a week which causes a lot of noise 91 | { 92 | matchPackagePatterns: [ 93 | "^aws.sdk.kotlin", 94 | "^com.google.http-client", 95 | "^com.google.auth", 96 | "^com.google.apis", 97 | "^com.google.api-client", 98 | "^com.google.http-client", 99 | ], 100 | schedule: ["after 9pm on sunday"] 101 | }, 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /plugin-settings/src/main/kotlin/com/freeletics/gradle/plugin/SettingsExtension.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.plugin 2 | 3 | import org.gradle.api.initialization.Settings 4 | 5 | public abstract class SettingsExtension(private val settings: Settings) { 6 | /** 7 | * Automatically find and include Gradle projects in this build. It will only search in the given folders and find 8 | * any project where the build file name matches the path, e.g. `:example` should have `example.gradle` as build 9 | * file and `:foo:bar` should have `foo-bar.gradle. 10 | * 11 | * This does not support nested projects. E.g. `foo:api` and `foo:impl` are generally fine, but if `:foo` also 12 | * is a project (has a build file) then these 2 are not considered. 13 | */ 14 | @JvmOverloads 15 | public fun discoverProjectsIn(kts: Boolean = true, vararg directories: String) { 16 | val extensions = if (kts) listOf("gradle.kts") else listOf("gradle") 17 | settings.discoverProjects(extensions, directories.toList()) 18 | } 19 | 20 | /** 21 | * @param androidXBuildId buildId for androidx snapshot artifacts. Can be taken from here: 22 | * https://androidx.dev/snapshots/builds 23 | */ 24 | @JvmOverloads 25 | public fun snapshots(androidXBuildId: String? = null) { 26 | @Suppress("UnstableApiUsage") 27 | settings.dependencyResolutionManagement.repositories.apply { 28 | addSonatypeSnapshotRepository() 29 | addKotlinSnapshotRepository() 30 | addAndroidXSnapshotRepositories(androidXBuildId) 31 | mavenLocal() 32 | } 33 | } 34 | 35 | /** 36 | * Include a local clone of Khonshu in this build. 37 | */ 38 | @JvmOverloads 39 | public fun includeKhonshu(path: String = "../khonshu") { 40 | settings.includeBuild(path) { build -> 41 | build.dependencySubstitution { 42 | it.substitute(it.module("com.freeletics.khonshu:state-machine")) 43 | .using(it.project(":state-machine")) 44 | it.substitute(it.module("com.freeletics.khonshu:state-machine-testing")) 45 | .using(it.project(":state-machine-testing")) 46 | it.substitute(it.module("com.freeletics.khonshu:text-resource")) 47 | .using(it.project(":text-resource")) 48 | it.substitute(it.module("com.freeletics.khonshu:navigation")) 49 | .using(it.project(":navigation")) 50 | it.substitute(it.module("com.freeletics.khonshu:navigation-testing")) 51 | .using(it.project(":navigation-testing")) 52 | it.substitute(it.module("com.freeletics.khonshu:codegen-runtime")) 53 | .using(it.project(":codegen")) 54 | it.substitute(it.module("com.freeletics.khonshu:codegen-compiler")) 55 | .using(it.project(":codegen-compiler")) 56 | } 57 | } 58 | } 59 | 60 | @JvmOverloads 61 | public fun includeFlowRedux(path: String = "../flowredux") { 62 | settings.includeBuild(path) { build -> 63 | build.dependencySubstitution { 64 | it.substitute(it.module("com.freeletics.flowredux:flowredux")) 65 | .using(it.project(":flowredux")) 66 | it.substitute(it.module("com.freeletics.flowredux:compose")) 67 | .using(it.project(":compose")) 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/util/PackageName.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.util 2 | 3 | import com.freeletics.gradle.monorepo.util.ProjectType 4 | import com.freeletics.gradle.monorepo.util.appType 5 | import com.freeletics.gradle.monorepo.util.toProjectTypeOrNull 6 | import org.gradle.api.Project 7 | 8 | internal fun Project.defaultPackageName(): String { 9 | val appType = project.appType() 10 | val appPrefix = stringProperty("fgp.defaultPackageName.${appType?.name}").orNull 11 | val prefix = appPrefix ?: stringProperty("fgp.defaultPackageName").get() 12 | if (booleanProperty("fgp.useLegacyPackageNaming", false).get()) { 13 | return legacyPackageName(path, prefix) 14 | } 15 | return defaultPackageName(path, prefix) 16 | } 17 | 18 | internal fun defaultPackageName(path: String, prefix: String): String { 19 | val projectType = path.toProjectTypeOrNull() 20 | val pathElements = path.split(":").drop(1) 21 | val projectPackageElements = when (projectType) { 22 | // skip first part of the app name because it's generally already part of the prefix 23 | ProjectType.APP -> pathElements[1].transformPathPart().drop(1) 24 | ProjectType.CORE_API -> pathElements[1].transformPathPart() 25 | ProjectType.CORE_IMPLEMENTATION -> pathElements[1].transformPathPart() + projectType.suffix 26 | ProjectType.CORE_TESTING -> pathElements[1].transformPathPart() + projectType.suffix 27 | ProjectType.CORE_DEBUG -> pathElements[1].transformPathPart() + projectType.suffix 28 | ProjectType.DOMAIN_API -> pathElements[1].transformPathPart() 29 | ProjectType.DOMAIN_IMPLEMENTATION -> pathElements[1].transformPathPart() + projectType.suffix 30 | ProjectType.DOMAIN_TESTING -> pathElements[1].transformPathPart() + projectType.suffix 31 | ProjectType.DOMAIN_DEBUG -> pathElements[1].transformPathPart() + projectType.suffix 32 | ProjectType.FEATURE_NAV -> pathElements[1].transformPathPart() + projectType.suffix 33 | ProjectType.FEATURE_IMPLEMENTATION -> pathElements[1].transformPathPart() 34 | ProjectType.FEATURE_DEBUG -> pathElements[1].transformPathPart() 35 | ProjectType.LEGACY -> pathElements[1].transformPathPart() 36 | null -> pathElements.flatMap { it.transformPathPart() } 37 | } 38 | val packageElements = prefix.split(".") + projectPackageElements 39 | return packageElements.joinToString(separator = ".") { it.lowercase() } 40 | } 41 | 42 | private fun String.transformPathPart(): List { 43 | return split("-") 44 | } 45 | 46 | internal fun legacyPackageName(path: String, prefix: String): String { 47 | val prefixElements = prefix.split(".") 48 | val pathElements = path.drop(1) 49 | .split(":") 50 | .mapIndexed { index, pathElement -> 51 | val parts = pathElement.split("-") 52 | if (index == 0) { 53 | // top level folders like core, domain, feature etc. handle dashes separately by 54 | // having them become separate package elements, also ignore the -freeletics suffix 55 | // to avoid package names like com.freeletics.domain.freeletics 56 | parts.filterNot { prefixElements.contains(it) }.joinToString(separator = ".") 57 | } else { 58 | // for second and lower level folders dashes are ignored and the elements are 59 | // merged into one word 60 | parts.joinToString(separator = "") 61 | } 62 | } 63 | return (prefixElements + pathElements).joinToString(separator = ".") 64 | } 65 | -------------------------------------------------------------------------------- /scripts-formatting/src/jvmMain/kotlin/com/freeletics/gradle/scripts/KtLintFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.scripts 2 | 3 | import com.pinterest.ktlint.rule.engine.api.Code 4 | import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults 5 | import com.pinterest.ktlint.rule.engine.api.KtLintParseException 6 | import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine 7 | import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision 8 | import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider 9 | import java.nio.file.Path 10 | import kotlin.io.path.writeText 11 | import org.ec4j.core.model.EditorConfig 12 | import org.ec4j.core.model.Glob 13 | import org.ec4j.core.model.Property 14 | import org.ec4j.core.model.Section 15 | 16 | internal class KtLintFormatter : Formatter { 17 | private val editorConfigDefaults = EditorConfigDefaults( 18 | EditorConfig 19 | .builder() 20 | .section( 21 | Section 22 | .builder() 23 | .glob(Glob("*.{kt,kts}")) 24 | .properties( 25 | // allow test methods with back ticked names to be longer than the line limit 26 | Property 27 | .builder() 28 | .name("ktlint_ignore_back_ticked_identifier") 29 | .value("true"), 30 | // allow composable functions to be capitalized 31 | Property 32 | .builder() 33 | .name("ktlint_function_naming_ignore_when_annotated_with") 34 | .value("Composable"), 35 | // remove unused imports 36 | Property 37 | .builder() 38 | .name("ktlint_standard_no-unused-imports") 39 | .value("enabled"), 40 | // see https://github.com/pinterest/ktlint/issues/2138 41 | Property 42 | .builder() 43 | .name("ktlint_standard_annotation") 44 | .value("disabled"), 45 | ), 46 | ) 47 | .build(), 48 | ) 49 | 50 | private val engine = KtLintRuleEngine( 51 | ruleProviders = StandardRuleSetProvider().getRuleProviders(), 52 | editorConfigDefaults = editorConfigDefaults, 53 | ) 54 | 55 | override fun format(path: Path): FormattingResult { 56 | val errors = mutableListOf() 57 | 58 | // need to use fromFile instead of fromPath because the latter drops the final new line 59 | val code = Code.fromFile(path.toFile()) 60 | val formatted = try { 61 | engine.format(code) { error -> 62 | if (!error.canBeAutoCorrected) { 63 | errors += "$path:${error.line} ${error.ruleId} ${error.detail}" 64 | } 65 | AutocorrectDecision.ALLOW_AUTOCORRECT 66 | } 67 | } catch (e: KtLintParseException) { 68 | return FormattingResult.Error("$path: ${e.message ?: "Failed to parse file"}") 69 | } 70 | 71 | if (errors.isNotEmpty()) { 72 | return FormattingResult.Error(errors.joinToString(separator = "\n")) 73 | } else if (formatted != code.content) { 74 | path.writeText(formatted) 75 | return FormattingResult.Formatted 76 | } else { 77 | return FormattingResult.AlreadyFormatted 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /scripts-google/api/scripts-google.api: -------------------------------------------------------------------------------- 1 | public final class com/freeletics/gradle/scripts/GoogleApiOptions : com/github/ajalt/clikt/parameters/groups/OptionGroup { 2 | public fun ()V 3 | public final fun getJsonKeyData ()Ljava/lang/String; 4 | } 5 | 6 | public final class com/freeletics/gradle/scripts/GooglePlayEdit { 7 | public static final field Companion Lcom/freeletics/gradle/scripts/GooglePlayEdit$Companion; 8 | public static final field STATUS_COMPLETED Ljava/lang/String; 9 | public static final field STATUS_IN_PROGRESS Ljava/lang/String; 10 | public static final field TRACK_BETA Ljava/lang/String; 11 | public static final field TRACK_INTERNAL Ljava/lang/String; 12 | public static final field TRACK_PRODUCTION Ljava/lang/String; 13 | public fun (Lcom/google/api/services/androidpublisher/AndroidPublisher$Edits;Ljava/lang/String;Ljava/lang/String;)V 14 | public final fun publishToTrack (Ljava/lang/String;DLcom/freeletics/gradle/scripts/GooglePlayReleaseVersion;Ljava/util/Map;)V 15 | public final fun updateRolloutInTrack (Ljava/lang/String;DLcom/freeletics/gradle/scripts/GooglePlayReleaseVersion;)V 16 | public final fun upload (Ljava/io/File;Lcom/freeletics/gradle/scripts/GooglePlayReleaseVersion;)V 17 | public final fun versionsInTrack (Ljava/lang/String;)Ljava/util/List; 18 | } 19 | 20 | public final class com/freeletics/gradle/scripts/GooglePlayEdit$Companion { 21 | } 22 | 23 | public final class com/freeletics/gradle/scripts/GooglePlayPublisher { 24 | public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V 25 | public final fun downloadApkTo (Ljava/io/File;I)V 26 | public final fun edit (Lkotlin/jvm/functions/Function1;)V 27 | } 28 | 29 | public abstract class com/freeletics/gradle/scripts/GooglePlayReleaseVersion { 30 | public abstract fun getCode ()J 31 | public final fun getCodePrefix ()J 32 | public abstract fun getName ()Ljava/lang/String; 33 | public final fun getNamePrefix ()Ljava/lang/String; 34 | } 35 | 36 | public final class com/freeletics/gradle/scripts/GooglePlayReleaseVersion$Simple : com/freeletics/gradle/scripts/GooglePlayReleaseVersion { 37 | public fun (Ljava/lang/String;J)V 38 | public final fun component1 ()Ljava/lang/String; 39 | public final fun component2 ()J 40 | public final fun copy (Ljava/lang/String;J)Lcom/freeletics/gradle/scripts/GooglePlayReleaseVersion$Simple; 41 | public static synthetic fun copy$default (Lcom/freeletics/gradle/scripts/GooglePlayReleaseVersion$Simple;Ljava/lang/String;JILjava/lang/Object;)Lcom/freeletics/gradle/scripts/GooglePlayReleaseVersion$Simple; 42 | public fun equals (Ljava/lang/Object;)Z 43 | public fun getCode ()J 44 | public fun getName ()Ljava/lang/String; 45 | public fun hashCode ()I 46 | public fun toString ()Ljava/lang/String; 47 | } 48 | 49 | public final class com/freeletics/gradle/scripts/GooglePlayReleaseVersion$WithRollout : com/freeletics/gradle/scripts/GooglePlayReleaseVersion { 50 | public fun (Ljava/lang/String;JD)V 51 | public final fun asSimple ()Lcom/freeletics/gradle/scripts/GooglePlayReleaseVersion; 52 | public final fun component1 ()Ljava/lang/String; 53 | public final fun component2 ()J 54 | public final fun component3 ()D 55 | public final fun copy (Ljava/lang/String;JD)Lcom/freeletics/gradle/scripts/GooglePlayReleaseVersion$WithRollout; 56 | public static synthetic fun copy$default (Lcom/freeletics/gradle/scripts/GooglePlayReleaseVersion$WithRollout;Ljava/lang/String;JDILjava/lang/Object;)Lcom/freeletics/gradle/scripts/GooglePlayReleaseVersion$WithRollout; 57 | public fun equals (Ljava/lang/Object;)Z 58 | public fun getCode ()J 59 | public fun getName ()Ljava/lang/String; 60 | public final fun getRolloutPercentage ()D 61 | public fun hashCode ()I 62 | public fun toString ()Ljava/lang/String; 63 | } 64 | 65 | public final class com/freeletics/gradle/scripts/GoogleSheetsReader { 66 | public fun (Ljava/lang/String;Ljava/lang/String;)V 67 | public final fun read (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List; 68 | } 69 | 70 | -------------------------------------------------------------------------------- /docs/plugins-monorepo/settings.md: -------------------------------------------------------------------------------- 1 | # Settings plugin 2 | 3 | Add the following to `settings.gradle` or `settings.gradle.kts` to apply the plugin: 4 | 5 | ```groovy 6 | plugins { 7 | id("com.freeletics.gradle.settings").version("") 8 | } 9 | ``` 10 | 11 | ## Features 12 | 13 | ### Project discovery 14 | 15 | The plugin will automatically discover all projects by finding their build files and include them. This means it is 16 | not necessary anymore to manually add `include("...")` for each project to `settings.gradle.kts`. The requirement for this 17 | to work is that the build file is named after the path. For example the project `:foo:bar` should have a build file 18 | called `foo-bar.gradle.kts` (or `foo-bar.gradle`). ` 19 | 20 | Instead of using project root directory as a base for automatic discoverability it is possible to only include 21 | certain subdirectories by adding `fgp.discoverProjects.automatically=false` to project's `gradle.properties` and adding 22 | the following: 23 | 24 | ```kotlin 25 | freeletics { 26 | discoverProjectsIn("app", "features") 27 | } 28 | ``` 29 | 30 | ### `dependencyResolutionManagement` 31 | 32 | The project will be configured to fail when repositories are defined on a project instead of through 33 | `dependencyResolutionManagement`. 34 | 35 | The following repositories are automatically added to `dependencyResolutionManagement`: 36 | - Maven Central 37 | - the Google Maven Repository with `exclusiveContent` rules 38 | - the Gradle plugin portal limited to `com.gradle.*` and `org.gradle.*` 39 | - the Kotlin JS and WASM repositories 40 | 41 | If `fgp.internalArtifacts.url` is set a repository for that URL is created. The content of that repository is limited 42 | to the group regex specified by `fgp.internalArtifacts.regex`. The username and password for this repository are expected 43 | to be set through `internalArtifactsUsername` and `internalArtifactsPassword`. 44 | 45 | ### Snapshots 46 | 47 | By adding the following snipped to `settings.gradle` it is possible to add various snapshot repositories to the project: 48 | 49 | ```groovy 50 | freeletics { 51 | snapshots() 52 | // or to include one of the AndroidX snapshot repositories from https://androidx.dev/snapshots/builds use 53 | snapshots("") 54 | } 55 | ``` 56 | 57 | This adds: 58 | - both Maven Central snapshot repository 59 | - the Kotlin bootstrap repository which contains dev builds 60 | - the AndroidX snapshot repository if a build id was specified 61 | - maven local (`~/.m2`) 62 | 63 | ### Build cache 64 | 65 | If the Gradle build cache is generally enabled the plugin allows configuring it through Gradle properties. By default 66 | the local build cache is enabled but can be disabled by setting `fgp.buildcache.local=false`. 67 | 68 | The remote build cache can be enabled and configured like this: 69 | ```properties 70 | fgp.buildcache.remote=true 71 | fgp.buildcache.url=https://... 72 | fgp.buildcache.push=true 73 | fgp.buildcache.username=... 74 | fgp.buildcache.password=... 75 | ``` 76 | 77 | ### Included builds 78 | 79 | With the following snippet it is possible to configure an included build for [Khonshu][4] and [FlowRedux][5] Freeletics 80 | open source projects. 81 | 82 | ```groovy 83 | freeletics { 84 | includeKhonshu("path/to/cloned/khonshu/repository") // path can be omitted if it is ../khonshu 85 | includeFlowRedux("path/to/cloned/flowredux/repository") // path can be omitted if it is ../flowredux 86 | } 87 | ``` 88 | 89 | ### Other 90 | 91 | - [Type-safe project accessors][2] are enabled by default. 92 | - [Strict configuration cache][3] is enabled by default. 93 | - Configures the jvm toolchain management for auto provisioning using the [Foojay Toolchains Plugin][1] 94 | - Configures the Gradle enterprise plugin 95 | 96 | 97 | [1]: https://github.com/gradle/foojay-toolchains 98 | [2]: https://docs.gradle.org/current/userguide/declaring_dependencies_basics.html#sec:type-safe-project-accessors 99 | [3]: https://docs.gradle.org/current/userguide/configuration_cache_enabling.html#config_cache:stable 100 | [4]: https://github.com/freeletics/khonshu 101 | [5]: https://github.com/freeletics/flowredux 102 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/util/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.util 2 | 3 | import com.android.build.api.dsl.ApplicationExtension 4 | import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryExtension 5 | import com.android.build.api.variant.ApplicationAndroidComponentsExtension 6 | import com.freeletics.gradle.plugin.FreeleticsBaseExtension 7 | import com.freeletics.gradle.plugin.FreeleticsMultiplatformAndroidExtension 8 | import com.freeletics.gradle.plugin.FreeleticsMultiplatformExtension 9 | import org.gradle.api.Project 10 | import org.gradle.api.plugins.JavaPluginExtension 11 | import org.jetbrains.kotlin.gradle.dsl.HasConfigurableKotlinCompilerOptions 12 | import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension 13 | import org.jetbrains.kotlin.gradle.dsl.KotlinCommonCompilerOptions 14 | import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions 15 | import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension 16 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 17 | import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension 18 | import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType 19 | 20 | internal val Project.freeleticsExtension: FreeleticsBaseExtension 21 | get() = extensions.getByType(FreeleticsBaseExtension::class.java) 22 | 23 | internal val Project.freeleticsMultiplatformExtension: FreeleticsMultiplatformExtension 24 | get() = freeleticsExtension.extensions.getByType(FreeleticsMultiplatformExtension::class.java) 25 | 26 | internal val Project.freeleticsAndroidMultiplatformExtension: FreeleticsMultiplatformAndroidExtension 27 | get() = freeleticsExtension.extensions.getByType(FreeleticsMultiplatformAndroidExtension::class.java) 28 | 29 | internal fun Project.java(action: JavaPluginExtension.() -> Unit) { 30 | extensions.configure(JavaPluginExtension::class.java) { 31 | it.action() 32 | } 33 | } 34 | 35 | internal fun Project.kotlin(action: KotlinProjectExtension.() -> Unit) { 36 | (project.extensions.getByName("kotlin") as KotlinProjectExtension).action() 37 | } 38 | 39 | internal fun KotlinProjectExtension.compilerOptionsCommon(configure: KotlinCommonCompilerOptions.() -> Unit) { 40 | when (this) { 41 | is KotlinJvmProjectExtension -> compilerOptions(configure) 42 | is KotlinAndroidProjectExtension -> compilerOptions(configure) 43 | is KotlinMultiplatformExtension -> compilerOptions(configure) 44 | else -> throw IllegalStateException("Unsupported kotlin extension ${this::class}") 45 | } 46 | } 47 | 48 | internal fun KotlinProjectExtension.compilerOptionsJvm(configure: KotlinJvmCompilerOptions.(Boolean) -> Unit) { 49 | when (this) { 50 | is KotlinJvmProjectExtension -> compilerOptions { configure(false) } 51 | is KotlinAndroidProjectExtension -> compilerOptions { configure(true) } 52 | is KotlinMultiplatformExtension -> targets.configureEach { 53 | (it as? HasConfigurableKotlinCompilerOptions<*>)?.compilerOptions { 54 | if (this is KotlinJvmCompilerOptions) { 55 | configure(it.platformType == KotlinPlatformType.androidJvm) 56 | } 57 | } 58 | } 59 | else -> throw IllegalStateException("Unsupported kotlin extension ${this::class}") 60 | } 61 | } 62 | 63 | internal fun Project.kotlinMultiplatform(action: KotlinMultiplatformExtension.() -> Unit) { 64 | extensions.configure(KotlinMultiplatformExtension::class.java) { 65 | it.action() 66 | } 67 | } 68 | 69 | internal fun Project.androidMultiplatform(action: KotlinMultiplatformAndroidLibraryExtension.() -> Unit) { 70 | kotlinMultiplatform { 71 | extensions.configure(KotlinMultiplatformAndroidLibraryExtension::class.java) { 72 | it.action() 73 | } 74 | } 75 | } 76 | 77 | internal fun Project.androidApp(action: ApplicationExtension.() -> Unit) { 78 | extensions.configure(ApplicationExtension::class.java) { 79 | it.action() 80 | } 81 | } 82 | 83 | internal fun Project.androidComponentsApp(action: ApplicationAndroidComponentsExtension.() -> Unit) { 84 | extensions.configure(ApplicationAndroidComponentsExtension::class.java) { 85 | it.action() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/plugin/FreeleticsAndroidAppPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.plugin 2 | 3 | import com.android.build.api.AndroidPluginVersion 4 | import com.android.build.api.variant.HasUnitTestBuilder 5 | import com.freeletics.gradle.setup.configure 6 | import com.freeletics.gradle.util.addCompileOnlyDependency 7 | import com.freeletics.gradle.util.addImplementationDependency 8 | import com.freeletics.gradle.util.addMaybe 9 | import com.freeletics.gradle.util.androidApp 10 | import com.freeletics.gradle.util.androidComponentsApp 11 | import com.freeletics.gradle.util.defaultPackageName 12 | import com.freeletics.gradle.util.freeleticsExtension 13 | import com.freeletics.gradle.util.getBundleOrNull 14 | import com.freeletics.gradle.util.getDependencyOrNull 15 | import com.freeletics.gradle.util.getVersion 16 | import com.freeletics.gradle.util.getVersionOrNull 17 | import com.freeletics.gradle.util.javaTargetVersion 18 | import kotlin.jvm.java 19 | import kotlin.text.toInt 20 | import org.gradle.api.Plugin 21 | import org.gradle.api.Project 22 | 23 | public abstract class FreeleticsAndroidAppPlugin : Plugin { 24 | override fun apply(target: Project) { 25 | target.plugins.apply("com.android.application") 26 | if (AndroidPluginVersion.getCurrent() < AndroidPluginVersion(9, 0, 0).beta(1)) { 27 | target.plugins.apply("org.jetbrains.kotlin.android") 28 | } 29 | target.plugins.apply(FreeleticsBasePlugin::class.java) 30 | 31 | target.freeleticsExtension.extensions.create("android", FreeleticsAndroidAppExtension::class.java) 32 | 33 | target.androidSetup() 34 | target.addDefaultAndroidDependencies() 35 | target.configureLint() 36 | target.disableReleaseUnitTests() 37 | target.disableAndroidTests() 38 | } 39 | 40 | private fun Project.androidSetup() { 41 | val desugarLibrary = project.getDependencyOrNull("android.desugarjdklibs") 42 | androidApp { 43 | namespace = defaultPackageName() 44 | 45 | val buildTools = getVersionOrNull("android.buildTools") 46 | if (buildTools != null) { 47 | buildToolsVersion = buildTools 48 | } 49 | 50 | compileSdk = getVersion("android.compile").toInt() 51 | defaultConfig.minSdk = getVersion("android.min").toInt() 52 | defaultConfig.targetSdk = getVersion("android.target").toInt() 53 | 54 | buildFeatures { 55 | resValues = true 56 | buildConfig = true 57 | 58 | viewBinding = false 59 | dataBinding = false 60 | aidl = false 61 | shaders = false 62 | } 63 | 64 | compileOptions { 65 | isCoreLibraryDesugaringEnabled = desugarLibrary != null 66 | sourceCompatibility = javaTargetVersion 67 | targetCompatibility = javaTargetVersion 68 | } 69 | } 70 | 71 | dependencies.addMaybe("coreLibraryDesugaring", desugarLibrary) 72 | } 73 | 74 | private fun Project.addDefaultAndroidDependencies() { 75 | val bundle = getBundleOrNull("default-android") 76 | if (bundle != null) { 77 | addImplementationDependency(bundle) 78 | } 79 | val compileBundle = getBundleOrNull("default-android-compile") 80 | if (compileBundle != null) { 81 | addCompileOnlyDependency(compileBundle) 82 | } 83 | } 84 | 85 | private fun Project.configureLint() { 86 | androidApp { 87 | lint.configure(project) 88 | } 89 | } 90 | 91 | private fun Project.disableReleaseUnitTests() { 92 | androidComponentsApp { 93 | beforeVariants(selector().withBuildType("release")) { 94 | (it as? HasUnitTestBuilder)?.enableUnitTest = false 95 | } 96 | } 97 | } 98 | 99 | private fun Project.disableAndroidTests() { 100 | androidComponentsApp { 101 | beforeVariants { 102 | it.androidTest.enable = false 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/com/freeletics/gradle/plugin/FreeleticsBaseExtension.kt: -------------------------------------------------------------------------------- 1 | package com.freeletics.gradle.plugin 2 | 3 | import com.freeletics.gradle.setup.basicArgument 4 | import com.freeletics.gradle.setup.configureProcessing 5 | import com.freeletics.gradle.setup.setupCompose 6 | import com.freeletics.gradle.setup.setupInternalPublishing 7 | import com.freeletics.gradle.setup.setupOssPublishing 8 | import com.freeletics.gradle.setup.setupSqlDelight 9 | import com.freeletics.gradle.util.addApiDependency 10 | import com.freeletics.gradle.util.addKspDependency 11 | import com.freeletics.gradle.util.compilerOptionsCommon 12 | import com.freeletics.gradle.util.getDependency 13 | import com.freeletics.gradle.util.kotlin 14 | import java.io.File 15 | import org.gradle.api.Project 16 | import org.gradle.api.artifacts.ProjectDependency 17 | import org.gradle.api.plugins.ExtensionAware 18 | import org.gradle.api.tasks.InputDirectory 19 | import org.gradle.api.tasks.PathSensitive 20 | import org.gradle.api.tasks.PathSensitivity 21 | import org.gradle.process.CommandLineArgumentProvider 22 | 23 | public abstract class FreeleticsBaseExtension(private val project: Project) : ExtensionAware { 24 | public fun explicitApi() { 25 | project.kotlin { 26 | explicitApi() 27 | } 28 | } 29 | 30 | public fun optIn(vararg classes: String) { 31 | project.kotlin { 32 | compilerOptionsCommon { 33 | optIn.addAll(*classes) 34 | } 35 | } 36 | } 37 | 38 | public fun useCompose() { 39 | project.setupCompose() 40 | } 41 | 42 | public fun useSerialization() { 43 | project.plugins.apply("org.jetbrains.kotlin.plugin.serialization") 44 | 45 | project.addApiDependency(project.getDependency("kotlinx-serialization")) 46 | } 47 | 48 | public fun useMetro() { 49 | project.plugins.apply("dev.zacsweers.metro") 50 | } 51 | 52 | public fun useKhonshu() { 53 | useMetro() 54 | project.configureProcessing() 55 | project.addApiDependency(project.getDependency("khonshu-codegen-runtime")) 56 | project.addKspDependency(project.getDependency("khonshu-codegen-compiler")) 57 | // TODO workaround for Gradle not being able to resolve this in the ksp config 58 | project.configurations.named("ksp").configure { 59 | it.exclude(mapOf("group" to "org.jetbrains.skiko", "module" to "skiko")) 60 | } 61 | } 62 | 63 | public fun usePoko() { 64 | project.plugins.apply("dev.drewhamilton.poko") 65 | } 66 | 67 | public fun useKopy() { 68 | project.plugins.apply("com.javiersc.kotlin.kopy") 69 | project.plugins.apply("org.jetbrains.kotlin.plugin.atomicfu") 70 | } 71 | 72 | public fun useSqlDelight( 73 | name: String = "Database", 74 | dependency: ProjectDependency? = null, 75 | ) { 76 | project.setupSqlDelight(name, dependency) 77 | } 78 | 79 | public fun useRoom(schemaLocation: String? = null) { 80 | val processingArguments = buildList { 81 | add(basicArgument("room.generateKotlin", "true")) 82 | schemaLocation?.let { 83 | add(RoomSchemaArgProvider(schemaDir = File(project.projectDir, schemaLocation))) 84 | } 85 | } 86 | 87 | project.configureProcessing(processingArguments) 88 | project.addApiDependency(project.getDependency("androidx-room-runtime")) 89 | project.addKspDependency(project.getDependency("androidx-room-compiler")) 90 | } 91 | 92 | public fun useBurst() { 93 | project.plugins.apply("app.cash.burst") 94 | } 95 | 96 | public fun enableOssPublishing() { 97 | setupOssPublishing(project) 98 | } 99 | 100 | public fun enableInternalPublishing() { 101 | setupInternalPublishing(project) 102 | } 103 | 104 | private class RoomSchemaArgProvider( 105 | @get:InputDirectory 106 | @get:PathSensitive(PathSensitivity.RELATIVE) 107 | val schemaDir: File, 108 | ) : CommandLineArgumentProvider { 109 | override fun asArguments(): Iterable { 110 | return listOf("room.schemaLocation=${schemaDir.path}") 111 | } 112 | } 113 | } 114 | --------------------------------------------------------------------------------