/* * AboutOss is a utility library to retrieve and display * opensource licenses in Android applications. * * Copyright (C) 2023-2025 by Frederic-Charles Barthelery. * * This file is part of AboutOss. * * AboutOss is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * AboutOss is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with AboutOss. If not, see . */ package com.geekorum.build import com.android.build.api.dsl.AndroidSourceSet import com.android.build.api.dsl.CommonExtension import com.android.build.gradle.BaseExtension import com.android.build.gradle.DynamicFeaturePlugin import com.android.build.gradle.TestPlugin import com.hierynomus.gradle.license.LicenseBasePlugin import com.hierynomus.gradle.license.tasks.LicenseCheck import com.hierynomus.gradle.license.tasks.LicenseFormat import nl.javadude.gradle.plugins.license.License import nl.javadude.gradle.plugins.license.LicenseExtension import nl.javadude.gradle.plugins.license.LicensePlugin import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Project import org.gradle.api.file.FileTree import org.gradle.api.tasks.TaskProvider import org.gradle.kotlin.dsl.* import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper import org.jetbrains.kotlin.gradle.plugin.KotlinJsPluginWrapper import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper import java.io.File import java.util.Locale internal fun Project.configureSourceLicenseChecker() { apply() configure { header = file("$rootDir/config/license/header.txt") mapping("java", "SLASHSTAR_STYLE") mapping("json", "SLASHSTAR_STYLE") mapping("kt", "SLASHSTAR_STYLE") excludes(listOf("**/*.webp", "**/*.png", "**/*.jpeg", "**/*.jpg")) } // the LicensePlugin doesn't configure itself properly on DynamicFeaturePlugin // Copied the code to configure it plugins.withType { configureAndroid() } // the LicensePlugin doesn't configure itself properly on Android Test plugin // Copied the code to configure it plugins.withType { configureAndroid() } // make the license tasks looks for kotlin files in an Android project plugins.withType { configureKotlinAndroid() } // make the license tasks for kotlin js project plugins.withType { configureKotlin() } plugins.withType { configureKotlin() configureComposeResources() } tasks.withType().configureEach { notCompatibleWithConfigurationCache("License tasks calls getProject() at execution time") } } private fun Project.configureKotlin() { val kotlin = the() val taskInfix = "" kotlin.sourceSets.configureEach { val kotlinSource = this val sourceSetTaskName = "${LicenseBasePlugin.getLICENSE_TASK_BASE_NAME()}${taskInfix}${name.capitalize()}" if (name.startsWith("generated")) { logger.info("Skip sourceSet $name because it's generated code") return@configureEach } val configureLicenseCheckTaskLambda: LicenseCheck.() -> Unit = { source(kotlinSource.kotlin) exclude { // exclude generated files, notably protobuf, ksp, hilt "/generated/" in it.file.path } } if (sourceSetTaskName in tasks.names) { // tasks may have already been added by configuration for the Android plugin logger.info("Tasks $sourceSetTaskName already exists. configure it") tasks.named(sourceSetTaskName, LicenseCheck::class.java, configureLicenseCheckTaskLambda) } else { logger.info("Adding ${project.name}:$sourceSetTaskName task for sourceSet ${kotlinSource.name}") tasks.register(sourceSetTaskName, LicenseCheck::class.java, configureLicenseCheckTaskLambda) } val configureLicenseFormatTaskLambda: LicenseFormat.() -> Unit = { source(kotlinSource.kotlin) exclude { // exclude generated files, notably protobuf, ksp, hilt "/generated/" in it.file.path } } val sourceSetFormatTaskName = "${LicenseBasePlugin.getFORMAT_TASK_BASE_NAME()}${taskInfix}${name.capitalize()}" if (sourceSetFormatTaskName in tasks.names) { // tasks may have already been added by configuration for the Android plugin logger.info("Tasks $sourceSetFormatTaskName already exists. configure it") tasks.named(sourceSetFormatTaskName, LicenseFormat::class.java, configureLicenseFormatTaskLambda) } else { logger.info("Adding ${project.name}:$sourceSetFormatTaskName task for sourceSet ${kotlinSource.name}") tasks.register(sourceSetFormatTaskName, LicenseFormat::class.java, configureLicenseFormatTaskLambda) } } } private fun Project.configureComposeResources() { val kotlin = the() val taskInfix = "ComposeResources" kotlin.sourceSets.configureEach { val kotlinSource = this val sourceSetTaskName = "${LicenseBasePlugin.getLICENSE_TASK_BASE_NAME()}${taskInfix}${name.capitalize()}" if (name.startsWith("generated")) { logger.info("Skip sourceSet $name because it's generated code") return@configureEach } val resourceDir = kotlinSource.resources.sourceDirectories.files.first() val composeResourceDir = File(resourceDir, "../composeResources") val configureLicenseCheckTaskLambda: LicenseCheck.() -> Unit = { source(composeResourceDir) } if (sourceSetTaskName in tasks.names) { // tasks may have already been added by configuration for the Android plugin logger.info("Tasks $sourceSetTaskName already exists. configure it") tasks.named(sourceSetTaskName, LicenseCheck::class.java, configureLicenseCheckTaskLambda) } else { logger.info("Adding ${project.name}:$sourceSetTaskName task for sourceSet ${kotlinSource.name}") tasks.register(sourceSetTaskName, LicenseCheck::class.java, configureLicenseCheckTaskLambda) } val configureLicenseFormatTaskLambda: LicenseFormat.() -> Unit = { source(composeResourceDir) } val sourceSetFormatTaskName = "${LicenseBasePlugin.getFORMAT_TASK_BASE_NAME()}${taskInfix}${name.capitalize()}" if (sourceSetFormatTaskName in tasks.names) { // tasks may have already been added by configuration for the Android plugin logger.info("Tasks $sourceSetFormatTaskName already exists. configure it") tasks.named(sourceSetFormatTaskName, LicenseFormat::class.java, configureLicenseFormatTaskLambda) } else { logger.info("Adding ${project.name}:$sourceSetFormatTaskName task for sourceSet ${kotlinSource.name}") tasks.register(sourceSetFormatTaskName, LicenseFormat::class.java, configureLicenseFormatTaskLambda) } } } @OptIn(ExperimentalStdlibApi::class) private fun Project.configureKotlinAndroid() { val kotlin = the() val android = the() val taskInfix = "Android" android.sourceSets.configureEach { val kotlinSource = kotlin.sourceSets[name] logger.info("Adding kotlin sources from sourceSet $name to License plugin tasks") val sourceSetTaskName = "${LicenseBasePlugin.getLICENSE_TASK_BASE_NAME()}${taskInfix}${name.capitalize()}" tasks.named(sourceSetTaskName, LicenseCheck::class.java) { source(kotlinSource.kotlin, manifest.srcFile) exclude { // exclude generated files, notably protobuf "/generated/" in it.file.path } } val sourceSetFormatTaskName = "${LicenseBasePlugin.getFORMAT_TASK_BASE_NAME()}${taskInfix}${name.capitalize()}" tasks.named(sourceSetFormatTaskName, LicenseFormat::class.java) { source(kotlinSource.kotlin, manifest.srcFile) exclude { // exclude generated files, notably protobuf "/generated/" in it.file.path } } } } private fun Project.configureAndroid() { val android = the() configureSourceSetRule(android.sourceSets, "Android") { ss -> @Suppress("DEPRECATION") when (ss) { // the dsl.AndroidSourceSet don't expose any getter, so we still need to cast it is com.android.build.gradle.api.AndroidSourceSet -> { val kotlinFileTrees = ss.kotlin.directories.foldIndexed(fileTree() as FileTree) { index, acc, n -> if (index == 0) { // skip first as it's an empty tree created for typing fileTree(n) } else { acc + fileTree(n) } } ss.java.getSourceFiles() + ss.res.getSourceFiles() + fileTree(ss.manifest.srcFile) + kotlinFileTrees } else -> fileTree() } } } /** * Dynamically create a task for each sourceSet, and register with check */ @Suppress("DefaultLocale") private fun Project.configureSourceSetRule(androidSourceSetContainer: NamedDomainObjectContainer, taskInfix: String, sourceSetSources: (AndroidSourceSet) -> FileTree) { // This follows the other check task pattern androidSourceSetContainer.configureEach { val sourceSetTaskName = "${LicenseBasePlugin.getLICENSE_TASK_BASE_NAME()}${taskInfix}${name.capitalize()}" logger.info("Adding $sourceSetTaskName task for sourceSet $name") val checkTask = tasks.register(sourceSetTaskName, LicenseCheck::class.java) configureForSourceSet(this, checkTask, sourceSetSources) // Add independent license task, which will perform format val sourceSetFormatTaskName = "${LicenseBasePlugin.getFORMAT_TASK_BASE_NAME()}${taskInfix}${name.capitalize()}" val formatTask = tasks.register(sourceSetFormatTaskName, LicenseFormat::class.java) configureForSourceSet(this, formatTask, sourceSetSources) } } private fun configureForSourceSet(sourceSet: AndroidSourceSet, task: TaskProvider, sourceSetSources: (AndroidSourceSet) -> FileTree) { task.configure { // Explicitly set description description = "Scanning license on ${sourceSet.name} files" // Default to all source files from SourceSet source = sourceSetSources(sourceSet) } } private fun String.capitalize() = replaceFirstChar { if (it.isLowerCase()) it.titlecase( Locale.ROOT ) else it.toString() }