diff --git a/CHANGELOG.md b/CHANGELOG.md index 50b7ae4a..afcfa10c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,16 @@ improvements compared to previous versions of the plugin, such as: - Supporting [up-to-date checks](https://docs.gradle.org/current/userguide/more_about_tasks.html#sec:up_to_date_checks) - Supporting [task configuration avoidance](https://docs.gradle.org/current/userguide/task_configuration_avoidance.html) +- Supporting the [configuration cache](https://docs.gradle.org/current/userguide/configuration_cache.html) - Avoidance of unnecessary work See the [Upgrade Guide](./UPGRADING.md) for migration instructions. ### Changes +Support configuration caching +[#257](https://github.com/bugsnag/bugsnag-android-gradle-plugin/pull/257) + Convert plugin extension to use property syntax [#251](https://github.com/bugsnag/bugsnag-android-gradle-plugin/pull/251) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c053..e708b1c0 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8e9b6083..910c5a98 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Mon Jul 06 17:13:48 BST 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-rc-6-all.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat index a9f778a7..ac1b06f9 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +64,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/src/main/kotlin/com.bugsnag.android.gradle/AndroidManifestInfo.kt b/src/main/kotlin/com.bugsnag.android.gradle/AndroidManifestInfo.kt index aad52de4..2f7906af 100644 --- a/src/main/kotlin/com.bugsnag.android.gradle/AndroidManifestInfo.kt +++ b/src/main/kotlin/com.bugsnag.android.gradle/AndroidManifestInfo.kt @@ -9,11 +9,11 @@ import java.io.File @JsonClass(generateAdapter = true) data class AndroidManifestInfo( - var apiKey: String, - var versionCode: String, - var buildUUID: String, - var versionName: String, - var applicationId: String + val apiKey: String, + val versionCode: String, + val buildUUID: String, + val versionName: String, + val applicationId: String ) { internal fun write(file: File) { file.sink().buffer().use { diff --git a/src/main/kotlin/com.bugsnag.android.gradle/AndroidManifestParser.kt b/src/main/kotlin/com.bugsnag.android.gradle/AndroidManifestParser.kt index 02451528..a3657701 100644 --- a/src/main/kotlin/com.bugsnag.android.gradle/AndroidManifestParser.kt +++ b/src/main/kotlin/com.bugsnag.android.gradle/AndroidManifestParser.kt @@ -13,11 +13,10 @@ import java.io.IOException import java.io.PrintWriter import javax.xml.parsers.ParserConfigurationException -class AndroidManifestParser { +internal class AndroidManifestParser { private val namespace = Namespace("http://schemas.android.com/apk/res/android", "android") - @Throws(ParserConfigurationException::class, SAXException::class, IOException::class) fun readManifest(manifestPath: File, logger: Logger): AndroidManifestInfo { logger.debug("Bugsnag: Reading manifest at: $manifestPath") val root = XmlParser().parse(manifestPath) @@ -65,6 +64,7 @@ class AndroidManifestParser { |buildUUID=$buildUUID |versionName=$versionName |applicationId=$applicationId + |Manifest file = $manifestPath """.trimMargin()) } return AndroidManifestInfo(apiKey, versionCode, buildUUID, versionName, applicationId) diff --git a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagFileUploadTask.kt b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagFileUploadTask.kt index 8b9aa63c..04baf550 100644 --- a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagFileUploadTask.kt +++ b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagFileUploadTask.kt @@ -1,5 +1,6 @@ package com.bugsnag.android.gradle +import com.bugsnag.android.gradle.internal.BugsnagHttpClientHelper import org.gradle.api.provider.Property interface BugsnagFileUploadTask { @@ -8,6 +9,7 @@ interface BugsnagFileUploadTask { val endpoint: Property val retryCount: Property val timeoutMillis: Property + val httpClientHelper: Property fun configureWith(bugsnag: BugsnagPluginExtension) { failOnUploadError.set(bugsnag.failOnUploadError) diff --git a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagInstallJniLibsTask.kt b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagInstallJniLibsTask.kt index 598aba49..64c2a02b 100644 --- a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagInstallJniLibsTask.kt +++ b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagInstallJniLibsTask.kt @@ -1,42 +1,56 @@ package com.bugsnag.android.gradle +import com.bugsnag.android.gradle.internal.GradleVersions +import com.bugsnag.android.gradle.internal.register +import com.bugsnag.android.gradle.internal.versionNumber import org.gradle.api.DefaultTask import org.gradle.api.Project +import org.gradle.api.file.ArchiveOperations import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.CopySpec +import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.FileCollection -import org.gradle.api.file.RegularFileProperty +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.file.FileTree +import org.gradle.api.file.ProjectLayout import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.Property import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.WorkResult import java.io.File import javax.inject.Inject -open class BugsnagInstallJniLibsTask @Inject constructor( - objects: ObjectFactory +sealed class BugsnagInstallJniLibsTask( + objects: ObjectFactory, + projectLayout: ProjectLayout ) : DefaultTask() { - init { description = "Copies shared object files from the bugsnag-android AAR to the required build directory" group = BugsnagPlugin.GROUP_NAME } @get:OutputDirectory - val buildDirDestination: RegularFileProperty = objects.fileProperty() + val buildDirDestination: DirectoryProperty = objects.directoryProperty() + .convention(projectLayout.buildDirectory.dir("intermediates/bugsnag-libs")) @get:InputFiles - val bugsnagArtefacts: Property = objects.property(FileCollection::class.java) + abstract val bugsnagArtifacts: ConfigurableFileCollection + + internal abstract fun copy(action: (CopySpec) -> Unit): WorkResult + internal abstract fun zipTree(file: File): FileTree /** * Looks at all the dependencies and their dependencies and finds the `com.bugsnag` artifacts with SO files. */ @TaskAction fun setupNdkProject() { - bugsnagArtefacts.get().forEach { file: File -> - project.copy { - it.from(project.zipTree(file)) - it.into(project.file(buildDirDestination)) + val destination = buildDirDestination.asFile.get() + bugsnagArtifacts.forEach { file: File -> + copy { + it.from(zipTree(file)) + it.into(destination) } } } @@ -45,7 +59,7 @@ open class BugsnagInstallJniLibsTask @Inject constructor( private val sharedObjectAarIds = listOf("bugsnag-android", "bugsnag-android-ndk", "bugsnag-plugin-android-anr", "bugsnag-plugin-android-ndk") - internal fun resolveBugsnagArtefacts(project: Project): FileCollection { + internal fun resolveBugsnagArtifacts(project: Project): FileCollection { val files = project.configurations .filter { it.toString().contains("CompileClasspath") } .map { it.resolvedConfiguration } @@ -60,5 +74,93 @@ open class BugsnagInstallJniLibsTask @Inject constructor( .toSet() return project.files(files) } + + /** + * Registers the appropriate subtype to this [project] with the given [name] and + * [configurationAction] + */ + internal fun register( + project: Project, + name: String, + configurationAction: BugsnagInstallJniLibsTask.() -> Unit + ): TaskProvider { + val gradleVersion = project.gradle.versionNumber() + return when { + gradleVersion >= GradleVersions.VERSION_6 -> { + when { + gradleVersion >= GradleVersions.VERSION_6_6 -> { + project.tasks.register(name, configurationAction) + } + else -> { + project.tasks.register(name, configurationAction) + } + } + } + gradleVersion >= GradleVersions.VERSION_5_3 -> { + project.tasks.register(name, configurationAction) + } + else -> { + project.tasks.register(name, configurationAction) + } + } + } } } + +/** + * Legacy [BugsnagInstallJniLibsTask] task that requires using [getProject] and + * [ProjectLayout.configurableFiles]. + */ +internal open class BugsnagInstallJniLibsTaskLegacy @Inject constructor( + objects: ObjectFactory, + projectLayout: ProjectLayout +) : BugsnagInstallJniLibsTask(objects, projectLayout) { + @Suppress("DEPRECATION") // Here for backward compatibility + @get:InputFiles + override val bugsnagArtifacts: ConfigurableFileCollection = projectLayout.configurableFiles() + + override fun copy(action: (CopySpec) -> Unit): WorkResult = project.copy(action) + override fun zipTree(file: File): FileTree = project.zipTree(file) +} + +/** Legacy [BugsnagInstallJniLibsTask] task that requires using [getProject]. */ +internal open class BugsnagInstallJniLibsTask53Plus @Inject constructor( + objects: ObjectFactory, + projectLayout: ProjectLayout +) : BugsnagInstallJniLibsTask(objects, projectLayout) { + @get:InputFiles + override val bugsnagArtifacts: ConfigurableFileCollection = objects.fileCollection() + + override fun copy(action: (CopySpec) -> Unit): WorkResult = project.copy(action) + override fun zipTree(file: File): FileTree = project.zipTree(file) +} + +/** A Gradle 6+ compatible [BugsnagInstallJniLibsTask], which uses [FileSystemOperations]. */ +internal open class BugsnagInstallJniLibsTaskGradle6Plus @Inject constructor( + objects: ObjectFactory, + projectLayout: ProjectLayout, + private val fsOperations: FileSystemOperations +) : BugsnagInstallJniLibsTask(objects, projectLayout) { + @get:InputFiles + override val bugsnagArtifacts: ConfigurableFileCollection = objects.fileCollection() + + override fun copy(action: (CopySpec) -> Unit): WorkResult = fsOperations.copy(action) + override fun zipTree(file: File): FileTree = project.zipTree(file) +} + +/** + * A Gradle 6.6+ compatible [BugsnagInstallJniLibsTask], which uses [FileSystemOperations] + * and [ArchiveOperations] to support configuration caching. + */ +internal open class BugsnagInstallJniLibsTaskGradle66Plus @Inject constructor( + objects: ObjectFactory, + projectLayout: ProjectLayout, + private val fsOperations: FileSystemOperations, + private val archiveOperations: ArchiveOperations +) : BugsnagInstallJniLibsTask(objects, projectLayout) { + @get:InputFiles + override val bugsnagArtifacts: ConfigurableFileCollection = objects.fileCollection() + + override fun copy(action: (CopySpec) -> Unit): WorkResult = fsOperations.copy(action) + override fun zipTree(file: File): FileTree = archiveOperations.zipTree(file) +} diff --git a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagManifestUuidTask.kt b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagManifestUuidTask.kt index fe85d28a..2471f8e2 100644 --- a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagManifestUuidTask.kt +++ b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagManifestUuidTask.kt @@ -3,6 +3,7 @@ package com.bugsnag.android.gradle import com.android.build.gradle.api.ApkVariant import com.android.build.gradle.api.ApkVariantOutput import com.android.build.gradle.tasks.ManifestProcessorTask +import com.bugsnag.android.gradle.internal.property import org.gradle.api.DefaultTask import org.gradle.api.Project import org.gradle.api.file.Directory @@ -27,7 +28,7 @@ abstract class BaseBugsnagManifestUuidTask(objects: ObjectFactory) : DefaultTask } @get:Input - val buildUuid: Property = objects.property(String::class.java) + val buildUuid: Property = objects.property() @get:OutputFile val manifestInfoProvider: RegularFileProperty = objects.fileProperty() @@ -59,10 +60,10 @@ open class BugsnagManifestUuidTask @Inject constructor(objects: ObjectFactory) : fun updateManifest() { val manifestPath = getManifestPaths(project, variant, variantOutput) if (manifestPath == null) { - project.logger.warn("Bugsnag: Failed to find manifest at $manifestPath for $variantOutput") + logger.warn("Bugsnag: Failed to find manifest at $manifestPath for $variantOutput") } - project.logger.info("Bugsnag: Updating manifest with build UUID: $manifestPath") + logger.info("Bugsnag: Updating manifest with build UUID: $manifestPath") // read the manifest information and store it for subsequent tasks val manifestParser = AndroidManifestParser() @@ -98,16 +99,16 @@ open class BugsnagManifestUuidTask @Inject constructor(objects: ObjectFactory) : } val processManifest = variantOutput.processManifestProvider.get() if (getMergedManifest) { - directoryMerged = getManifestOutputDir(processManifest, project) + directoryMerged = getManifestOutputDir(processManifest) if (directoryMerged != null) { - addManifestPath(manifestPaths, directoryMerged, project.logger, variantOutput) + addManifestPath(manifestPaths, directoryMerged, logger, variantOutput) } } // Attempt to get the bundle manifest directory if required if (getBundleManifest) { directoryBundle = resolveBundleManifestOutputDirectory(processManifest) - addManifestPath(manifestPaths, directoryBundle, project.logger, variantOutput) + addManifestPath(manifestPaths, directoryBundle, logger, variantOutput) } require(manifestPaths.size == 1) { "Unexpected number of manifest paths.$manifestPaths" } return manifestPaths[0] @@ -116,31 +117,29 @@ open class BugsnagManifestUuidTask @Inject constructor(objects: ObjectFactory) : private fun addManifestPath(manifestPaths: MutableList, directory: File, logger: Logger, variantOutput: ApkVariantOutput) { val manifestFile = Paths.get(directory.toString(), variantOutput.dirName, "AndroidManifest.xml").toFile() if (manifestFile.exists()) { - logger.info("Bugsnag: Found manifest at ${manifestFile}") + logger.info("Bugsnag: Found manifest at $manifestFile") manifestPaths.add(manifestFile) } else { - logger.info("Bugsnag: Failed to find manifest at ${manifestFile}") + logger.info("Bugsnag: Failed to find manifest at $manifestFile") } } - private fun getManifestOutputDir(processManifest: ManifestProcessorTask, project: Project): File? { - try { + private fun getManifestOutputDir(processManifest: ManifestProcessorTask): File? { + return try { val outputDir = processManifest.javaClass.getMethod("getManifestOutputDirectory").invoke(processManifest) if (outputDir is File) { - return outputDir + outputDir } else { // gradle 4.7 introduced a provider API for lazy evaluation of properties, // AGP subsequently changed the API from File to Provider // see https://docs.gradle.org/4.7/userguide/lazy_configuration.html @Suppress("UNCHECKED_CAST") val dir = (outputDir as Provider).orNull - if (dir != null) { - return dir.asFile - } + dir?.asFile } } catch (exc: Throwable) { - project.logger.warn("Bugsnag: failed to find manifestOutputDir", exc) + logger.warn("Bugsnag: failed to find manifestOutputDir", exc) + null } - return null } private fun resolveBundleManifestOutputDirectory(processManifest: ManifestProcessorTask): File { diff --git a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagMultiPartUploadRequest.kt b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagMultiPartUploadRequest.kt index ce8f8e62..93848b59 100644 --- a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagMultiPartUploadRequest.kt +++ b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagMultiPartUploadRequest.kt @@ -16,7 +16,6 @@ import retrofit2.http.POST import retrofit2.http.PartMap import retrofit2.http.Url import java.io.File -import java.time.Duration /** * Task to upload ProGuard mapping files to Bugsnag. @@ -37,10 +36,10 @@ class BugsnagMultiPartUploadRequest( private val overwrite: Boolean, private val endpoint: String, private val retryCount: Int, - timeoutDuration: Duration + okHttpClient: OkHttpClient ) { - private val bugsnagService = createService(timeoutDuration) + private val bugsnagService = createService(okHttpClient) fun uploadMultipartEntity( parts: MutableMap, @@ -124,17 +123,10 @@ class BugsnagMultiPartUploadRequest( overwrite = task.overwrite.get(), endpoint = task.endpoint.get(), retryCount = task.retryCount.get(), - timeoutDuration = Duration.ofMillis(task.timeoutMillis.get()) + okHttpClient = task.httpClientHelper.get().okHttpClient ) } - private fun createService(timeoutDuration: Duration): BugsnagService { - return createService(OkHttpClient.Builder() - .connectTimeout(timeoutDuration) - .callTimeout(timeoutDuration) - .build()) - } - internal fun createService(client: OkHttpClient): BugsnagService { return Retrofit.Builder() .baseUrl("https://upload.bugsnag.com") // Not actually used diff --git a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagPlugin.kt b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagPlugin.kt index 4950753d..72a7744f 100644 --- a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagPlugin.kt +++ b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagPlugin.kt @@ -7,7 +7,15 @@ import com.android.build.gradle.api.ApkVariant import com.android.build.gradle.api.ApkVariantOutput import com.android.build.gradle.api.ApplicationVariant import com.android.build.gradle.tasks.ExternalNativeBuildTask -import com.bugsnag.android.gradle.BugsnagInstallJniLibsTask.Companion.resolveBugsnagArtefacts +import com.bugsnag.android.gradle.BugsnagInstallJniLibsTask.Companion.resolveBugsnagArtifacts +import com.bugsnag.android.gradle.internal.BugsnagHttpClientHelper +import com.bugsnag.android.gradle.internal.BuildServiceBugsnagHttpClientHelper +import com.bugsnag.android.gradle.internal.GradleVersions +import com.bugsnag.android.gradle.internal.LegacyBugsnagHttpClientHelper +import com.bugsnag.android.gradle.internal.UploadRequestClient +import com.bugsnag.android.gradle.internal.newUploadRequestClientProvider +import com.bugsnag.android.gradle.internal.register +import com.bugsnag.android.gradle.internal.versionNumber import org.gradle.api.DomainObjectSet import org.gradle.api.Plugin import org.gradle.api.Project @@ -15,7 +23,6 @@ import org.gradle.api.Task import org.gradle.api.file.RegularFile import org.gradle.api.provider.Provider import org.gradle.api.tasks.TaskProvider -import java.io.File import java.util.UUID /** @@ -41,10 +48,7 @@ class BugsnagPlugin : Plugin { const val BUNDLE_TASK = "bundle" } - private val releasesUploadClient = UploadRequestClient() - private val proguardUploadClient = UploadRequestClient() - private val ndkUploadClient = UploadRequestClient() - + @Suppress("LongMethod") override fun apply(project: Project) { // After Gradle 5.2, this can use service injection for injecting ObjectFactory val bugsnag = project.extensions.create( @@ -57,6 +61,24 @@ class BugsnagPlugin : Plugin { return@withPlugin } + val canUseBuildService = project.gradle.versionNumber() >= GradleVersions.VERSION_6_1 + val httpClientHelperProvider = if (canUseBuildService) { + project.gradle.sharedServices.registerIfAbsent("bugsnagHttpClientHelper", + BuildServiceBugsnagHttpClientHelper::class.java + ) { spec -> + // Provide some parameters + spec.parameters.timeoutMillis.set(bugsnag.requestTimeoutMs) + } + } else { + // Reuse instance + val client = LegacyBugsnagHttpClientHelper(bugsnag.requestTimeoutMs) + project.provider { client } + } + + val releasesUploadClientProvider = newUploadRequestClientProvider(project, "releases") + val proguardUploadClientProvider = newUploadRequestClientProvider(project, "proguard") + val ndkUploadClientProvider = newUploadRequestClientProvider(project, "ndk") + val android = project.extensions.getByType(AppExtension::class.java) if (BugsnagManifestUuidTaskV2.isApplicable()) { check(android is CommonExtension<*, *, *, *, *, *, *, *>) @@ -83,28 +105,40 @@ class BugsnagPlugin : Plugin { project.afterEvaluate { android.applicationVariants.configureEach { variant -> - registerBugsnagTasksForVariant(project, variant, bugsnag) + registerBugsnagTasksForVariant( + project, + variant, + bugsnag, + httpClientHelperProvider, + releasesUploadClientProvider, + proguardUploadClientProvider, + ndkUploadClientProvider + ) } registerNdkLibInstallTask(project, bugsnag, android) } } } - private fun registerNdkLibInstallTask(project: Project, - bugsnag: BugsnagPluginExtension, - android: AppExtension) { + private fun registerNdkLibInstallTask( + project: Project, + bugsnag: BugsnagPluginExtension, + android: AppExtension + ) { val ndkTasks = project.tasks.withType(ExternalNativeBuildTask::class.java) val cleanTasks = ndkTasks.filter { it.name.contains(CLEAN_TASK) }.toSet() val buildTasks = ndkTasks.filter { !it.name.contains(CLEAN_TASK) }.toSet() if (buildTasks.isNotEmpty()) { - val ndkSetupTask = project.tasks.create("bugsnagInstallJniLibsTask", BugsnagInstallJniLibsTask::class.java) - ndkSetupTask.buildDirDestination.set(File(project.buildDir, "/intermediates/bugsnag-libs")) - val files = resolveBugsnagArtefacts(project) - ndkSetupTask.bugsnagArtefacts.set(files) + val ndkSetupTask = BugsnagInstallJniLibsTask.register(project, "bugsnagInstallJniLibsTask") { + val files = resolveBugsnagArtifacts(project) + bugsnagArtifacts.from(files) + } if (isNdkUploadEnabled(bugsnag, android)) { - ndkSetupTask.mustRunAfter(cleanTasks) + ndkSetupTask.configure { + it.mustRunAfter(cleanTasks) + } buildTasks.forEach { it.dependsOn(ndkSetupTask) } } } @@ -115,10 +149,15 @@ class BugsnagPlugin : Plugin { * * See https://sites.google.com/a/android.com/tools/tech-docs/new-build-system/user-guide#TOC-Build-Variants */ + @Suppress("LongParameterList") private fun registerBugsnagTasksForVariant( project: Project, variant: ApkVariant, - bugsnag: BugsnagPluginExtension + bugsnag: BugsnagPluginExtension, + httpClientHelperProvider: Provider, + releasesUploadClientProvider: Provider, + proguardUploadClientProvider: Provider, + ndkUploadClientProvider: Provider ) { variant.outputs.configureEach { val output = it as ApkVariantOutput @@ -133,11 +172,23 @@ class BugsnagPlugin : Plugin { // register bugsnag tasks val manifestInfoFile = registerManifestUuidTask(project, variant, output) val proguardTask = when { - jvmMinificationEnabled -> registerProguardUploadTask(project, variant, output, bugsnag) + jvmMinificationEnabled -> registerProguardUploadTask( + project, + variant, + output, + bugsnag, + httpClientHelperProvider + ) else -> null } val symbolFileTask = when { - ndkEnabled -> registerSharedObjectUploadTask(project, variant, output, bugsnag) + ndkEnabled -> registerSharedObjectUploadTask( + project, + variant, + output, + bugsnag, + httpClientHelperProvider + ) else -> null } val releasesTask = registerReleasesUploadTask(project, variant, output, bugsnag) @@ -149,15 +200,15 @@ class BugsnagPlugin : Plugin { val mappingFileProvider = createMappingFileProvider(project, variant, output) task.mappingFileProperty.set(mappingFileProvider) releasesTask.get().jvmMappingFileProperty.set(mappingFileProvider) - task.uploadRequestClient.set(proguardUploadClient) + task.uploadRequestClient.set(proguardUploadClientProvider) } symbolFileTask?.get()?.let { task -> val ndkSearchDirs = symbolFileTask.get().searchDirectories - releasesTask.get().ndkMappingFileProperty.set(ndkSearchDirs) - task.uploadRequestClient.set(ndkUploadClient) + releasesTask.get().ndkMappingFileProperty.from(ndkSearchDirs) + task.uploadRequestClient.set(ndkUploadClientProvider) } - releasesTask.get().uploadRequestClient.set(releasesUploadClient) + releasesTask.get().uploadRequestClient.set(releasesUploadClientProvider) releasesTask.get().manifestInfoFile.set(manifestInfoFile) symbolFileTask?.get()?.manifestInfoFile?.set(manifestInfoFile) releasesTask.get().manifestInfoFile.set(manifestInfoFile) @@ -198,60 +249,72 @@ class BugsnagPlugin : Plugin { /** * Creates a bugsnag task to upload proguard mapping file */ - private fun registerProguardUploadTask(project: Project, - variant: ApkVariant, - output: ApkVariantOutput, - bugsnag: BugsnagPluginExtension): TaskProvider { + private fun registerProguardUploadTask( + project: Project, + variant: ApkVariant, + output: ApkVariantOutput, + bugsnag: BugsnagPluginExtension, + httpClientHelperProvider: Provider + ): TaskProvider { val outputName = taskNameForOutput(output) val taskName = "uploadBugsnag${outputName}Mapping" val path = "intermediates/bugsnag/requests/proguardFor${outputName}.json" - val requestOutputFile = project.layout.buildDirectory.file(path) - return project.tasks.register(taskName, BugsnagUploadProguardTask::class.java) { - it.requestOutputFile.set(requestOutputFile) - addTaskToExecutionGraph(it, variant, output, project, bugsnag, bugsnag.uploadJvmMappings.get()) - it.configureWith(bugsnag) + val requestOutputFileProvider = project.layout.buildDirectory.file(path) + return project.tasks.register(taskName) { + requestOutputFile.set(requestOutputFileProvider) + httpClientHelper.set(httpClientHelperProvider) + addTaskToExecutionGraph(this, variant, output, project, bugsnag, bugsnag.uploadJvmMappings.get()) + configureWith(bugsnag) } } - private fun registerSharedObjectUploadTask(project: Project, - variant: ApkVariant, - output: ApkVariantOutput, - bugsnag: BugsnagPluginExtension): TaskProvider { + private fun registerSharedObjectUploadTask( + project: Project, + variant: ApkVariant, + output: ApkVariantOutput, + bugsnag: BugsnagPluginExtension, + httpClientHelperProvider: Provider + ): TaskProvider { // Create a Bugsnag task to upload NDK mapping file(s) val outputName = taskNameForOutput(output) val taskName = "uploadBugsnagNdk${outputName}Mapping" val path = "intermediates/bugsnag/requests/ndkFor${outputName}.json" val requestOutputFile = project.layout.buildDirectory.file(path) - return project.tasks.register(taskName, BugsnagUploadNdkTask::class.java) { - it.requestOutputFile.set(requestOutputFile) - it.projectRoot.set(bugsnag.projectRoot.getOrElse(project.projectDir.toString())) - it.searchDirectories.set(getSearchDirectories(project, variant)) - it.variantOutput = output - addTaskToExecutionGraph(it, variant, output, project, bugsnag, true) - it.configureWith(bugsnag) + return BugsnagUploadNdkTask.register(project, taskName) { + this.requestOutputFile.set(requestOutputFile) + projectRoot.set(bugsnag.projectRoot.getOrElse(project.projectDir.toString())) + searchDirectories.from(getSearchDirectories(project, variant)) + variantOutput = output + objDumpPaths.set(bugsnag.objdumpPaths) + httpClientHelper.set(httpClientHelperProvider) + addTaskToExecutionGraph(this, variant, output, project, bugsnag, true) + configureWith(bugsnag) } } - private fun registerReleasesUploadTask(project: Project, - variant: ApkVariant, - output: ApkVariantOutput, - bugsnag: BugsnagPluginExtension): TaskProvider { + private fun registerReleasesUploadTask( + project: Project, + variant: ApkVariant, + output: ApkVariantOutput, + bugsnag: BugsnagPluginExtension + ): TaskProvider { val outputName = taskNameForOutput(output) val taskName = "bugsnagRelease${outputName}Task" val path = "intermediates/bugsnag/requests/releasesFor${outputName}.json" val requestOutputFile = project.layout.buildDirectory.file(path) - return project.tasks.register(taskName, BugsnagReleasesTask::class.java) { - it.requestOutputFile.set(requestOutputFile) - it.retryCount.set(bugsnag.retryCount) - it.timeoutMillis.set(bugsnag.requestTimeoutMs) - it.releasesEndpoint.set(bugsnag.releasesEndpoint) - it.sourceControlProvider.set(bugsnag.sourceControl.provider) - it.sourceControlRepository.set(bugsnag.sourceControl.repository) - it.sourceControlRevision.set(bugsnag.sourceControl.revision) - it.metadata.set(bugsnag.metadata) - it.builderName.set(bugsnag.builderName) - addTaskToExecutionGraph(it, variant, output, project, bugsnag, bugsnag.reportBuilds.get()) - it.configureMetadata(project) + return BugsnagReleasesTask.register(project, taskName) { + this.requestOutputFile.set(requestOutputFile) + retryCount.set(bugsnag.retryCount) + timeoutMillis.set(bugsnag.requestTimeoutMs) + releasesEndpoint.set(bugsnag.releasesEndpoint) + sourceControlProvider.set(bugsnag.sourceControl.provider) + sourceControlRepository.set(bugsnag.sourceControl.repository) + sourceControlRevision.set(bugsnag.sourceControl.revision) + metadata.set(bugsnag.metadata) + builderName.set(bugsnag.builderName) + gradleVersion.set(project.gradle.gradleVersion) + addTaskToExecutionGraph(this, variant, output, project, bugsnag, bugsnag.reportBuilds.get()) + configureMetadata() } } @@ -317,7 +380,7 @@ class BugsnagPlugin : Plugin { output: ApkVariantOutput, prefix: String): Set { val variantName = output.name.split("-")[0].capitalize() - val assembleTask = variant.assembleProvider.getOrNull() + val assembleTask = variant.assembleProvider.orNull val taskNames = HashSet() taskNames.add(prefix) diff --git a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagPluginExtension.kt b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagPluginExtension.kt index beb810f1..106f99e1 100644 --- a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagPluginExtension.kt +++ b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagPluginExtension.kt @@ -1,5 +1,9 @@ package com.bugsnag.android.gradle +import com.bugsnag.android.gradle.internal.listProperty +import com.bugsnag.android.gradle.internal.mapProperty +import com.bugsnag.android.gradle.internal.newInstance +import com.bugsnag.android.gradle.internal.property import groovy.lang.Closure import org.gradle.api.Action import org.gradle.api.model.ObjectFactory @@ -19,53 +23,53 @@ private val NULL_BOOLEAN: Boolean? = null // After Gradle 5.2, this can use service injection for injecting ObjectFactory open class BugsnagPluginExtension(objects: ObjectFactory) { - val sourceControl: SourceControl = objects.newInstance(SourceControl::class.java) + val sourceControl: SourceControl = objects.newInstance() - val enabled: Property = objects.property(Boolean::class.javaObjectType) + val enabled: Property = objects.property() .convention(true) - val uploadJvmMappings: Property = objects.property(Boolean::class.javaObjectType) + val uploadJvmMappings: Property = objects.property() .convention(true) - val uploadNdkMappings: Property = objects.property(Boolean::class.javaObjectType) + val uploadNdkMappings: Property = objects.property() .convention(NULL_BOOLEAN) - val reportBuilds: Property = objects.property(Boolean::class.javaObjectType) + val reportBuilds: Property = objects.property() .convention(true) - val uploadDebugBuildMappings: Property = objects.property(Boolean::class.javaObjectType) + val uploadDebugBuildMappings: Property = objects.property() .convention(false) - val endpoint: Property = objects.property(String::class.javaObjectType) + val endpoint: Property = objects.property() .convention("https://upload.bugsnag.com") - val releasesEndpoint = objects.property(String::class.javaObjectType) + val releasesEndpoint = objects.property() .convention("https://build.bugsnag.com") - val overwrite: Property = objects.property(Boolean::class.javaObjectType) + val overwrite: Property = objects.property() .convention(false) - val retryCount: Property = objects.property(Int::class.javaObjectType) + val retryCount: Property = objects.property() .convention(0) - val sharedObjectPaths: ListProperty = objects.listProperty(File::class.java) + val sharedObjectPaths: ListProperty = objects.listProperty() .convention(emptyList()) - val projectRoot: Property = objects.property(String::class.java).convention(NULL_STRING) + val projectRoot: Property = objects.property().convention(NULL_STRING) - val failOnUploadError: Property = objects.property(Boolean::class.javaObjectType) + val failOnUploadError: Property = objects.property() .convention(true) - val requestTimeoutMs: Property = objects.property(Long::class.javaObjectType) + val requestTimeoutMs: Property = objects.property() .convention(60000) // release API values - val builderName: Property = objects.property(String::class.java).convention(NULL_STRING) + val builderName: Property = objects.property().convention(NULL_STRING) - val metadata: MapProperty = objects.mapProperty(String::class.java, String::class.java) + val metadata: MapProperty = objects.mapProperty() .convention(emptyMap()) - val objdumpPaths: MapProperty = objects.mapProperty(String::class.java, String::class.java) + val objdumpPaths: MapProperty = objects.mapProperty() .convention(emptyMap()) // exposes sourceControl as a nested object on the extension, diff --git a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagReleasesTask.kt b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagReleasesTask.kt index 8071b119..79c5a55a 100644 --- a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagReleasesTask.kt +++ b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagReleasesTask.kt @@ -1,24 +1,36 @@ package com.bugsnag.android.gradle +import com.bugsnag.android.gradle.internal.GradleVersions +import com.bugsnag.android.gradle.internal.UploadRequestClient +import com.bugsnag.android.gradle.internal.mapProperty +import com.bugsnag.android.gradle.internal.property +import com.bugsnag.android.gradle.internal.register +import com.bugsnag.android.gradle.internal.versionNumber import com.squareup.moshi.JsonClass import okhttp3.OkHttpClient import org.gradle.api.DefaultTask import org.gradle.api.Project -import org.gradle.api.file.FileCollection +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.ProjectLayout import org.gradle.api.file.RegularFileProperty import org.gradle.api.logging.LogLevel import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property +import org.gradle.api.provider.ProviderFactory import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.Internal import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity.NONE import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.process.ExecOperations +import org.gradle.process.ExecResult +import org.gradle.process.ExecSpec import org.gradle.process.internal.ExecException import org.gradle.util.VersionNumber import retrofit2.Response @@ -36,8 +48,9 @@ import java.nio.charset.Charset import java.time.Duration import javax.inject.Inject -open class BugsnagReleasesTask @Inject constructor( - objects: ObjectFactory +sealed class BugsnagReleasesTask( + objects: ObjectFactory, + private val providerFactory: ProviderFactory ) : DefaultTask(), AndroidManifestInfoReceiver { init { @@ -46,7 +59,7 @@ open class BugsnagReleasesTask @Inject constructor( } @get:Internal - internal val uploadRequestClient: Property = objects.property(UploadRequestClient::class.java) + internal val uploadRequestClient: Property = objects.property() @get:PathSensitive(NONE) @get:InputFile @@ -66,68 +79,70 @@ open class BugsnagReleasesTask @Inject constructor( @get:PathSensitive(NONE) @get:InputFiles @get:Optional - val ndkMappingFileProperty: Property = objects.property(FileCollection::class.java) + abstract val ndkMappingFileProperty: ConfigurableFileCollection @get:Input - val retryCount: Property = objects.property(Int::class.javaObjectType) + val retryCount: Property = objects.property() @get:Input - val timeoutMillis: Property = objects.property(Long::class.javaObjectType) + val timeoutMillis: Property = objects.property() @get:Input - val releasesEndpoint: Property = objects.property(String::class.java) + val releasesEndpoint: Property = objects.property() @get:Optional @get:Input - val builderName: Property = objects.property(String::class.java) + val builderName: Property = objects.property() @get:Optional @get:Input - val metadata: MapProperty = objects.mapProperty(String::class.java, String::class.java) + val metadata: MapProperty = objects.mapProperty() @get:Optional @get:Input - val sourceControlProvider: Property = objects.property(String::class.java) + val sourceControlProvider: Property = objects.property() @get:Optional @get:Input - val sourceControlRepository: Property = objects.property(String::class.java) + val sourceControlRepository: Property = objects.property() @get:Optional @get:Input - val sourceControlRevision: Property = objects.property(String::class.java) + val sourceControlRevision: Property = objects.property() @get:Input @get:Optional - val osArch: Property = objects.property(String::class.java) + val osArch: Property = objects.property() @get:Input @get:Optional - val osName: Property = objects.property(String::class.java) + val osName: Property = objects.property() @get:Input @get:Optional - val osVersion: Property = objects.property(String::class.java) + val osVersion: Property = objects.property() @get:Input @get:Optional - val javaVersion: Property = objects.property(String::class.java) + val javaVersion: Property = objects.property() @get:Input @get:Optional - val gradleVersion: Property = objects.property(String::class.java) + val gradleVersion: Property = objects.property() @get:Input @get:Optional - val gitVersion: Property = objects.property(String::class.java) + val gitVersion: Property = objects.property() + + internal abstract fun exec(action: (ExecSpec) -> Unit): ExecResult @TaskAction fun fetchReleaseInfo() { val manifestInfo = parseManifestInfo() val payload = generateJsonPayload(manifestInfo) - val response = uploadRequestClient.get().makeRequestIfNeeded(manifestInfo, payload.toString()) { - project.logger.lifecycle("Bugsnag: Attempting upload to Releases API") + val response = uploadRequestClient.get().makeRequestIfNeeded(manifestInfo, payload.hashCode()) { + logger.lifecycle("Bugsnag: Attempting upload to Releases API") lateinit var response: String object : Call(retryCount, logger) { override fun makeApiCall(): Boolean { @@ -140,7 +155,7 @@ open class BugsnagReleasesTask @Inject constructor( response } requestOutputFile.asFile.get().writeText(response) - project.logger.lifecycle("Bugsnag: Releases request complete") + logger.lifecycle("Bugsnag: Releases request complete") } private fun deliverPayload( @@ -240,7 +255,7 @@ open class BugsnagReleasesTask @Inject constructor( private fun runCmd(vararg cmd: String): String? { return try { val baos = ByteArrayOutputStream() - project.exec { execSpec -> + exec { execSpec -> execSpec.commandLine(*cmd) execSpec.standardOutput = baos logging.captureStandardError(LogLevel.INFO) @@ -251,21 +266,22 @@ open class BugsnagReleasesTask @Inject constructor( } } - internal fun configureMetadata(project: Project) { - val gradleVersionString = project.gradle.gradleVersion - val gradleVersionNumber = VersionNumber.parse(gradleVersionString) - gradleVersion.set(gradleVersionString) - gitVersion.set(project.provider { runCmd(VCS_COMMAND, "--version") } ) - if (gradleVersionNumber >= SYS_PROPERTIES_VERSION) { - osArch.set(project.providers.systemProperty(MK_OS_ARCH) ) - osName.set(project.providers.systemProperty(MK_OS_NAME) ) - osVersion.set(project.providers.systemProperty(MK_OS_VERSION) ) - javaVersion.set(project.providers.systemProperty(MK_JAVA_VERSION)) + internal fun configureMetadata() { + val gradleVersionNumber = gradleVersion.orNull?.let { + gradleVersion.set(it) + VersionNumber.parse(it) + } + gitVersion.set(providerFactory.provider { runCmd(VCS_COMMAND, "--version") } ) + if (gradleVersionNumber != null && gradleVersionNumber >= GradleVersions.VERSION_6_1) { + osArch.set(providerFactory.systemProperty(MK_OS_ARCH) ) + osName.set(providerFactory.systemProperty(MK_OS_NAME) ) + osVersion.set(providerFactory.systemProperty(MK_OS_VERSION) ) + javaVersion.set(providerFactory.systemProperty(MK_JAVA_VERSION)) } else { - osArch.set(project.provider { System.getProperty(MK_OS_ARCH) } ) - osName.set(project.provider { System.getProperty(MK_OS_NAME) } ) - osVersion.set(project.provider { System.getProperty(MK_OS_VERSION) } ) - javaVersion.set(project.provider { System.getProperty(MK_JAVA_VERSION) }) + osArch.set(providerFactory.provider { System.getProperty(MK_OS_ARCH) } ) + osName.set(providerFactory.provider { System.getProperty(MK_OS_NAME) } ) + osVersion.set(providerFactory.provider { System.getProperty(MK_OS_VERSION) } ) + javaVersion.set(providerFactory.provider { System.getProperty(MK_JAVA_VERSION) }) } } @@ -278,7 +294,6 @@ open class BugsnagReleasesTask @Inject constructor( private const val MK_JAVA_VERSION = "java.version" private const val VCS_COMMAND = "git" private const val CHARSET_UTF8 = "UTF-8" - private val SYS_PROPERTIES_VERSION = VersionNumber.parse("6.1") @JvmStatic fun isValidVcsProvider(provider: String?): Boolean { @@ -318,6 +333,81 @@ open class BugsnagReleasesTask @Inject constructor( .build() .create() } + + /** + * Registers the appropriate subtype to this [project] with the given [name] and + * [configurationAction] + */ + internal fun register( + project: Project, + name: String, + configurationAction: BugsnagReleasesTask.() -> Unit + ): TaskProvider { + return when { + project.gradle.versionNumber() >= GradleVersions.VERSION_6 -> { + project.tasks.register(name, configurationAction) + } + project.gradle.versionNumber() >= GradleVersions.VERSION_5_3 -> { + project.tasks.register(name, configurationAction) + } + else -> { + project.tasks.register(name, configurationAction) + } + } + } + } +} + +/** + * Legacy [BugsnagReleasesTask] task that requires using [getProject] and + * [ProjectLayout.configurableFiles]. + */ +internal open class BugsnagReleasesTaskLegacy @Inject constructor( + objects: ObjectFactory, + providerFactory: ProviderFactory, + projectLayout: ProjectLayout +) : BugsnagReleasesTask(objects, providerFactory) { + + @Suppress("DEPRECATION") // Here for backward compat + @get:PathSensitive(NONE) + @get:InputFiles + @get:Optional + override val ndkMappingFileProperty: ConfigurableFileCollection = projectLayout.configurableFiles() + + override fun exec(action: (ExecSpec) -> Unit): ExecResult = project.exec(action) +} + +/** Legacy [BugsnagReleasesTask] task that requires using [getProject]. */ +internal open class BugsnagReleasesTaskGradle53Plus @Inject constructor( + objects: ObjectFactory, + providerFactory: ProviderFactory +) : BugsnagReleasesTask(objects, providerFactory) { + @get:PathSensitive(NONE) + @get:InputFiles + @get:Optional + override val ndkMappingFileProperty: ConfigurableFileCollection = objects.fileCollection() + + override fun exec(action: (ExecSpec) -> Unit): ExecResult { + return project.exec(action) + } +} + +/** + * A Gradle 6.0+ compatible [BugsnagReleasesTask], which uses [ExecOperations] + * and supports configuration caching. + */ +internal open class BugsnagReleasesTaskGradle6Plus @Inject constructor( + objects: ObjectFactory, + providerFactory: ProviderFactory, + private val execOperations: ExecOperations +) : BugsnagReleasesTask(objects, providerFactory) { + @get:PathSensitive(NONE) + @get:InputFiles + @get:Optional + override val ndkMappingFileProperty: ConfigurableFileCollection = objects.fileCollection() + + override fun exec(action: (ExecSpec) -> Unit): ExecResult { + return execOperations.exec(action) } } diff --git a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagUploadNdkTask.kt b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagUploadNdkTask.kt index 8ec92288..9286607b 100644 --- a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagUploadNdkTask.kt +++ b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagUploadNdkTask.kt @@ -3,7 +3,17 @@ package com.bugsnag.android.gradle import com.android.build.gradle.AppExtension import com.android.build.gradle.api.ApkVariantOutput import com.bugsnag.android.gradle.Abi.Companion.findByName +import com.bugsnag.android.gradle.internal.BugsnagHttpClientHelper +import com.bugsnag.android.gradle.internal.GradleVersions +import com.bugsnag.android.gradle.internal.UploadRequestClient +import com.bugsnag.android.gradle.internal.mapProperty +import com.bugsnag.android.gradle.internal.md5HashCode +import com.bugsnag.android.gradle.internal.property +import com.bugsnag.android.gradle.internal.register +import com.bugsnag.android.gradle.internal.versionNumber import okhttp3.RequestBody +import okio.HashingSource +import okio.blackholeSink import okio.buffer import okio.gzip import okio.sink @@ -11,18 +21,23 @@ import okio.source import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.DefaultTask import org.gradle.api.Project -import org.gradle.api.file.FileCollection +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.ProjectLayout import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity.NONE import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider import java.io.File import java.io.InputStream import java.io.Reader @@ -41,8 +56,9 @@ import javax.inject.Inject * it is usually safe to have this be the absolute last task executed during * a build. */ -open class BugsnagUploadNdkTask @Inject constructor( - objects: ObjectFactory +sealed class BugsnagUploadNdkTask( + objects: ObjectFactory, + projectLayout: ProjectLayout ) : DefaultTask(), AndroidManifestInfoReceiver, BugsnagFileUploadTask { init { @@ -51,13 +67,13 @@ open class BugsnagUploadNdkTask @Inject constructor( } @get:Internal - internal val uploadRequestClient: Property = objects.property(UploadRequestClient::class.java) + internal val uploadRequestClient: Property = objects.property() - @Input - val projectRoot: Property = objects.property(String::class.javaObjectType) + @get:Internal + override val httpClientHelper: Property = objects.property() - @get:InputFiles - val searchDirectories: Property = objects.property(FileCollection::class.java) + @Input + val projectRoot: Property = objects.property() @get:PathSensitive(NONE) @get:InputFile @@ -69,33 +85,43 @@ open class BugsnagUploadNdkTask @Inject constructor( @get:OutputFile val requestOutputFile: RegularFileProperty = objects.fileProperty() + @get:OutputDirectory + val intermediateOutputDir: DirectoryProperty = objects.directoryProperty() + .convention(projectLayout.buildDirectory.dir("intermediates/bugsnag")) + @get:Input - override val failOnUploadError: Property = objects.property(Boolean::class.javaObjectType) + override val failOnUploadError: Property = objects.property() @get:Input - override val overwrite: Property = objects.property(Boolean::class.javaObjectType) + override val overwrite: Property = objects.property() @get:Input - override val endpoint: Property = objects.property(String::class.javaObjectType) + override val endpoint: Property = objects.property() @get:Input - override val retryCount: Property = objects.property(Int::class.javaObjectType) + override val retryCount: Property = objects.property() @get:Input - override val timeoutMillis: Property = objects.property(Long::class.javaObjectType) + override val timeoutMillis: Property = objects.property() + + @get:Input + val objDumpPaths: MapProperty = objects.mapProperty() + + @get:InputFiles + abstract val searchDirectories: ConfigurableFileCollection @TaskAction fun upload() { - project.logger.lifecycle("Starting ndk upload") - val searchDirs = searchDirectories.get().files.toList() + logger.lifecycle("Starting ndk upload") + val searchDirs = searchDirectories.files.toList() val files = findSharedObjectMappingFiles(project, variantOutput, searchDirs) - project.logger.lifecycle("Processing shared object files") + logger.lifecycle("Processing shared object files") processFiles(files) requestOutputFile.asFile.get().writeText("OK") } private fun processFiles(files: Collection) { - project.logger.info("Bugsnag: Found shared object files for upload: $files") + logger.info("Bugsnag: Found shared object files for upload: $files") files.forEach { file -> processFile(file) @@ -119,14 +145,11 @@ open class BugsnagUploadNdkTask @Inject constructor( private fun generateSymbolsForSharedObject(sharedObject: File, arch: String): File? { // Get the path the version of objdump to use to get symbols val objDumpPath = getObjDumpExecutable(arch) - val logger = project.logger + val logger = logger if (objDumpPath != null) { val outReader: Reader? = null try { - val outputDir = File(project.buildDir, "bugsnag") - if (!outputDir.exists()) { - outputDir.mkdir() - } + val outputDir = intermediateOutputDir.asFile.get() val outputFile = File(outputDir, "$arch.gz") val errorOutputFile = File(outputDir, "$arch.error.txt") logger.info("Bugsnag: Creating symbol file for $arch at $outputFile") @@ -168,7 +191,7 @@ open class BugsnagUploadNdkTask @Inject constructor( // a SO file may not contain debug info. if that's the case then the mapping file should be very small, // so we try and reject it here as otherwise the event-worker will reject it with a 400 status code. if (!mappingFile.exists() || mappingFile.length() < VALID_SO_FILE_THRESHOLD) { - project.logger.warn("Bugsnag: Skipping upload of empty/invalid mapping file: $mappingFile") + logger.warn("Bugsnag: Skipping upload of empty/invalid mapping file: $mappingFile") return } val parts = mutableMapOf() @@ -181,13 +204,13 @@ open class BugsnagUploadNdkTask @Inject constructor( } parts["projectRoot"] = projectRoot.get().toTextRequestBody() val request = BugsnagMultiPartUploadRequest.from(this) - project.logger.lifecycle("Bugsnag: Attempting to upload shared object mapping " + + logger.lifecycle("Bugsnag: Attempting to upload shared object mapping " + "file for $sharedObjectName-$arch from $mappingFile") val manifestInfo = parseManifestInfo() - val mappingFileContents = mappingFile.readText() - val response = uploadRequestClient.get().makeRequestIfNeeded(manifestInfo, mappingFileContents) { - project.logger.lifecycle("Bugsnag: Attempting to upload shared object mapping " + + val mappingFileHash = mappingFile.md5HashCode() + val response = uploadRequestClient.get().makeRequestIfNeeded(manifestInfo, mappingFileHash) { + logger.lifecycle("Bugsnag: Attempting to upload shared object mapping " + "file for $sharedObjectName-$arch from $mappingFile") request.uploadMultipartEntity(parts, parseManifestInfo()) } @@ -210,15 +233,13 @@ open class BugsnagUploadNdkTask @Inject constructor( } return objDumpFile } catch (ex: Throwable) { - project.logger.error("Bugsnag: Error attempting to calculate objdump location: " + ex.message) + logger.error("Bugsnag: Error attempting to calculate objdump location: " + ex.message) } return null } private fun getObjDumpOverride(arch: String): String? { - val bugsnag = project.extensions.getByType(BugsnagPluginExtension::class.java) - val paths = bugsnag.objdumpPaths - return paths.get()[arch] + return objDumpPaths.get()[arch] } companion object { @@ -264,5 +285,39 @@ open class BugsnagUploadNdkTask @Inject constructor( else -> null } } + + internal fun register( + project: Project, + name: String, + configurationAction: BugsnagUploadNdkTask.() -> Unit + ): TaskProvider { + val gradleVersion = project.gradle.versionNumber() + return when { + gradleVersion >= GradleVersions.VERSION_5_3 -> { + project.tasks.register(name, configurationAction) + } + else -> { + project.tasks.register(name, configurationAction) + } + } + } } } + +/** A legacy [BugsnagUploadNdkTask] that uses [ProjectLayout.configurableFiles]. */ +internal open class BugsnagUploadNdkTaskLegacy @Inject constructor( + objects: ObjectFactory, + projectLayout: ProjectLayout +) : BugsnagUploadNdkTask(objects, projectLayout) { + @Suppress("DEPRECATION") // Here for backward compat + @get:InputFiles + override val searchDirectories: ConfigurableFileCollection = projectLayout.configurableFiles() +} + +internal open class BugsnagUploadNdkTask53Plus @Inject constructor( + objects: ObjectFactory, + projectLayout: ProjectLayout +) : BugsnagUploadNdkTask(objects, projectLayout) { + @get:InputFiles + override val searchDirectories: ConfigurableFileCollection = objects.fileCollection() +} diff --git a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagUploadProguardTask.kt b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagUploadProguardTask.kt index 7838bad6..02c6d780 100644 --- a/src/main/kotlin/com.bugsnag.android.gradle/BugsnagUploadProguardTask.kt +++ b/src/main/kotlin/com.bugsnag.android.gradle/BugsnagUploadProguardTask.kt @@ -1,6 +1,14 @@ package com.bugsnag.android.gradle +import com.bugsnag.android.gradle.internal.BugsnagHttpClientHelper +import com.bugsnag.android.gradle.internal.UploadRequestClient +import com.bugsnag.android.gradle.internal.md5HashCode +import com.bugsnag.android.gradle.internal.property import okhttp3.RequestBody +import okio.HashingSource +import okio.blackholeSink +import okio.buffer +import okio.source import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory @@ -37,7 +45,10 @@ open class BugsnagUploadProguardTask @Inject constructor( } @get:Internal - internal val uploadRequestClient: Property = objects.property(UploadRequestClient::class.java) + internal val uploadRequestClient: Property = objects.property() + + @get:Internal + override val httpClientHelper: Property = objects.property() @get:PathSensitive(NONE) @get:InputFile @@ -51,31 +62,30 @@ open class BugsnagUploadProguardTask @Inject constructor( val requestOutputFile: RegularFileProperty = objects.fileProperty() @get:Input - override val failOnUploadError: Property = objects.property(Boolean::class.javaObjectType) + override val failOnUploadError: Property = objects.property() @get:Input - override val overwrite: Property = objects.property(Boolean::class.javaObjectType) + override val overwrite: Property = objects.property() @get:Input - override val endpoint: Property = objects.property(String::class.javaObjectType) + override val endpoint: Property = objects.property() @get:Input - override val retryCount: Property = objects.property(Int::class.javaObjectType) + override val retryCount: Property = objects.property() @get:Input - override val timeoutMillis: Property = objects.property(Long::class.javaObjectType) + override val timeoutMillis: Property = objects.property() @TaskAction fun upload() { val mappingFile = mappingFileProperty.asFile.get() if (mappingFile.length() == 0L) { // proguard's -dontobfuscate generates an empty mapping file - project.logger.warn("Bugsnag: Ignoring empty proguard file") + logger.warn("Bugsnag: Ignoring empty proguard file") return } if (!mappingFile.exists()) { logger.warn("Bugsnag: Mapping file not found: $mappingFile") - val bugsnag = project.extensions.findByType(BugsnagPluginExtension::class.java)!! - if (bugsnag.failOnUploadError.get()) { + if (failOnUploadError.get()) { throw IllegalStateException("Mapping file not found: $mappingFile") } } @@ -89,8 +99,8 @@ open class BugsnagUploadProguardTask @Inject constructor( // Send the request val request = BugsnagMultiPartUploadRequest.from(this) val manifestInfo = parseManifestInfo() - val mappingFileContents = mappingFile.readText() - val response = uploadRequestClient.get().makeRequestIfNeeded(manifestInfo, mappingFileContents) { + val mappingFileHash = mappingFile.md5HashCode() + val response = uploadRequestClient.get().makeRequestIfNeeded(manifestInfo, mappingFileHash) { logger.lifecycle("Bugsnag: Attempting to upload JVM mapping file: $mappingFile") request.uploadMultipartEntity(parts, manifestInfo) } diff --git a/src/main/kotlin/com.bugsnag.android.gradle/SharedObjectMappingFileProvider.kt b/src/main/kotlin/com.bugsnag.android.gradle/SharedObjectMappingFileProvider.kt index 5e284ebb..16288098 100644 --- a/src/main/kotlin/com.bugsnag.android.gradle/SharedObjectMappingFileProvider.kt +++ b/src/main/kotlin/com.bugsnag.android.gradle/SharedObjectMappingFileProvider.kt @@ -35,7 +35,7 @@ fun findSharedObjectMappingFiles(project: Project, private fun findSymbolPath(variantOutput: ApkVariantOutput?): File { val resources = variantOutput!!.processResourcesProvider.get() - return resources.property("textSymbolOutputFile") as File + return resources.property("textSymbolOutputFile") as? File ?: throw IllegalStateException("Could not find symbol path") } diff --git a/src/main/kotlin/com.bugsnag.android.gradle/UploadRequestClient.kt b/src/main/kotlin/com.bugsnag.android.gradle/UploadRequestClient.kt deleted file mode 100644 index 342e9091..00000000 --- a/src/main/kotlin/com.bugsnag.android.gradle/UploadRequestClient.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.bugsnag.android.gradle - -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.FutureTask - -class UploadRequestClient { - - private val requestMap = ConcurrentHashMap>() - - /** - * Executes request with automatic de-duplication of similar requests. - * - * If the version information and payload match another request that - * has already been enqueued, the response value of the existing request - * will be returned. If no existing requests match, a new request will - * be enqueued and executed. - * - * Equality is measured by the [AndroidManifestInfo] (excluding buildUuid) - * and the request payload. - */ - fun makeRequestIfNeeded(manifestInfo: AndroidManifestInfo, - payload: String, - request: () -> String): String { - val versionInfoHash = manifestInfo.hashCode() - val requestIdHash = versionInfoHash + payload.hashCode() - - val future = requestMap.getOrPut(requestIdHash.toString()) { - FutureTask { request() } - } - future.run() - return future.get() - } -} diff --git a/src/main/kotlin/com/bugsnag/android/gradle/internal/BugsnagHttpClientHelper.kt b/src/main/kotlin/com/bugsnag/android/gradle/internal/BugsnagHttpClientHelper.kt new file mode 100644 index 00000000..323ad3f9 --- /dev/null +++ b/src/main/kotlin/com/bugsnag/android/gradle/internal/BugsnagHttpClientHelper.kt @@ -0,0 +1,45 @@ +package com.bugsnag.android.gradle.internal + +import com.bugsnag.android.gradle.internal.BuildServiceBugsnagHttpClientHelper.Params +import okhttp3.OkHttpClient +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters +import java.time.Duration + +/** A simple API for providing a shared [OkHttpClient] instance for shared use in upload tasks. */ +interface BugsnagHttpClientHelper : AutoCloseable { + val okHttpClient: OkHttpClient + + override fun close() { + okHttpClient.dispatcher.executorService.shutdown() + okHttpClient.connectionPool.evictAll() + okHttpClient.cache?.close() + } +} + +/** A [BuildService] implementation of [BugsnagHttpClientHelper]. */ +abstract class BuildServiceBugsnagHttpClientHelper + : BuildService, BugsnagHttpClientHelper { + + interface Params : BuildServiceParameters { + val timeoutMillis: Property + } + + override val okHttpClient: OkHttpClient by lazy { newClient(parameters.timeoutMillis.get()) } +} + +/** A simple instance-based [BugsnagHttpClientHelper] for use on Gradle <6.1. */ +class LegacyBugsnagHttpClientHelper(timeoutMillis: Provider) : BugsnagHttpClientHelper { + override val okHttpClient: OkHttpClient by lazy { newClient(timeoutMillis.get()) } +} + +private fun newClient(timeoutMillis: Long): OkHttpClient { + val timeoutDuration = Duration.ofMillis(timeoutMillis) + return OkHttpClient.Builder() + .connectTimeout(timeoutDuration) + .callTimeout(timeoutDuration) + .build() +} + diff --git a/src/main/kotlin/com/bugsnag/android/gradle/internal/GradleUtil.kt b/src/main/kotlin/com/bugsnag/android/gradle/internal/GradleUtil.kt new file mode 100644 index 00000000..d0d59136 --- /dev/null +++ b/src/main/kotlin/com/bugsnag/android/gradle/internal/GradleUtil.kt @@ -0,0 +1,92 @@ +@file:Suppress("MatchingDeclarationName") // This file contains multiple top-level members +package com.bugsnag.android.gradle.internal + +import okio.HashingSource +import okio.blackholeSink +import okio.buffer +import okio.source +import org.gradle.api.Task +import org.gradle.api.invocation.Gradle +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.TaskContainer +import org.gradle.api.tasks.TaskProvider +import org.gradle.util.VersionNumber +import java.io.File + +internal object GradleVersions { + val VERSION_5_3: VersionNumber = VersionNumber.parse("5.3") + val VERSION_6: VersionNumber = VersionNumber.parse("6.0") + val VERSION_6_1: VersionNumber = VersionNumber.parse("6.1") + val VERSION_6_6: VersionNumber = VersionNumber.parse("6.6") +} + +internal fun Gradle.versionNumber(): VersionNumber = VersionNumber.parse(gradleVersion) + +/** A fast file hash that don't load the entire file contents into memory at once. */ +internal fun File.md5HashCode(): Int { + return HashingSource.md5(source()).use { hashingSource -> + hashingSource.buffer().readAll(blackholeSink()) + hashingSource.hash.hashCode() + } +} + +/* Borrowed helper functions from the Gradle Kotlin DSL. */ + +/** + * Create a new instance of [T], using [parameters] as the construction parameters. + * + * @param T The type of object to create + * @param parameters The construction parameters + * @return the created named object + * + * @see [ObjectFactory.newInstance] + */ +@Suppress("SpreadOperator") +inline fun ObjectFactory.newInstance(vararg parameters: Any): T = + newInstance(T::class.javaObjectType, *parameters) + +/** + * Creates a [Property] that holds values of the given type [T]. + * + * @see [ObjectFactory.property] + */ +internal inline fun ObjectFactory.property(): Property = + property(T::class.javaObjectType) + +/** + * Creates a [SetProperty] that holds values of the given type [T]. + * + * @see [ObjectFactory.setProperty] + */ +internal inline fun ObjectFactory.setProperty(): SetProperty = + setProperty(T::class.javaObjectType) + +/** + * Creates a [ListProperty] that holds values of the given type [T]. + * + * @see [ObjectFactory.listProperty] + */ +internal inline fun ObjectFactory.listProperty(): ListProperty = + listProperty(T::class.javaObjectType) + +/** + * Creates a [MapProperty] that holds values of the given key type [K] and value type [V]. + * + * @see [ObjectFactory.mapProperty] + */ +internal inline fun ObjectFactory.mapProperty(): MapProperty = + mapProperty(K::class.javaObjectType, V::class.javaObjectType) + +/** + * Defines and configure a new task, which will be created when it is required. + * + * @see [TaskContainer.register] + */ +internal inline fun TaskContainer.register( + name: String, + noinline configuration: T.() -> Unit +): TaskProvider = register(name, T::class.javaObjectType, configuration) diff --git a/src/main/kotlin/com/bugsnag/android/gradle/internal/UploadRequestClient.kt b/src/main/kotlin/com/bugsnag/android/gradle/internal/UploadRequestClient.kt new file mode 100644 index 00000000..96d1c1c8 --- /dev/null +++ b/src/main/kotlin/com/bugsnag/android/gradle/internal/UploadRequestClient.kt @@ -0,0 +1,67 @@ +package com.bugsnag.android.gradle.internal + +import com.bugsnag.android.gradle.AndroidManifestInfo +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters +import java.util.Objects +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.FutureTask + +sealed class UploadRequestClient : AutoCloseable { + + private val requestMap = ConcurrentHashMap>() + + /** + * Executes request with automatic de-duplication of similar requests. + * + * If the version information and payload match another request that + * has already been enqueued, the response value of the existing request + * will be returned. If no existing requests match, a new request will + * be enqueued and executed. + * + * Equality is measured by the [AndroidManifestInfo] + * and the request payload. + */ + fun makeRequestIfNeeded( + manifestInfo: AndroidManifestInfo, + payloadHash: Int, + request: () -> String + ): String { + val versionInfoHash = manifestInfo.hashCode() + val requestIdHash = Objects.hash(versionInfoHash, payloadHash) + + val future = requestMap.getOrPut(requestIdHash.toString()) { + FutureTask { request() } + } + future.run() + return future.get() + } + + override fun close() { + requestMap.forEach { (_, future) -> + future.cancel(true) + } + } +} + +/** A [BuildService]-based implementation of [UploadRequestClient]. */ +abstract class BuildServiceUploadRequestClient : UploadRequestClient(), BuildService + +/** A simple [UploadRequestClient] for use on Gradle <6.1 */ +class LegacyUploadRequestClient : UploadRequestClient() + +internal fun newUploadRequestClientProvider(project: Project, prefix: String): Provider { + return if (project.gradle.versionNumber() >= GradleVersions.VERSION_6_1) { + project.gradle.sharedServices.registerIfAbsent("bugsnag${prefix.capitalize()}UploadRequestClient", + BuildServiceUploadRequestClient::class.java + ) { + // No parameters! + } + } else { + // Reuse this single instance every time it's provided + val provider = LegacyUploadRequestClient() + project.provider { provider } + } +} diff --git a/src/test/kotlin/com/bugsnag/android/gradle/UploadRequestClientTest.kt b/src/test/kotlin/com/bugsnag/android/gradle/UploadRequestClientTest.kt index b7a3ad73..2bbc504f 100644 --- a/src/test/kotlin/com/bugsnag/android/gradle/UploadRequestClientTest.kt +++ b/src/test/kotlin/com/bugsnag/android/gradle/UploadRequestClientTest.kt @@ -1,5 +1,6 @@ package com.bugsnag.android.gradle +import com.bugsnag.android.gradle.internal.LegacyUploadRequestClient import org.junit.Assert.assertEquals import org.junit.Test @@ -15,40 +16,40 @@ class UploadRequestClientTest { @Test fun testRequestDiffVersionInfo() { - val client = UploadRequestClient() + val client = LegacyUploadRequestClient() var requestCount = 0 val request = { requestCount += 1 "" } - client.makeRequestIfNeeded(info, "{}", request) - client.makeRequestIfNeeded(info.copy(apiKey = "40fadb0123094f"), "{}", request) + client.makeRequestIfNeeded(info, "{}".hashCode(), request) + client.makeRequestIfNeeded(info.copy(apiKey = "40fadb0123094f"), "{}".hashCode(), request) assertEquals(2, requestCount) } @Test fun testRequestDiffPayload() { - val client = UploadRequestClient() + val client = LegacyUploadRequestClient() var requestCount = 0 val request = { requestCount += 1 "" } - client.makeRequestIfNeeded(info, "{}", request) - client.makeRequestIfNeeded(info, "[]", request) + client.makeRequestIfNeeded(info, "{}".hashCode(), request) + client.makeRequestIfNeeded(info, "[]".hashCode(), request) assertEquals(2, requestCount) } @Test fun testRequestSameInfo() { - val client = UploadRequestClient() + val client = LegacyUploadRequestClient() var requestCount = 0 val request = { requestCount += 1 "" } - client.makeRequestIfNeeded(info, "{}", request) - client.makeRequestIfNeeded(info, "{}", request) + client.makeRequestIfNeeded(info, "{}".hashCode(), request) + client.makeRequestIfNeeded(info, "{}".hashCode(), request) assertEquals(1, requestCount) } }