jaredsburrows / gradle-license-plugin

Gradle plugin that provides a task to generate a HTML license report of your project.
https://jaredsburrows.github.io/gradle-license-plugin/
Apache License 2.0
414 stars 66 forks source link

Duplicate assets issue #226

Open realdadfish opened 2 years ago

realdadfish commented 2 years ago

In https://github.com/jaredsburrows/gradle-license-plugin/blob/a441c46b1b1f90792772d76638ddc48af53a2261/gradle-license-plugin/src/main/kotlin/com/jaredsburrows/license/projectAndroid.kt#L97 the plugin determines all configured assets directories of a specific build variant to be able to copy the generated resources to it.

IMHO this is the wrong approach, because this leads to a "duplicate assets" issue as shown below:

> Task :app:mergeDebugAssets FAILED
ERROR:/path/to/app/src/main/assets/open_source_licenses.html /path/to/app/build/generated/other-generated-assets/open_source_licenses.html: Resource and asset merger: Duplicate resources

It doesn't make much sense to just take "the first" or only the first not in /build either, because you cannot foresee what the module owner had in mind when (s)he configured his module with multiple asset directories. A better approach would be

  1. Add your own build directory to the assets source set, so it can be merged with the rest of the assets, or
  2. Allow users to configure a specific asset directory where generated assets should be copied to

Obviously, these two options should be mutual exclusive, otherwise people will stumble upon the same error above.

realdadfish commented 2 years ago

On a related note - generated assets should be separated by build variant under Android, so that

build/reports/licenses/firstVariant
build/reports/licenses/secondVariant
...

are all separated from each other and each target file can keep its original file name.

That way it is easier in multi-variant environments, because consumers don't have to rename files by hand or via a separate Gradle Copy task and also don't have to distinguish different filenames on runtime.

All you need to do is to add the appropriate variant-named build dir to the assets source set of the same variant and you're done.

realdadfish commented 1 year ago

The common output directory is even more problematic in Gradle 8, which introduced more strict task validation rules. Since all of the LicenseReport tasks output to the same configured @OutputDirectory, Gradle 8 cannot properly wire tasks that depend on the output of one specific task to another, further processing task. Consider the following example:

androidComponents.onVariants { variant ->
    val variantName = variant.name.capitalize()
    val licenseReportTask = tasks.matching { it.name == "license${variantName}Report" }
    val copyTask = tasks.register("copy${variantName}LicenseReport", Copy::class.java) {
        from(layout.buildDirectory.dir("reports/licenses/")) {  // <<-- this is the problematic line
            include("license${variantName}Report.*")
            rename("license${variantName}Report.(.*)", "open_source_licenses.$1")
        }
        into(layout.projectDirectory.dir("src/${variant.name}/assets/"))
    }
    licenseReportTask.configureEach {
        finalizedBy(copyTask)
    }
    tasks.matching { it.name == "merge${variantName}Assets" }.all {
        dependsOn(licenseReportTask)
        dependsOn(copyTask)
    }
}

errors out with

Reason: Task ':app:copyDevelopReleaseLicenseReport' uses this output of task ':app:licenseDevelopDebugReport' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. 

when the tasks for more than one build variant are executed at the same time.

AndreyBulgakow commented 1 year ago

Are there any news on this? Because I am struggling with it in our gradle upgrade from 7 to 8, in which the plugin produces a lot of error output and does not work properly.

jaredsburrows commented 9 months ago

@realdadfish Are you still having this issue?

I am trying to repro with 0.9.4:

git clone https://github.com/jaredsburrows/android-gif-search
cd android-gif-search
gradlew licenseDebugReport
gradlew licenseReleaseReport
gradlew assembleDebug
realdadfish commented 9 months ago

I switched projects in the meantime, in the current one I'm not using the plugin anymore, so I can't say. Maybe @AndreyBulgakow can say?

sergeys-opera commented 9 months ago

At least #339 seems to be fixed in 0.9.4

sergeys-opera commented 9 months ago

Actually the build failed to me on CI:

FAILURE: Build failed with an exception.
* What went wrong:
A problem was found with the configuration of task ':ui:licenseReleaseReport' (type 'LicenseReportTask').
  - Gradle detected a problem with the following location: '/builds/app/ui/build/reports/licenses'.

    Reason: Task ':ui:generateDebugLicenseReport' uses this output of task ':ui:licenseReleaseReport' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.

    Possible solutions:
      1. Declare task ':ui:licenseReleaseReport' as an input of ':ui:generateDebugLicenseReport'.
      2. Declare an explicit dependency on ':ui:licenseReleaseReport' from ':ui:generateDebugLicenseReport' using Task#dependsOn.
      3. Declare an explicit dependency on ':ui:licenseReleaseReport' from ':ui:generateDebugLicenseReport' using Task#mustRunAfter.

    For more information, please refer to https://docs.gradle.org/8.5/userguide/validation_problems.html#implicit_dependency in the Gradle documentation.
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org/.
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
BUILD FAILED
jaredsburrows commented 9 months ago

Where is :ui:generateDebugLicenseReport coming from? There is only one task in this project: licenseReport.

sergeys-opera commented 9 months ago

It's a custom task that triggers a licenseReport task and transforms its output to a different format:

private fun Project.createLicenseTask(
    variantName: String,
    assetDirs: Set<File>,
    ignoredPatterns: Set<String>
) = task("generate${variantName}LicenseReport") {
    // Trigger the license plugin task first to generate the JSON report.
    dependsOn("license${variantName}Report")

    // Prepare output directions and mark them as outputs.
    val outputDirName = "licenses"
    val outputDirs = assetDirs.map {
        it.toPath().resolve(outputDirName).toFile()
    }
    outputDirs.forEach {
        outputs.dir(it)
    }

    // Prepare inputs.
    val jsonReport = licenseReportFile(variantName)
    inputs.file(jsonReport)

    val knownLicensesDir = knownLicencesDir()
    inputs.dir(knownLicensesDir)

    val extraDeps = extraDepsFile()
    inputs.file(extraDeps)

    // Mapping between a license URL and its text.
    val knownLicensesByUrl = knownLicencesByUrl()

    doLast {
        val deps = loadDeps(
            from = jsonReport,
            ignoredPatterns = ignoredPatterns
        ).toMutableMap()

        // Load extra dependencies. These are dependencies coming from other
        // sources then the license report plugin. E.g. if we use a native
        // library or a jar-file requiring attribution.
        deps.putAll(loadDeps(from = extraDeps))

        // Make sure each dependency has a license defined.
        checkLicenseExistence(deps)

        // Add the license file name to the report or fail if missing.
        deps.values.forEach { dep ->
            dep.licenses?.forEach {
                val licenseFile = knownLicensesByUrl[it.licenseUrl]
                if (licenseFile != null) {
                    it.licenseFile = "$outputDirName/$licenseFile"
                } else {
                    throw GradleException("Missing license file for license url: ${it.licenseUrl} coming from ${dep.dependency}")
                }
            }
        }

        // Copy result json report and licenses to assets
        outputDirs.forEach { outputDir ->
            writeOutput(outputDir, deps, knownLicensesDir)
        }
    }
}

While generate${variantName}LicenseReport depends on license${variantName}Report Gradle doesn't like that license${variantName}Report writes reports into the same output folder.