From cc6f70dd70087bac1e3630742f2d6e6c362fdf7a Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 11 Aug 2022 16:50:41 +0100 Subject: [PATCH 01/12] feat(ndk): use `objcopy` instead of `objdump` on to produce symbols files on newer NDK versions --- detekt-baseline.xml | 5 +- .../gradle/BugsnagGenerateNdkSoMappingTask.kt | 54 ++---- .../BugsnagGenerateUnitySoMappingTask.kt | 63 +++---- .../bugsnag/android/gradle/BugsnagPlugin.kt | 7 + .../gradle/SharedObjectMappingFileFactory.kt | 156 ------------------ .../gradle/internal/AbstractSoMappingTask.kt | 96 +++++++++++ .../android/gradle/internal/NdkToolchain.kt | 66 ++++++++ .../android/gradle/ObjDumpLocationTest.kt | 52 ------ 8 files changed, 211 insertions(+), 288 deletions(-) delete mode 100644 src/main/kotlin/com/bugsnag/android/gradle/SharedObjectMappingFileFactory.kt create mode 100644 src/main/kotlin/com/bugsnag/android/gradle/internal/AbstractSoMappingTask.kt create mode 100644 src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt delete mode 100644 src/test/kotlin/com/bugsnag/android/gradle/ObjDumpLocationTest.kt diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 44c65c55..c75a0b6f 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -8,18 +8,21 @@ MagicNumber:BugsnagPluginExtension.kt$BugsnagPluginExtension$60000 MagicNumber:BugsnagReleasesTask.kt$BugsnagReleasesTask$200 MagicNumber:MappingFileProvider.kt$9 + MagicNumber:NdkToolchain.kt$NdkToolchain$23 ReturnCount:BugsnagPlugin.kt$BugsnagPlugin$ @Suppress("SENSELESS_COMPARISON") internal fun isUnityLibraryUploadEnabled( bugsnag: BugsnagPluginExtension, android: BaseExtension ): Boolean ReturnCount:BugsnagPlugin.kt$BugsnagPlugin$ private fun registerUploadSourceMapTask( project: Project, variant: BaseVariant, output: BaseVariantOutput, bugsnag: BugsnagPluginExtension, manifestInfoProvider: Provider<RegularFile> ): TaskProvider<out BugsnagUploadJsSourceMapTask>? ReturnCount:ManifestUuidTaskV2Compat.kt$internal fun createManifestUpdateTask( bugsnag: BugsnagPluginExtension, project: Project, variantName: String, variantOutput: VariantOutput ): TaskProvider<BugsnagManifestUuidTask>? ReturnCount:SharedObjectMappingFileFactory.kt$SharedObjectMappingFileFactory$ fun generateSoMappingFile(project: Project, params: Params): File? SpreadOperator:DexguardCompat.kt$(buildDir, *path, variant.dirName, outputDir, "mapping.txt") + SwallowedException:NdkToolchain.kt$catch (e: Exception) { return@provider extensions.getByType(BaseExtension::class.java).ndkDirectory.absoluteFile } + TooGenericExceptionCaught:AbstractSoMappingTask.kt$AbstractSoMappingTask$e: Exception TooGenericExceptionCaught:BugsnagHttpClientHelper.kt$exc: Throwable TooGenericExceptionCaught:BugsnagMultiPartUploadRequest.kt$BugsnagMultiPartUploadRequest$exc: Throwable TooGenericExceptionCaught:BugsnagReleasesTask.kt$BugsnagReleasesTask$exc: Throwable TooGenericExceptionCaught:MappingFileProvider.kt$exc: Throwable + TooGenericExceptionCaught:NdkToolchain.kt$e: Exception TooGenericExceptionCaught:SharedObjectMappingFileFactory.kt$SharedObjectMappingFileFactory$e: Exception TooGenericExceptionCaught:SharedObjectMappingFileFactory.kt$SharedObjectMappingFileFactory$ex: Throwable TooManyFunctions:BugsnagPlugin.kt$BugsnagPlugin : Plugin - UnusedPrivateMember:BugsnagPlugin.kt$BugsnagPlugin$private fun BaseVariantOutput.findVersionCode(): Int diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateNdkSoMappingTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateNdkSoMappingTask.kt index 6f50c22a..bae36888 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateNdkSoMappingTask.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateNdkSoMappingTask.kt @@ -3,28 +3,21 @@ package com.bugsnag.android.gradle import com.android.build.VariantOutput import com.android.build.gradle.api.ApkVariantOutput import com.android.build.gradle.api.BaseVariant +import com.bugsnag.android.gradle.internal.AbstractSoMappingTask import com.bugsnag.android.gradle.internal.ExternalNativeBuildTaskUtil import com.bugsnag.android.gradle.internal.VariantTaskCompanion import com.bugsnag.android.gradle.internal.clearDir -import com.bugsnag.android.gradle.internal.dependsOn -import com.bugsnag.android.gradle.internal.forBuildOutput -import com.bugsnag.android.gradle.internal.mapProperty +import com.bugsnag.android.gradle.internal.ndkToolchain import com.bugsnag.android.gradle.internal.property import com.bugsnag.android.gradle.internal.register -import org.gradle.api.DefaultTask import org.gradle.api.Project import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.file.DirectoryProperty -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.provider.Provider import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Optional -import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import java.io.File import javax.inject.Inject @@ -32,43 +25,32 @@ import javax.inject.Inject /** * Task that generates NDK shared object mapping files for upload to Bugsnag. */ -open class BugsnagGenerateNdkSoMappingTask @Inject constructor( +internal abstract class BugsnagGenerateNdkSoMappingTask @Inject constructor( objects: ObjectFactory -) : DefaultTask(), AndroidManifestInfoReceiver { +) : AbstractSoMappingTask(objects) { init { group = BugsnagPlugin.GROUP_NAME description = "Generates NDK mapping files for upload to Bugsnag" } - @get:InputFile - override val manifestInfo: RegularFileProperty = objects.fileProperty() - @get:Input @get:Optional val abi: Property = objects.property() - @get:OutputDirectory - val intermediateOutputDir: DirectoryProperty = objects.directoryProperty() - - @get:Input - val objDumpPaths: MapProperty = objects.mapProperty() - @get:InputFiles val searchDirectories: ConfigurableFileCollection = objects.fileCollection() @TaskAction fun generateMappingFiles() { logger.lifecycle("Generating NDK mapping files") - val searchDirs = searchDirectories.files.toList() - val files = findSharedObjectMappingFiles(searchDirs) + val files = findSharedObjectMappingFiles() processFiles(files) } - private fun findSharedObjectMappingFiles( - searchDirectories: List - ): Collection { - return searchDirectories.flatMap(this::findSharedObjectFiles) + private fun findSharedObjectMappingFiles(): Collection { + return searchDirectories + .flatMap(this::findSharedObjectFiles) .toSortedSet(compareBy { it.absolutePath }) } @@ -96,18 +78,12 @@ open class BugsnagGenerateNdkSoMappingTask @Inject constructor( private fun processFiles(files: Collection) { logger.info("Bugsnag: Found shared object files for upload: $files") - val outputDir = intermediateOutputDir.get().asFile - outputDir.clearDir() + outputDirectory.get().asFile.clearDir() files.forEach { sharedObjectFile -> val arch = sharedObjectFile.parentFile.name - val params = SharedObjectMappingFileFactory.Params( - sharedObjectFile, - requireNotNull(Abi.findByName(arch)), - objDumpPaths.get(), - outputDir - ) - val outputFile = SharedObjectMappingFileFactory.generateSoMappingFile(project, params) + val abi = requireNotNull(Abi.findByName(arch)) { "unknown abi: $arch" } + val outputFile = generateMappingFile(sharedObjectFile, abi) if (outputFile != null) { logger.info("Bugsnag: Created symbol file for $arch at $outputFile") } @@ -131,8 +107,8 @@ open class BugsnagGenerateNdkSoMappingTask @Inject constructor( soMappingOutputPath: String ) = register(project, output) { abi.set(output.getFilter(VariantOutput.FilterType.ABI)) - objDumpPaths.set(objdumpPaths) - manifestInfo.set(BugsnagManifestUuidTask.manifestInfoForOutput(project, output)) + ndkDirectory.set(project.ndkToolchain) + objDumpOverrides.set(objdumpPaths) val externalNativeBuildTaskUtil = ExternalNativeBuildTaskUtil(project.providers) @@ -140,8 +116,8 @@ open class BugsnagGenerateNdkSoMappingTask @Inject constructor( variant.externalNativeBuildProviders.forEach { provider -> searchDirectories.from(externalNativeBuildTaskUtil.findSearchPaths(provider)) } - intermediateOutputDir.set(project.layout.buildDirectory.dir(soMappingOutputPath)) - }.dependsOn(BugsnagManifestUuidTask.forBuildOutput(project, output)) + outputDirectory.set(project.layout.buildDirectory.dir(soMappingOutputPath)) + } override fun taskNameFor(variantOutputName: String) = "generateBugsnagNdk${variantOutputName.capitalize()}Mapping" diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateUnitySoMappingTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateUnitySoMappingTask.kt index 3de5ff88..41d3b787 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateUnitySoMappingTask.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateUnitySoMappingTask.kt @@ -1,26 +1,20 @@ package com.bugsnag.android.gradle import com.android.build.gradle.api.ApkVariantOutput +import com.bugsnag.android.gradle.internal.AbstractSoMappingTask import com.bugsnag.android.gradle.internal.VariantTaskCompanion import com.bugsnag.android.gradle.internal.clearDir -import com.bugsnag.android.gradle.internal.dependsOn -import com.bugsnag.android.gradle.internal.forBuildOutput import com.bugsnag.android.gradle.internal.includesAbi -import com.bugsnag.android.gradle.internal.mapProperty +import com.bugsnag.android.gradle.internal.ndkToolchain import com.bugsnag.android.gradle.internal.register import okio.BufferedSource import okio.buffer import okio.sink import okio.source -import org.gradle.api.DefaultTask import org.gradle.api.Project import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Provider -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.Internal import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction @@ -31,27 +25,18 @@ import javax.inject.Inject /** * Task that generates Unity shared object mapping files for upload to Bugsnag. */ -internal open class BugsnagGenerateUnitySoMappingTask @Inject constructor( +internal abstract class BugsnagGenerateUnitySoMappingTask @Inject constructor( objects: ObjectFactory -) : DefaultTask(), AndroidManifestInfoReceiver { +) : AbstractSoMappingTask(objects) { init { group = BugsnagPlugin.GROUP_NAME description = "Generates Unity mapping files for upload to Bugsnag" } - @get:InputFile - override val manifestInfo: RegularFileProperty = objects.fileProperty() - @get:Internal internal lateinit var variantOutput: ApkVariantOutput - @get:Input - val objDumpPaths: MapProperty = objects.mapProperty() - - @get:OutputDirectory - val intermediateOutputDir: DirectoryProperty = objects.directoryProperty() - @get:OutputDirectory val unitySharedObjectDir: DirectoryProperty = objects.directoryProperty() @@ -64,7 +49,7 @@ internal open class BugsnagGenerateUnitySoMappingTask @Inject constructor( // search the internal Gradle build + exported Gradle build locations val symbolArchives = getUnitySymbolArchives(rootProjectDir) val copyDir = unitySharedObjectDir.asFile.get() - val outputDir = intermediateOutputDir.asFile.get() + val outputDir = outputDirectory.asFile.get() copyDir.clearDir() outputDir.clearDir() @@ -82,20 +67,30 @@ internal open class BugsnagGenerateUnitySoMappingTask @Inject constructor( return } - sharedObjectFiles.addAll(extractSoFilesFromGzipArchive(symbolArchives, copyDir)) + sharedObjectFiles.addAll(extractSoFilesFromZipArchive(symbolArchives, copyDir)) logger.info("Extracted Unity SO files: $sharedObjectFiles") // generate mapping files for each SO file sharedObjectFiles.forEach { sharedObjectFile -> - generateUnitySoMappingFile(sharedObjectFile) + val abi = Abi.findByName(sharedObjectFile.parentFile.name)!! + generateMappingFile(sharedObjectFile, abi) } } + override fun objdump(inputFile: File, abi: Abi): ProcessBuilder { + val objdump = ndkToolchain.objdumpForAbi(abi).path + return ProcessBuilder( + objdump, + "--sym", + inputFile.path + ) + } + /** - * Extracts the libunity/libil2cpp SO files from inside a GZIP archive, + * Extracts the libunity/libil2cpp SO files from inside a ZIP archive, * which is where the files are located for exported Gradle projects */ - private fun extractSoFilesFromGzipArchive(symbolArchives: List, copyDir: File): List { + private fun extractSoFilesFromZipArchive(symbolArchives: List, copyDir: File): List { copyDir.mkdirs() return symbolArchives.flatMap { archive -> val zipFile = ZipFile(archive) @@ -158,18 +153,6 @@ internal open class BugsnagGenerateUnitySoMappingTask @Inject constructor( return dst } - private fun generateUnitySoMappingFile(sharedObjectFile: File) { - val arch = sharedObjectFile.parentFile.name - val params = SharedObjectMappingFileFactory.Params( - sharedObjectFile, - requireNotNull(Abi.findByName(arch)), - objDumpPaths.get(), - intermediateOutputDir.get().asFile, - SharedObjectMappingFileFactory.SharedObjectType.UNITY - ) - SharedObjectMappingFileFactory.generateSoMappingFile(project, params) - } - /** * The directory below the exported symbols. When Unity exports a project to an Android Gradle project * the symbols are exported as an archive in the same directory. @@ -207,12 +190,12 @@ internal open class BugsnagGenerateUnitySoMappingTask @Inject constructor( copyOutputDir: String ) = register(project, output) { variantOutput = output - objDumpPaths.set(objdumpPaths) - manifestInfo.set(BugsnagManifestUuidTask.manifestInfoForOutput(project, output)) + ndkDirectory.set(project.ndkToolchain) + objDumpOverrides.set(objdumpPaths) rootProjectDir.set(project.rootProject.projectDir) - intermediateOutputDir.set(project.layout.buildDirectory.dir(mappingFileOutputDir)) + outputDirectory.set(project.layout.buildDirectory.dir(mappingFileOutputDir)) unitySharedObjectDir.set(project.layout.buildDirectory.dir(copyOutputDir)) - }.dependsOn(BugsnagManifestUuidTask.forBuildOutput(project, output)) + } internal fun isUnitySymbolsArchive(name: String, projectName: String): Boolean { return name.endsWith("symbols.zip") && name.startsWith(projectName) diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt index 94ac5e36..422f2dcb 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt @@ -220,6 +220,7 @@ class BugsnagPlugin : Plugin { bugsnag.failOnUploadError, mappingFilesProvider ) + else -> null } @@ -233,6 +234,7 @@ class BugsnagPlugin : Plugin { proguardUploadClientProvider, generateProguardTaskProvider ).dependsOn(manifestTaskProvider) + else -> null } val ndkSoMappingOutput = "$NDK_SO_MAPPING_DIR/${output.name}" @@ -246,6 +248,7 @@ class BugsnagPlugin : Plugin { getSharedObjectSearchPaths(project, bugsnag, android), ndkSoMappingOutput ) + else -> null } val uploadNdkMappingProvider = when { @@ -259,6 +262,7 @@ class BugsnagPlugin : Plugin { ndkSoMappingOutput ) } + else -> null } @@ -273,6 +277,7 @@ class BugsnagPlugin : Plugin { unityMappingDir, "$UNITY_SO_COPY_DIR/${output.name}" ) + else -> null } val uploadUnityMappingProvider = when { @@ -286,6 +291,7 @@ class BugsnagPlugin : Plugin { unityMappingDir ) } + else -> null } @@ -297,6 +303,7 @@ class BugsnagPlugin : Plugin { bugsnag, manifestInfoProvider )?.dependsOn(manifestTaskProvider) + else -> null } diff --git a/src/main/kotlin/com/bugsnag/android/gradle/SharedObjectMappingFileFactory.kt b/src/main/kotlin/com/bugsnag/android/gradle/SharedObjectMappingFileFactory.kt deleted file mode 100644 index 95ee6e39..00000000 --- a/src/main/kotlin/com/bugsnag/android/gradle/SharedObjectMappingFileFactory.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.bugsnag.android.gradle - -import com.android.build.gradle.BaseExtension -import com.bugsnag.android.gradle.SharedObjectMappingFileFactory.SharedObjectType.NDK -import com.bugsnag.android.gradle.SharedObjectMappingFileFactory.SharedObjectType.UNITY -import com.bugsnag.android.gradle.internal.outputZipFile -import org.apache.tools.ant.taskdefs.condition.Os -import org.gradle.api.Project -import java.io.File - -/** - * Generates a mapping file for the supplied shared object file. - * - * Currently this only supports NDK SO mapping files but in future this will support - * other platforms which require different SO mapping support. - */ -internal object SharedObjectMappingFileFactory { - - enum class SharedObjectType { - NDK, - UNITY - } - - internal data class Params( - val sharedObject: File, - val abi: Abi, - val objDumpPaths: Map, - val outputDirectory: File, - val sharedObjectType: SharedObjectType = NDK - ) - - /** - * Uses objdump to create a symbols file for the given shared object file. - * - * @param project the gradle project - * @param params the parameters required to generate a SO mapping file - * @return the output file location, or null on error - */ - fun generateSoMappingFile(project: Project, params: Params): File? { - // Get the path the version of objdump to use to get symbols - val arch = params.abi.abiName - val objDumpPath = getObjDumpExecutable(project, params.objDumpPaths, arch) - val logger = project.logger - - if (objDumpPath == null) { - logger.error("Bugsnag: Unable to upload NDK symbols: Could not find objdump location for $arch") - return null - } - - try { - val archDir = prepareArchDirectory(params, arch) - val sharedObjectName = params.sharedObject.name - val dst = File(archDir, "$sharedObjectName.gz") - val processBuilder = getObjDumpCommand(objDumpPath, params) - logger.info( - "Bugsnag: Creating symbol file for $sharedObjectName at $dst," + - "running ${processBuilder.command()}" - ) - makeSoMappingFile(dst, processBuilder) - return dst - } catch (e: Exception) { - logger.error("Bugsnag: failed to generate symbols for $arch ${e.message}", e) - } - return null - } - - /** - * Gets the command used to generate the SO mapping file with objdump. - * This differs for NDK and Unity SO files. - */ - private fun getObjDumpCommand(objDumpPath: File, params: Params): ProcessBuilder { - val soPath = params.sharedObject.path - val objdump = objDumpPath.path - return when (params.sharedObjectType) { - NDK -> ProcessBuilder(objdump, "--dwarf=info", "--dwarf=rawline", soPath) - UNITY -> ProcessBuilder(objdump, "--sym", soPath) - } - } - - private fun makeSoMappingFile(dst: File, processBuilder: ProcessBuilder) { - // ensure any errors are dumped to stderr - processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT) - val process = processBuilder.start() - outputZipFile(process.inputStream, dst) - - val exitCode = process.waitFor() - if (exitCode != 0) { - throw IllegalStateException( - "Failed to generate symbols for $dst," + - " objdump exited with code $exitCode" - ) - } - } - - private fun prepareArchDirectory(params: Params, arch: String): File { - val rootDir = params.outputDirectory - return File(rootDir, arch).apply { - mkdir() - } - } - - /** - * Gets the path to the objdump executable to use to get symbols from a shared object - * @param arch The arch of the shared object - * @return The objdump executable, or null if not found - */ - private fun getObjDumpExecutable(project: Project, objDumpPaths: Map, arch: String): File? { - try { - val override = getObjDumpOverride(objDumpPaths, arch) - val objDumpFile: File - objDumpFile = override?.let { File(it) } ?: findObjDump(project, arch) - check((objDumpFile.exists() && objDumpFile.canExecute())) { - "Failed to find executable objdump at $objDumpFile" - } - return objDumpFile - } catch (ex: Throwable) { - project.logger.error("Bugsnag: Error attempting to calculate objdump location: " + ex.message) - } - return null - } - - private fun getObjDumpOverride(objDumpPaths: Map, arch: String) = objDumpPaths[arch] - - private fun findObjDump(project: Project, arch: String): File { - val abi = Abi.findByName(arch) - val android = project.extensions.getByType(BaseExtension::class.java) - val ndkDir = android.ndkDirectory.absolutePath - val osName = calculateOsName() - checkNotNull(abi) { "Failed to find ABI for $arch" } - checkNotNull(osName) { "Failed to calculate OS name" } - return calculateObjDumpLocation(ndkDir, abi, osName) - } - - @JvmStatic - fun calculateObjDumpLocation(ndkDir: String?, abi: Abi, osName: String): File { - val executable = if (osName.startsWith("windows")) "objdump.exe" else "objdump" - return File( - "$ndkDir/toolchains/${abi.toolchainPrefix}-4.9/prebuilt/" + - "$osName/bin/${abi.objdumpPrefix}-$executable" - ) - } - - private fun calculateOsName(): String? { - return when { - Os.isFamily(Os.FAMILY_MAC) -> "darwin-x86_64" - Os.isFamily(Os.FAMILY_UNIX) -> "linux-x86_64" - Os.isFamily(Os.FAMILY_WINDOWS) -> { - when { - "x86" == System.getProperty("os.arch") -> "windows" - else -> "windows-x86_64" - } - } - else -> null - } - } -} diff --git a/src/main/kotlin/com/bugsnag/android/gradle/internal/AbstractSoMappingTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/internal/AbstractSoMappingTask.kt new file mode 100644 index 00000000..f3c4bf8b --- /dev/null +++ b/src/main/kotlin/com/bugsnag/android/gradle/internal/AbstractSoMappingTask.kt @@ -0,0 +1,96 @@ +package com.bugsnag.android.gradle.internal + +import com.bugsnag.android.gradle.Abi +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +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.Internal +import org.gradle.api.tasks.OutputDirectory +import java.io.File + +abstract class AbstractSoMappingTask(objects: ObjectFactory) : DefaultTask() { + + @get:Input + val forceLegacyMapping: Property = objects.property().convention(true) + + @get:Input + abstract val objDumpOverrides: MapProperty + + @get:Input + abstract val ndkDirectory: Property + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @get:Internal + protected val ndkToolchain by lazy { + NdkToolchain(ndkDirectory.get(), objDumpOverrides.get().mapKeys { Abi.findByName(it.key)!! }) + } + + protected open fun objcopy(inputFile: File, abi: Abi): ProcessBuilder { + return ProcessBuilder( + ndkToolchain.objcopy.path, + "--compress-debug-sections=zlib", + "--only-keep-debug", + inputFile.path, + "-" // output to stdout + ) + } + + protected open fun objdump(inputFile: File, abi: Abi): ProcessBuilder { + val objdump = ndkToolchain.objdumpForAbi(abi).path + return ProcessBuilder( + objdump, + "--dwarf=info", + "--dwarf=rawline", + inputFile.path + ) + } + + fun generateMappingFile(soFile: File, abi: Abi): File? { + try { + val process = + if (ndkToolchain.isLLVM() && !forceLegacyMapping.get()) objcopy(soFile, abi) + else objdump(soFile, abi) + + val dst = outputFileFor(soFile, abi) + makeSoMappingFile(dst, process) + + return dst + } catch (e: Exception) { + logger.error("Bugsnag: failed to generate symbols for $abi ${e.message}", e) + } + + return null + } + + private fun makeSoMappingFile(dst: File, processBuilder: ProcessBuilder) { + // ensure any errors are dumped to stderr + processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT) + val process = processBuilder.start() + outputZipFile(process.inputStream, dst) + + val exitCode = process.waitFor() + if (exitCode != 0) { + throw IllegalStateException( + "Failed to generate symbols for $dst, objdump exited with code $exitCode" + ) + } + } + + protected open fun outputFileFor(soFile: File, abi: Abi): File { + return File(prepareArchDirectory(abi), "${soFile.name}.gz") + } + + private fun prepareArchDirectory(abi: Abi): File { + val rootDir = outputDirectory.get().asFile + return File(rootDir, abi.abiName).apply { + if (!isDirectory) { + mkdir() + } + } + } +} diff --git a/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt b/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt new file mode 100644 index 00000000..c00adc17 --- /dev/null +++ b/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt @@ -0,0 +1,66 @@ +package com.bugsnag.android.gradle.internal + +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.gradle.BaseExtension +import com.bugsnag.android.gradle.Abi +import org.apache.tools.ant.taskdefs.condition.Os +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.gradle.util.VersionNumber +import java.io.File + +/* + * SdkComponents.ndkDirectory + * https://developer.android.com/reference/tools/gradle-api/7.2/com/android/build/api/dsl/SdkComponents#ndkDirectory() + * sometimes fails to resolve when ndkPath is not defined (Cannot query the value of this property because it has + * no value available.). This means that even `map` and `isPresent` will break. + * + * So we also fall back use the old BaseExtension if it appears broken + */ +val Project.ndkToolchain: Provider + get() { + val sdkComponents = extensions.getByType(AndroidComponentsExtension::class.java)?.sdkComponents + + return provider { + try { + return@provider sdkComponents!!.ndkDirectory.get().asFile + } catch (e: Exception) { + return@provider extensions.getByType(BaseExtension::class.java).ndkDirectory.absoluteFile + } + } + } + +val osName = when { + Os.isFamily(Os.FAMILY_MAC) -> "darwin-x86_64" + Os.isFamily(Os.FAMILY_UNIX) -> "linux-x86_64" + Os.isFamily(Os.FAMILY_WINDOWS) -> { + when { + "x86" == System.getProperty("os.arch") -> "windows" + else -> "windows-x86_64" + } + } + + else -> null +} + +class NdkToolchain( + val baseDir: File, + private val objdumpOverrides: Map +) { + val version = VersionNumber.parse(baseDir.name) + val objcopy: File = File(baseDir, "toolchains/llvm/prebuilt/$osName/bin/${executableName("llvm-objcopy")}") + + fun isLLVM(): Boolean = version >= VersionNumber.version(23, 0) + + private fun executableName(cmdName: String): String { + return if (osName?.startsWith("windows") == true) "$cmdName.exe" else cmdName + } + + fun objdumpForAbi(abi: Abi): File { + return objdumpOverrides[abi]?.let { File(it) } ?: File( + baseDir, + "toolchains/${abi.toolchainPrefix}-4.9/prebuilt/" + + "$osName/bin/${abi.objdumpPrefix}-${executableName("objdump")}" + ) + } +} diff --git a/src/test/kotlin/com/bugsnag/android/gradle/ObjDumpLocationTest.kt b/src/test/kotlin/com/bugsnag/android/gradle/ObjDumpLocationTest.kt deleted file mode 100644 index 3873cd60..00000000 --- a/src/test/kotlin/com/bugsnag/android/gradle/ObjDumpLocationTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.bugsnag.android.gradle - -import com.bugsnag.android.gradle.SharedObjectMappingFileFactory.calculateObjDumpLocation -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import java.util.ArrayList - -@RunWith(Parameterized::class) -class ObjDumpLocationTest { - - @Parameterized.Parameter(0) - lateinit var abi: Abi - - @Parameterized.Parameter(1) - lateinit var osName: String - - @Parameterized.Parameter(2) - lateinit var ndkDir: String - - @Test - fun testDefaultObjDumpLocation() { - val file = calculateObjDumpLocation(ndkDir, abi, osName) - val exec = when { - osName.startsWith("windows") -> "objdump.exe" - else -> "objdump" - } - val expected = "$ndkDir/toolchains/${abi.toolchainPrefix}-4.9/prebuilt/$osName/bin/${abi.objdumpPrefix}-$exec" - assertEquals(expected, file.path) - } - - companion object { - @Parameterized.Parameters - @JvmStatic - fun inputs(): Collection> { - val inputs: MutableCollection> = ArrayList() - for (abi in Abi.values()) { - for (os in listOf("darwin-x86_64", "linux-x86_64", "windows", "windows-x86_64")) { - for (ndkDir in listOf("/Users/bob/Library/Android/sdk/ndk-bundle", "/etc/ndk-bundle")) { - inputs.add(listOf(abi, os, ndkDir).toTypedArray()) - } - } - } - return inputs - } - - @Parameterized.Parameters - @JvmStatic - fun os(): Collection = listOf("windows", "linux") - } -} From 877d5c19df830481fedb3804173d4bc46d8c22f2 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 16 Aug 2022 08:28:40 +0100 Subject: [PATCH 02/12] refactor(ndk): made `NdkToolchain` a @Nested collection of properties instead of having to pass each property separately --- detekt-baseline.xml | 12 +- .../gradle/BugsnagGenerateNdkSoMappingTask.kt | 10 +- .../BugsnagGenerateUnitySoMappingTask.kt | 12 +- .../bugsnag/android/gradle/BugsnagPlugin.kt | 9 +- .../android/gradle/BugsnagPluginExtension.kt | 2 + .../gradle/internal/AbstractSoMappingTask.kt | 33 ++-- .../android/gradle/internal/NdkToolchain.kt | 154 ++++++++++++++---- 7 files changed, 158 insertions(+), 74 deletions(-) diff --git a/detekt-baseline.xml b/detekt-baseline.xml index c75a0b6f..335e076c 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -4,25 +4,25 @@ ComplexCondition:AndroidManifestParser.kt$AndroidManifestParser$apiKey == null || "" == apiKey || versionCode == null || buildUuid == null || versionName == null || applicationId == null ComplexCondition:BugsnagPlugin.kt$BugsnagPlugin$!jvmMinificationEnabled && !ndkEnabled && !unityEnabled && !reactNativeEnabled - LongParameterList:BugsnagGenerateNdkSoMappingTask.kt$BugsnagGenerateNdkSoMappingTask.Companion$( project: Project, variant: BaseVariant, output: ApkVariantOutput, objdumpPaths: Provider<Map<String, String>>, searchPaths: List<File>, soMappingOutputPath: String ) + LongParameterList:BugsnagGenerateNdkSoMappingTask.kt$BugsnagGenerateNdkSoMappingTask.Companion$( project: Project, variant: BaseVariant, output: ApkVariantOutput, ndk: NdkToolchain, searchPaths: List<File>, soMappingOutputPath: String ) MagicNumber:BugsnagPluginExtension.kt$BugsnagPluginExtension$60000 MagicNumber:BugsnagReleasesTask.kt$BugsnagReleasesTask$200 MagicNumber:MappingFileProvider.kt$9 MagicNumber:NdkToolchain.kt$NdkToolchain$23 + MagicNumber:NdkToolchain.kt$NdkToolchain.Companion$26 + MagicNumber:NdkToolchain.kt$NdkToolchain.Companion$5 + MaxLineLength:NdkToolchain.kt$NdkToolchain.Companion$/* * SdkComponents.ndkDirectory * https://developer.android.com/reference/tools/gradle-api/7.2/com/android/build/api/dsl/SdkComponents#ndkDirectory() * sometimes fails to resolve when ndkPath is not defined (Cannot query the value of this property because it has * no value available.). This means that even `map` and `isPresent` will break. * * So we also fall back use the old BaseExtension if it appears broken */ ReturnCount:BugsnagPlugin.kt$BugsnagPlugin$ @Suppress("SENSELESS_COMPARISON") internal fun isUnityLibraryUploadEnabled( bugsnag: BugsnagPluginExtension, android: BaseExtension ): Boolean ReturnCount:BugsnagPlugin.kt$BugsnagPlugin$ private fun registerUploadSourceMapTask( project: Project, variant: BaseVariant, output: BaseVariantOutput, bugsnag: BugsnagPluginExtension, manifestInfoProvider: Provider<RegularFile> ): TaskProvider<out BugsnagUploadJsSourceMapTask>? ReturnCount:ManifestUuidTaskV2Compat.kt$internal fun createManifestUpdateTask( bugsnag: BugsnagPluginExtension, project: Project, variantName: String, variantOutput: VariantOutput ): TaskProvider<BugsnagManifestUuidTask>? - ReturnCount:SharedObjectMappingFileFactory.kt$SharedObjectMappingFileFactory$ fun generateSoMappingFile(project: Project, params: Params): File? SpreadOperator:DexguardCompat.kt$(buildDir, *path, variant.dirName, outputDir, "mapping.txt") - SwallowedException:NdkToolchain.kt$catch (e: Exception) { return@provider extensions.getByType(BaseExtension::class.java).ndkDirectory.absoluteFile } + SwallowedException:NdkToolchain.kt$NdkToolchain.Companion$catch (e: Exception) { return@provider extensions.getByType(BaseExtension::class.java).ndkDirectory.absoluteFile } TooGenericExceptionCaught:AbstractSoMappingTask.kt$AbstractSoMappingTask$e: Exception TooGenericExceptionCaught:BugsnagHttpClientHelper.kt$exc: Throwable TooGenericExceptionCaught:BugsnagMultiPartUploadRequest.kt$BugsnagMultiPartUploadRequest$exc: Throwable TooGenericExceptionCaught:BugsnagReleasesTask.kt$BugsnagReleasesTask$exc: Throwable TooGenericExceptionCaught:MappingFileProvider.kt$exc: Throwable - TooGenericExceptionCaught:NdkToolchain.kt$e: Exception - TooGenericExceptionCaught:SharedObjectMappingFileFactory.kt$SharedObjectMappingFileFactory$e: Exception - TooGenericExceptionCaught:SharedObjectMappingFileFactory.kt$SharedObjectMappingFileFactory$ex: Throwable + TooGenericExceptionCaught:NdkToolchain.kt$NdkToolchain.Companion$e: Exception TooManyFunctions:BugsnagPlugin.kt$BugsnagPlugin : Plugin diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateNdkSoMappingTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateNdkSoMappingTask.kt index bae36888..48aacf33 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateNdkSoMappingTask.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateNdkSoMappingTask.kt @@ -5,16 +5,15 @@ import com.android.build.gradle.api.ApkVariantOutput import com.android.build.gradle.api.BaseVariant import com.bugsnag.android.gradle.internal.AbstractSoMappingTask import com.bugsnag.android.gradle.internal.ExternalNativeBuildTaskUtil +import com.bugsnag.android.gradle.internal.NdkToolchain import com.bugsnag.android.gradle.internal.VariantTaskCompanion import com.bugsnag.android.gradle.internal.clearDir -import com.bugsnag.android.gradle.internal.ndkToolchain import com.bugsnag.android.gradle.internal.property import com.bugsnag.android.gradle.internal.register import org.gradle.api.Project import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property -import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Optional @@ -27,7 +26,7 @@ import javax.inject.Inject */ internal abstract class BugsnagGenerateNdkSoMappingTask @Inject constructor( objects: ObjectFactory -) : AbstractSoMappingTask(objects) { +) : AbstractSoMappingTask() { init { group = BugsnagPlugin.GROUP_NAME @@ -102,13 +101,12 @@ internal abstract class BugsnagGenerateNdkSoMappingTask @Inject constructor( project: Project, variant: BaseVariant, output: ApkVariantOutput, - objdumpPaths: Provider>, + ndk: NdkToolchain, searchPaths: List, soMappingOutputPath: String ) = register(project, output) { abi.set(output.getFilter(VariantOutput.FilterType.ABI)) - ndkDirectory.set(project.ndkToolchain) - objDumpOverrides.set(objdumpPaths) + ndkToolchain.set(ndk) val externalNativeBuildTaskUtil = ExternalNativeBuildTaskUtil(project.providers) diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateUnitySoMappingTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateUnitySoMappingTask.kt index 41d3b787..573e1605 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateUnitySoMappingTask.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateUnitySoMappingTask.kt @@ -2,10 +2,10 @@ package com.bugsnag.android.gradle import com.android.build.gradle.api.ApkVariantOutput import com.bugsnag.android.gradle.internal.AbstractSoMappingTask +import com.bugsnag.android.gradle.internal.NdkToolchain import com.bugsnag.android.gradle.internal.VariantTaskCompanion import com.bugsnag.android.gradle.internal.clearDir import com.bugsnag.android.gradle.internal.includesAbi -import com.bugsnag.android.gradle.internal.ndkToolchain import com.bugsnag.android.gradle.internal.register import okio.BufferedSource import okio.buffer @@ -14,7 +14,6 @@ import okio.source import org.gradle.api.Project import org.gradle.api.file.DirectoryProperty import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.Provider import org.gradle.api.tasks.Internal import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction @@ -27,7 +26,7 @@ import javax.inject.Inject */ internal abstract class BugsnagGenerateUnitySoMappingTask @Inject constructor( objects: ObjectFactory -) : AbstractSoMappingTask(objects) { +) : AbstractSoMappingTask() { init { group = BugsnagPlugin.GROUP_NAME @@ -78,7 +77,7 @@ internal abstract class BugsnagGenerateUnitySoMappingTask @Inject constructor( } override fun objdump(inputFile: File, abi: Abi): ProcessBuilder { - val objdump = ndkToolchain.objdumpForAbi(abi).path + val objdump = ndkToolchain.get().objdumpForAbi(abi).path return ProcessBuilder( objdump, "--sym", @@ -185,13 +184,12 @@ internal abstract class BugsnagGenerateUnitySoMappingTask @Inject constructor( fun register( project: Project, output: ApkVariantOutput, - objdumpPaths: Provider>, + ndk: NdkToolchain, mappingFileOutputDir: String, copyOutputDir: String ) = register(project, output) { variantOutput = output - ndkDirectory.set(project.ndkToolchain) - objDumpOverrides.set(objdumpPaths) + ndkToolchain.set(ndk) rootProjectDir.set(project.rootProject.projectDir) outputDirectory.set(project.layout.buildDirectory.dir(mappingFileOutputDir)) unitySharedObjectDir.set(project.layout.buildDirectory.dir(copyOutputDir)) diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt index 422f2dcb..a98b375f 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt @@ -13,6 +13,7 @@ import com.bugsnag.android.gradle.internal.AgpVersions import com.bugsnag.android.gradle.internal.BugsnagHttpClientHelper import com.bugsnag.android.gradle.internal.ExternalNativeBuildTaskUtil import com.bugsnag.android.gradle.internal.NDK_SO_MAPPING_DIR +import com.bugsnag.android.gradle.internal.NdkToolchain import com.bugsnag.android.gradle.internal.TASK_JNI_LIBS import com.bugsnag.android.gradle.internal.UNITY_SO_COPY_DIR import com.bugsnag.android.gradle.internal.UNITY_SO_MAPPING_DIR @@ -192,6 +193,10 @@ class BugsnagPlugin : Plugin { ndkUploadClientProvider: Provider, unityUploadClientProvider: Provider ) { + val ndkToolchain by lazy(LazyThreadSafetyMode.NONE) { + NdkToolchain.configureNdkToolkit(project, bugsnag, variant) + } + variant.outputs.configureEach { output -> check(output is ApkVariantOutput) { "Expected variant output to be ApkVariantOutput but found ${output.javaClass}" @@ -244,7 +249,7 @@ class BugsnagPlugin : Plugin { project, variant, output, - bugsnag.objdumpPaths, + ndkToolchain, getSharedObjectSearchPaths(project, bugsnag, android), ndkSoMappingOutput ) @@ -273,7 +278,7 @@ class BugsnagPlugin : Plugin { BugsnagGenerateUnitySoMappingTask.register( project, output, - bugsnag.objdumpPaths, + ndkToolchain, unityMappingDir, "$UNITY_SO_COPY_DIR/${output.name}" ) diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPluginExtension.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPluginExtension.kt index 8b1606e6..55411860 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPluginExtension.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPluginExtension.kt @@ -85,6 +85,8 @@ open class BugsnagPluginExtension @Inject constructor(objects: ObjectFactory) { val objdumpPaths: MapProperty = objects.mapProperty() .convention(emptyMap()) + val useLegacyNdkSymbolUpload: Property = objects.property().convention(true) + // exposes sourceControl as a nested object on the extension, // see https://docs.gradle.org/current/userguide/custom_gradle_types.html#nested_objects diff --git a/src/main/kotlin/com/bugsnag/android/gradle/internal/AbstractSoMappingTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/internal/AbstractSoMappingTask.kt index f3c4bf8b..1a0e7dd4 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/internal/AbstractSoMappingTask.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/internal/AbstractSoMappingTask.kt @@ -3,36 +3,22 @@ package com.bugsnag.android.gradle.internal import com.bugsnag.android.gradle.Abi import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty -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.Internal +import org.gradle.api.tasks.Nested import org.gradle.api.tasks.OutputDirectory import java.io.File -abstract class AbstractSoMappingTask(objects: ObjectFactory) : DefaultTask() { +abstract class AbstractSoMappingTask : DefaultTask() { - @get:Input - val forceLegacyMapping: Property = objects.property().convention(true) - - @get:Input - abstract val objDumpOverrides: MapProperty - - @get:Input - abstract val ndkDirectory: Property + @get:Nested + abstract val ndkToolchain: Property @get:OutputDirectory abstract val outputDirectory: DirectoryProperty - @get:Internal - protected val ndkToolchain by lazy { - NdkToolchain(ndkDirectory.get(), objDumpOverrides.get().mapKeys { Abi.findByName(it.key)!! }) - } - protected open fun objcopy(inputFile: File, abi: Abi): ProcessBuilder { return ProcessBuilder( - ndkToolchain.objcopy.path, + ndkToolchain.get().objcopyForAbi(abi).path, "--compress-debug-sections=zlib", "--only-keep-debug", inputFile.path, @@ -41,7 +27,7 @@ abstract class AbstractSoMappingTask(objects: ObjectFactory) : DefaultTask() { } protected open fun objdump(inputFile: File, abi: Abi): ProcessBuilder { - val objdump = ndkToolchain.objdumpForAbi(abi).path + val objdump = ndkToolchain.get().objdumpForAbi(abi).path return ProcessBuilder( objdump, "--dwarf=info", @@ -52,9 +38,10 @@ abstract class AbstractSoMappingTask(objects: ObjectFactory) : DefaultTask() { fun generateMappingFile(soFile: File, abi: Abi): File? { try { - val process = - if (ndkToolchain.isLLVM() && !forceLegacyMapping.get()) objcopy(soFile, abi) - else objdump(soFile, abi) + val process = when (ndkToolchain.get().preferredMappingTool()) { + NdkToolchain.MappingTool.OBJCOPY -> objcopy(soFile, abi) + NdkToolchain.MappingTool.OBJDUMP -> objdump(soFile, abi) + } val dst = outputFileFor(soFile, abi) makeSoMappingFile(dst, process) diff --git a/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt b/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt index c00adc17..cc013a88 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt @@ -2,35 +2,20 @@ package com.bugsnag.android.gradle.internal import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.gradle.BaseExtension +import com.android.build.gradle.api.ApkVariant import com.bugsnag.android.gradle.Abi +import com.bugsnag.android.gradle.BugsnagPluginExtension import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.Project +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.StopExecutionException import org.gradle.util.VersionNumber import java.io.File -/* - * SdkComponents.ndkDirectory - * https://developer.android.com/reference/tools/gradle-api/7.2/com/android/build/api/dsl/SdkComponents#ndkDirectory() - * sometimes fails to resolve when ndkPath is not defined (Cannot query the value of this property because it has - * no value available.). This means that even `map` and `isPresent` will break. - * - * So we also fall back use the old BaseExtension if it appears broken - */ -val Project.ndkToolchain: Provider - get() { - val sdkComponents = extensions.getByType(AndroidComponentsExtension::class.java)?.sdkComponents - - return provider { - try { - return@provider sdkComponents!!.ndkDirectory.get().asFile - } catch (e: Exception) { - return@provider extensions.getByType(BaseExtension::class.java).ndkDirectory.absoluteFile - } - } - } - -val osName = when { +private val osName = when { Os.isFamily(Os.FAMILY_MAC) -> "darwin-x86_64" Os.isFamily(Os.FAMILY_UNIX) -> "linux-x86_64" Os.isFamily(Os.FAMILY_WINDOWS) -> { @@ -43,24 +28,133 @@ val osName = when { else -> null } -class NdkToolchain( - val baseDir: File, - private val objdumpOverrides: Map -) { - val version = VersionNumber.parse(baseDir.name) - val objcopy: File = File(baseDir, "toolchains/llvm/prebuilt/$osName/bin/${executableName("llvm-objcopy")}") +abstract class NdkToolchain { + @get:Input + abstract val baseDir: Property + + @get:Input + abstract val useLegacyNdkSymbolUpload: Property - fun isLLVM(): Boolean = version >= VersionNumber.version(23, 0) + @get:Input + abstract val overrides: MapProperty + + fun preferredMappingTool(): MappingTool { + // useLegacyNdkSymbolUpload force overrides any defaults or options + if (useLegacyNdkSymbolUpload.get()) { + return MappingTool.OBJDUMP + } + + val ndkVersion = version.get() + return when { + ndkVersion >= VersionNumber.version(23, 0) -> MappingTool.OBJCOPY + else -> MappingTool.OBJDUMP + } + } + + /** + * Set all the fields of this `NdkToolchain` based on the given [other] `NdkToolchain` + */ + fun configureWith(other: NdkToolchain) { + baseDir.set(other.baseDir) + overrides.set(other.overrides) + useLegacyNdkSymbolUpload.set(other.useLegacyNdkSymbolUpload) + } private fun executableName(cmdName: String): String { return if (osName?.startsWith("windows") == true) "$cmdName.exe" else cmdName } fun objdumpForAbi(abi: Abi): File { + val objdumpOverrides = overrides.get() + return objdumpOverrides[abi]?.let { File(it) } ?: File( - baseDir, + baseDir.get(), "toolchains/${abi.toolchainPrefix}-4.9/prebuilt/" + "$osName/bin/${abi.objdumpPrefix}-${executableName("objdump")}" ) } + + fun objcopyForAbi(abi: Abi): File { + val objdumpOverrides = overrides.get() + + return objdumpOverrides[abi]?.let { File(it) } ?: File( + baseDir.get(), + "toolchains/llvm/prebuilt/$osName/bin/${executableName("llvm-objcopy")}" + ) + } + + enum class MappingTool { + OBJDUMP, + OBJCOPY + } + + companion object { + /* + * SdkComponents.ndkDirectory + * https://developer.android.com/reference/tools/gradle-api/7.2/com/android/build/api/dsl/SdkComponents#ndkDirectory() + * sometimes fails to resolve when ndkPath is not defined (Cannot query the value of this property because it has + * no value available.). This means that even `map` and `isPresent` will break. + * + * So we also fall back use the old BaseExtension if it appears broken + */ + private fun ndkToolchainDirectoryFor(project: Project): Provider { + val extensions = project.extensions + val sdkComponents = extensions.getByType(AndroidComponentsExtension::class.java)?.sdkComponents + + return project.provider { + try { + return@provider sdkComponents!!.ndkDirectory.get().asFile + } catch (e: Exception) { + return@provider extensions.getByType(BaseExtension::class.java).ndkDirectory.absoluteFile + } + } + } + + private fun isLegacyMappingRequired(variant: ApkVariant): Boolean? { + val bugsnagAndroidCoreVersion = variant.compileConfiguration.allDependencyConstraints + .find { it.group == "com.bugsnag" && it.name == "bugsnag-plugin-android-ndk" } + ?.version + ?: return null + + return VersionNumber.parse(bugsnagAndroidCoreVersion) < VersionNumber.version(5, 26) + } + + fun configureNdkToolkit( + project: Project, + bugsnag: BugsnagPluginExtension, + variant: ApkVariant + ): NdkToolchain { + val useLegacyNdkSymbolUpload = bugsnag.useLegacyNdkSymbolUpload.get() + var legacyUploadRequired = isLegacyMappingRequired(variant) + + if (legacyUploadRequired == null) { + project.logger.warn( + "Cannot detect Bugsnag SDK version for variant ${variant.name}, assuming a modern version is " + + "being used. This can cause problems with NDK symbols if older versions are being used. " + + "Please either specify the Bugsnag SDK version for ${variant.name} directly." + + "See https://docs.bugsnag.com/api/ndk-symbol-mapping-upload/ for details." + ) + + legacyUploadRequired = false + } + + if (!useLegacyNdkSymbolUpload && legacyUploadRequired) { + throw StopExecutionException( + "Your Bugsnag SDK configured for variant ${variant.name} does not support the new NDK " + + "symbols upload mechanism. Please set legacyNDKSymbolsUpload or upgrade your " + + "Bugsnag SDK. See https://docs.bugsnag.com/api/ndk-symbol-mapping-upload/ for details." + ) + } + + val overrides = bugsnag.objdumpPaths.map { it.mapKeys { (abi, _) -> Abi.findByName(abi)!! } } + val ndkToolchain = project.objects.newInstance() + ndkToolchain.baseDir.set(ndkToolchainDirectoryFor(project)) + ndkToolchain.useLegacyNdkSymbolUpload.set(useLegacyNdkSymbolUpload) + ndkToolchain.overrides.set(overrides) + + return ndkToolchain + } + } } + +val NdkToolchain.version get() = baseDir.map { VersionNumber.parse(it.name) } From d46a33b7957f303561e3ddd8d5007bdd8e79b896 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 18 Aug 2022 11:51:00 +0100 Subject: [PATCH 03/12] test(ndk): added an end-to-end test to ensure the SDK version validation works as expected --- features/fixtures/config/ndk/old_sdk_upload_failure.gradle | 5 +++++ features/fixtures/config/ndk/standard.gradle | 3 +++ features/fixtures/ndkapp/app/build.gradle | 7 +++---- features/ndk_app.feature | 5 +++++ features/steps/gradle_plugin_steps.rb | 7 +++++++ 5 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 features/fixtures/config/ndk/old_sdk_upload_failure.gradle create mode 100644 features/fixtures/config/ndk/standard.gradle diff --git a/features/fixtures/config/ndk/old_sdk_upload_failure.gradle b/features/fixtures/config/ndk/old_sdk_upload_failure.gradle new file mode 100644 index 00000000..84126f8e --- /dev/null +++ b/features/fixtures/config/ndk/old_sdk_upload_failure.gradle @@ -0,0 +1,5 @@ +dependencies { + implementation 'com.bugsnag:bugsnag-android:5.9.4' +} + +bugsnag.useLegacyNdkSymbolUpload = true diff --git a/features/fixtures/config/ndk/standard.gradle b/features/fixtures/config/ndk/standard.gradle new file mode 100644 index 00000000..10049421 --- /dev/null +++ b/features/fixtures/config/ndk/standard.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation 'com.bugsnag:bugsnag-android:5.9.4' +} diff --git a/features/fixtures/ndkapp/app/build.gradle b/features/fixtures/ndkapp/app/build.gradle index e30452e9..a5fd6385 100644 --- a/features/fixtures/ndkapp/app/build.gradle +++ b/features/fixtures/ndkapp/app/build.gradle @@ -60,10 +60,6 @@ android { } } -dependencies { - implementation 'com.bugsnag:bugsnag-android:5.9.4' -} - bugsnag { uploadNdkMappings = true endpoint = "http://localhost:9339/builds" @@ -89,3 +85,6 @@ bugsnag { ] } } + +def ndkConfig = System.env.BUGSNAG_NDK_CONFIG ?: "standard" +apply from: "../../config/ndk/${ndkConfig}.gradle" diff --git a/features/ndk_app.feature b/features/ndk_app.feature index e770e03a..361f39dd 100644 --- a/features/ndk_app.feature +++ b/features/ndk_app.feature @@ -85,3 +85,8 @@ Scenario: Mapping files uploaded for custom sharedObjectPaths And 1 requests are valid for the android mapping API and match the following: | appId | | com.bugsnag.android.ndkapp | + +Scenario: Mapping fails when using obcopy and an incompatible SDK + When I build the NDK app using the "old_sdk_upload_failure" config + And I wait for 3 seconds + Then I should receive no requests diff --git a/features/steps/gradle_plugin_steps.rb b/features/steps/gradle_plugin_steps.rb index ed6c2b97..6896542a 100644 --- a/features/steps/gradle_plugin_steps.rb +++ b/features/steps/gradle_plugin_steps.rb @@ -33,6 +33,13 @@ } end +When("I build the NDK app using the {string} config") do |config| + Maze::Runner.environment['BUGSNAG_NDK_CONFIG'] = config + steps %Q{ + And I run the script "features/scripts/build_ndk_app.sh" synchronously +} +end + When("I set the fixture JVM arguments to {string}") do |jvm_args| steps %Q{ When I set environment variable "CUSTOM_JVM_ARGS" to "#{jvm_args}" From bbcdc4bfd09f14a4e60ce05fa921f82630d4565d Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 23 Aug 2022 17:27:09 +0100 Subject: [PATCH 04/12] feat(ndk): introduce upload task to handle symbol-only .so files --- features/fixtures/config/ndk/objcopy.gradle | 6 + .../config/ndk/old_sdk_upload_failure.gradle | 1 + features/ndk_app.feature | 19 +++ features/steps/gradle_plugin_steps.rb | 19 ++- .../gradle/AndroidManifestInfoReceiver.kt | 4 +- .../android/gradle/BugsnagFileUploadTask.kt | 13 ++ .../gradle/BugsnagMultiPartUploadRequest.kt | 64 ++++---- .../bugsnag/android/gradle/BugsnagPlugin.kt | 11 +- .../gradle/BugsnagUploadProguardTask.kt | 3 +- .../gradle/BugsnagUploadSharedObjectTask.kt | 6 +- .../android/gradle/BugsnagUploadSoSymTask.kt | 139 ++++++++++++++++++ 11 files changed, 247 insertions(+), 38 deletions(-) create mode 100644 features/fixtures/config/ndk/objcopy.gradle create mode 100644 src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt diff --git a/features/fixtures/config/ndk/objcopy.gradle b/features/fixtures/config/ndk/objcopy.gradle new file mode 100644 index 00000000..e62bfeca --- /dev/null +++ b/features/fixtures/config/ndk/objcopy.gradle @@ -0,0 +1,6 @@ +dependencies { + implementation 'com.bugsnag:bugsnag-android:5.26.0' +} + +android.ndkVersion = "23.0.7599858" +bugsnag.useLegacyNdkSymbolUpload = false diff --git a/features/fixtures/config/ndk/old_sdk_upload_failure.gradle b/features/fixtures/config/ndk/old_sdk_upload_failure.gradle index 84126f8e..55dee428 100644 --- a/features/fixtures/config/ndk/old_sdk_upload_failure.gradle +++ b/features/fixtures/config/ndk/old_sdk_upload_failure.gradle @@ -2,4 +2,5 @@ dependencies { implementation 'com.bugsnag:bugsnag-android:5.9.4' } +android.ndkVersion = "23.0.7599858" bugsnag.useLegacyNdkSymbolUpload = true diff --git a/features/ndk_app.feature b/features/ndk_app.feature index 361f39dd..745e474d 100644 --- a/features/ndk_app.feature +++ b/features/ndk_app.feature @@ -90,3 +90,22 @@ Scenario: Mapping fails when using obcopy and an incompatible SDK When I build the NDK app using the "old_sdk_upload_failure" config And I wait for 3 seconds Then I should receive no requests + +Scenario: objcopy is used to produce symbols when configured + When I build the NDK app using the "objcopy" config + And I wait to receive 6 builds + + Then 1 requests are valid for the build API and match the following: + | appVersionCode | appVersion | buildTool | + | 1 | 1.0 | gradle-android | + + And 4 requests are valid for the android so symbol mapping API and match the following: + | projectRoot | sharedObjectName | + | /\S+/ | libnative-lib.so | + | /\S+/ | libnative-lib.so | + | /\S+/ | libnative-lib.so | + | /\S+/ | libnative-lib.so | + + And 1 requests are valid for the android mapping API and match the following: + | appId | + | com.bugsnag.android.ndkapp | diff --git a/features/steps/gradle_plugin_steps.rb b/features/steps/gradle_plugin_steps.rb index 6896542a..aa9cdaed 100644 --- a/features/steps/gradle_plugin_steps.rb +++ b/features/steps/gradle_plugin_steps.rb @@ -112,6 +112,16 @@ def setup_and_run_script(module_config, bugsnag_config, script_path, variant = n end end +Then('{int} requests are valid for the android so symbol mapping API and match the following:') do |request_count, data_table| + requests = get_requests_with_field('build', 'soFile') + assert_equal(request_count, requests.length, 'Wrong number of android .so symbol mapping API requests') + Maze::Assertions::RequestSetAssertions.assert_requests_match requests, data_table + + requests.each do |request| + valid_android_so_symbol_mapping_api?(request[:body]) + end +end + Then('{int} requests are valid for the JS source map API and match the following:') do |request_count, data_table| requests = get_requests_with_field('build', 'sourceMap') assert_equal(request_count, requests.length, 'Wrong number of JS source map API requests') @@ -174,16 +184,24 @@ def valid_build_api?(request_body) def valid_android_mapping_api?(request_body) valid_mapping_api?(request_body) + assert_not_nil(request_body['buildUUID']) assert_not_nil(request_body['proguard']) end def valid_android_ndk_mapping_api?(request_body) valid_mapping_api?(request_body) + assert_not_nil(request_body['buildUUID']) assert_not_nil(request_body['soSymbolFile']) end +def valid_android_so_symbol_mapping_api?(request_body) + valid_mapping_api?(request_body) + assert_not_nil(request_body['soFile']) +end + def valid_android_unity_ndk_mapping_api?(request_body) valid_mapping_api?(request_body) + assert_not_nil(request_body['buildUUID']) assert_not_nil(request_body['soSymbolTableFile']) end @@ -191,7 +209,6 @@ def valid_mapping_api?(request_body) assert_equal($api_key, request_body['apiKey']) assert_not_nil(request_body['appId']) assert_not_nil(request_body['versionCode']) - assert_not_nil(request_body['buildUUID']) assert_not_nil(request_body['versionName']) end diff --git a/src/main/kotlin/com/bugsnag/android/gradle/AndroidManifestInfoReceiver.kt b/src/main/kotlin/com/bugsnag/android/gradle/AndroidManifestInfoReceiver.kt index 39b38108..2f1812a2 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/AndroidManifestInfoReceiver.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/AndroidManifestInfoReceiver.kt @@ -1,10 +1,10 @@ package com.bugsnag.android.gradle import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile interface AndroidManifestInfoReceiver { - @get:Input + @get:InputFile val manifestInfo: RegularFileProperty } diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagFileUploadTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagFileUploadTask.kt index 04baf550..b5dc687f 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagFileUploadTask.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagFileUploadTask.kt @@ -2,13 +2,26 @@ package com.bugsnag.android.gradle import com.bugsnag.android.gradle.internal.BugsnagHttpClientHelper import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal interface BugsnagFileUploadTask { + @get:Input val failOnUploadError: Property + + @get:Input val overwrite: Property + + @get:Input val endpoint: Property + + @get:Input val retryCount: Property + + @get:Input val timeoutMillis: Property + + @get:Internal val httpClientHelper: Property fun configureWith(bugsnag: BugsnagPluginExtension) { diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagMultiPartUploadRequest.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagMultiPartUploadRequest.kt index 4d8ce78a..86d96ce0 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagMultiPartUploadRequest.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagMultiPartUploadRequest.kt @@ -4,21 +4,12 @@ import com.bugsnag.android.gradle.internal.runRequestWithRetries import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.Response import org.gradle.api.DefaultTask import java.io.IOException /** - * Task to upload ProGuard mapping files to Bugsnag. - * - * Reads meta-data tags from the project's AndroidManifest.xml to extract a - * build UUID (injected by BugsnagManifestTask) and a Bugsnag API Key: - * - * https://developer.android.com/guide/topics/manifest/manifest-intro.html - * https://developer.android.com/guide/topics/manifest/meta-data-element.html - * - * This task must be called after ProGuard mapping files are generated, so - * it is usually safe to have this be the absolute last task executed during - * a build. + * Wrapper for common Bugsnag HTTP multipart upload behaviours. */ class BugsnagMultiPartUploadRequest( private val failOnUploadError: Boolean, @@ -28,13 +19,10 @@ class BugsnagMultiPartUploadRequest( ) { fun uploadMultipartEntity( - manifestInfo: AndroidManifestInfo, retryCount: Int, - action: (MultipartBody.Builder) -> Unit + bodyBuilder: BugsnagMultiPartUploadRequest.(MultipartBody.Builder) -> Unit ): String { - val builder = buildMultipartBody(manifestInfo, overwrite) - action(builder) - val body = builder.build() + val body = createMultipartBody(bodyBuilder) return try { runRequestWithRetries(retryCount) { @@ -48,7 +36,21 @@ class BugsnagMultiPartUploadRequest( } } - private fun uploadToServer(body: MultipartBody): String? { + fun createMultipartBody(bodyBuilder: BugsnagMultiPartUploadRequest.(MultipartBody.Builder) -> Unit): MultipartBody { + return buildMultipartBody(overwrite) + .also { bodyBuilder(it) } + .build() + } + + fun MultipartBody.Builder.addAndroidManifestInfo(manifestInfo: AndroidManifestInfo): MultipartBody.Builder { + return addFormDataPart("apiKey", manifestInfo.apiKey) + .addFormDataPart("appId", manifestInfo.applicationId) + .addFormDataPart("versionCode", manifestInfo.versionCode) + .addFormDataPart("versionName", manifestInfo.versionName) + .addFormDataPart("buildUUID", manifestInfo.buildUUID) + } + + fun uploadRequest(body: MultipartBody, responseHandler: (Response) -> R): R { // Make the request val request = Request.Builder() .url(endpoint) @@ -56,28 +58,30 @@ class BugsnagMultiPartUploadRequest( .build() okHttpClient.newCall(request).execute().use { response -> + return responseHandler(response) + } + } + + private fun uploadToServer(body: MultipartBody): String? { + return uploadRequest(body) { response -> if (!response.isSuccessful) { throw IOException("Bugsnag upload failed with code ${response.code}") } - return response.body?.string() + + response.body?.string() } } companion object { - internal fun buildMultipartBody(manifestInfo: AndroidManifestInfo, overwrite: Boolean): MultipartBody.Builder { - val builder = MultipartBody.Builder() + internal fun buildMultipartBody(overwrite: Boolean): MultipartBody.Builder { + return MultipartBody.Builder() + .apply { + if (overwrite) { + addFormDataPart("overwrite", "true") + } + } .setType(MultipartBody.FORM) - .addFormDataPart("apiKey", manifestInfo.apiKey) - .addFormDataPart("appId", manifestInfo.applicationId) - .addFormDataPart("versionCode", manifestInfo.versionCode) - .addFormDataPart("buildUUID", manifestInfo.buildUUID) - .addFormDataPart("versionName", manifestInfo.versionName) - - if (overwrite) { - builder.addFormDataPart("overwrite", "true") - } - return builder } internal fun from( diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt index a98b375f..cec82541 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt @@ -257,7 +257,8 @@ class BugsnagPlugin : Plugin { else -> null } val uploadNdkMappingProvider = when { - ndkEnabled && generateNdkMappingProvider != null -> { + ndkEnabled && generateNdkMappingProvider != null + && ndkToolchain.preferredMappingTool() == NdkToolchain.MappingTool.OBJDUMP -> { BugsnagUploadSharedObjectTask.registerUploadNdkTask( project, output, @@ -268,6 +269,14 @@ class BugsnagPlugin : Plugin { ) } + ndkEnabled && generateNdkMappingProvider != null -> BugsnagUploadSoSymTask.register( + project, + output, + generateNdkMappingProvider, + httpClientHelperProvider, + ndkUploadClientProvider + ) + else -> null } diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadProguardTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadProguardTask.kt index 0ea92b03..5da19ec1 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadProguardTask.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadProguardTask.kt @@ -85,8 +85,9 @@ open class BugsnagUploadProguardTask @Inject constructor( val request = BugsnagMultiPartUploadRequest.from(this) val mappingFileHash = mappingFile.md5HashCode() val response = uploadRequestClient.get().makeRequestIfNeeded(manifestInfo, mappingFileHash) { - request.uploadMultipartEntity(manifestInfo, retryCount.get()) { builder -> + request.uploadMultipartEntity(retryCount.get()) { builder -> logger.lifecycle("Bugsnag: Uploading JVM mapping file from: $mappingFile") + builder.addAndroidManifestInfo(manifestInfo) builder.addFormDataPart("proguard", mappingFile.name, mappingFile.asRequestBody()) } } diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSharedObjectTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSharedObjectTask.kt index eefcf7ad..3122c81c 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSharedObjectTask.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSharedObjectTask.kt @@ -125,10 +125,10 @@ internal open class BugsnagUploadSharedObjectTask @Inject constructor( val mappingFileHash = mappingFile.md5HashCode() val response = uploadRequestClient.get().makeRequestIfNeeded(manifestInfo, mappingFileHash) { logger.lifecycle( - "Bugsnag: Uploading SO mapping file for " + - "$sharedObjectName ($arch) from $mappingFile" + "Bugsnag: Uploading SO mapping file for $sharedObjectName ($arch) from $mappingFile" ) - request.uploadMultipartEntity(manifestInfo, retryCount.get()) { builder -> + request.uploadMultipartEntity(retryCount.get()) { builder -> + builder.addAndroidManifestInfo(manifestInfo) builder.addFormDataPart(soUploadKey, mappingFile.name, mappingFile.asRequestBody()) builder.addFormDataPart("arch", arch) builder.addFormDataPart("sharedObjectName", sharedObjectName) diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt new file mode 100644 index 00000000..339b95fc --- /dev/null +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt @@ -0,0 +1,139 @@ +package com.bugsnag.android.gradle + +import com.android.build.gradle.api.BaseVariantOutput +import com.bugsnag.android.gradle.internal.AbstractSoMappingTask +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.taskNameSuffix +import okhttp3.RequestBody.Companion.asRequestBody +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.StopExecutionException +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import java.io.File +import java.net.HttpURLConnection.HTTP_NOT_FOUND + +abstract class BugsnagUploadSoSymTask : DefaultTask(), AndroidManifestInfoReceiver, BugsnagFileUploadTask { + + @get:InputDirectory + abstract val symbolFilesDir: DirectoryProperty + + @get:OutputFile + abstract val requestOutputFile: RegularFileProperty + + @get:Input + abstract val projectRoot: Property + + @get:Internal + internal abstract val uploadRequestClient: Property + + init { + group = BugsnagPlugin.GROUP_NAME + description = "Uploads SO Symbol files to Bugsnag" + } + + @TaskAction + fun upload() { + val rootDir = symbolFilesDir.asFile.get() + logger.info("Bugsnag: Found shared object files for upload: $rootDir") + rootDir.walkTopDown() + .filter { it.isFile && it.extension == "gz" && it.length() >= VALID_SO_FILE_THRESHOLD } + .forEach { uploadSymbols(it) } + } + + /** + * Uploads the given shared object mapping information + * @param mappingFile the file to upload + */ + private fun uploadSymbols(mappingFile: File) { + val sharedObjectName = mappingFile.nameWithoutExtension + val requestEndpoint = endpoint.get() + ENDPOINT_SUFFIX + + val request = BugsnagMultiPartUploadRequest.from(this, requestEndpoint) + val manifestInfo = parseManifestInfo() + val mappingFileHash = mappingFile.md5HashCode() + + val response = uploadRequestClient.get().makeRequestIfNeeded(manifestInfo, mappingFileHash) { + logger.lifecycle("Bugsnag: Uploading SO mapping file from $mappingFile") + val body = request.createMultipartBody { builder -> + builder + .addFormDataPart("apiKey", manifestInfo.apiKey) + .addFormDataPart("appId", manifestInfo.applicationId) + .addFormDataPart("versionCode", manifestInfo.versionCode) + .addFormDataPart("versionName", manifestInfo.versionName) + .addFormDataPart("soFile", mappingFile.name, mappingFile.asRequestBody()) + .addFormDataPart("sharedObjectName", sharedObjectName) + .addFormDataPart("projectRoot", projectRoot.get()) + } + + request.uploadRequest(body) { response -> + if (response.code == HTTP_NOT_FOUND && endpoint.get() != UPLOAD_ENDPOINT_DEFAULT) { + throw StopExecutionException( + "Bugsnag instance does not support the new NDK symbols upload mechanism. " + + "Please set legacyNDKSymbolsUpload or upgrade your Bugsnag instance. " + + "See https://docs.bugsnag.com/api/ndk-symbol-mapping-upload/ for details." + ) + } + + if (!response.isSuccessful) { + "Failure" + } else { + response.body!!.string() + } + } + } + requestOutputFile.asFile.get().writeText(response) + } + + companion object { + private const val ENDPOINT_SUFFIX = "/ndk-symbol" + + private const val VALID_SO_FILE_THRESHOLD = 1024 + + fun taskNameFor(variant: BaseVariantOutput) = + "uploadBugsnag${variant.baseName.capitalize()}Symbols" + + internal fun requestOutputFileFor(project: Project, output: BaseVariantOutput): Provider { + val path = "intermediates/bugsnag/requests/symFor${output.taskNameSuffix()}.json" + return project.layout.buildDirectory.file(path) + } + + fun register( + project: Project, + variant: BaseVariantOutput, + generateTaskProvider: TaskProvider, + httpClientHelperProvider: Provider, + ndkUploadClientProvider: Provider, + ): TaskProvider { + val bugsnag = project.extensions.getByType(BugsnagPluginExtension::class.java) + return project.tasks.register(taskNameFor(variant), BugsnagUploadSoSymTask::class.java) { task -> + task.dependsOn(generateTaskProvider) + task.usesService(httpClientHelperProvider) + task.usesService(ndkUploadClientProvider) + + task.endpoint.set(bugsnag.endpoint) + + task.manifestInfo.set(BugsnagManifestUuidTask.manifestInfoForOutput(project, variant)) + task.symbolFilesDir.set(generateTaskProvider.flatMap { it.outputDirectory }) + task.requestOutputFile.set(requestOutputFileFor(project, variant)) + task.projectRoot.set(bugsnag.projectRoot.getOrElse(project.projectDir.toString())) + + task.httpClientHelper.set(httpClientHelperProvider) + task.uploadRequestClient.set(ndkUploadClientProvider) + + task.configureWith(bugsnag) + } + } + } +} From 3492d55b50edba80ad612e6fd5d60e80dab6ac28 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 2 Sep 2022 14:29:11 +0100 Subject: [PATCH 05/12] feat(ndk): update the NdkToolchain to handle LLVM & GNU `objcopy` implementations --- detekt-baseline.xml | 5 +- .../android/gradle/internal/NdkToolchain.kt | 143 ++++++++++++------ .../gradle/internal/NdkToolchainTest.kt | 104 +++++++++++++ 3 files changed, 201 insertions(+), 51 deletions(-) create mode 100644 src/test/kotlin/com/bugsnag/android/gradle/internal/NdkToolchainTest.kt diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 335e076c..6834545b 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -8,14 +8,13 @@ MagicNumber:BugsnagPluginExtension.kt$BugsnagPluginExtension$60000 MagicNumber:BugsnagReleasesTask.kt$BugsnagReleasesTask$200 MagicNumber:MappingFileProvider.kt$9 - MagicNumber:NdkToolchain.kt$NdkToolchain$23 - MagicNumber:NdkToolchain.kt$NdkToolchain.Companion$26 - MagicNumber:NdkToolchain.kt$NdkToolchain.Companion$5 MaxLineLength:NdkToolchain.kt$NdkToolchain.Companion$/* * SdkComponents.ndkDirectory * https://developer.android.com/reference/tools/gradle-api/7.2/com/android/build/api/dsl/SdkComponents#ndkDirectory() * sometimes fails to resolve when ndkPath is not defined (Cannot query the value of this property because it has * no value available.). This means that even `map` and `isPresent` will break. * * So we also fall back use the old BaseExtension if it appears broken */ ReturnCount:BugsnagPlugin.kt$BugsnagPlugin$ @Suppress("SENSELESS_COMPARISON") internal fun isUnityLibraryUploadEnabled( bugsnag: BugsnagPluginExtension, android: BaseExtension ): Boolean ReturnCount:BugsnagPlugin.kt$BugsnagPlugin$ private fun registerUploadSourceMapTask( project: Project, variant: BaseVariant, output: BaseVariantOutput, bugsnag: BugsnagPluginExtension, manifestInfoProvider: Provider<RegularFile> ): TaskProvider<out BugsnagUploadJsSourceMapTask>? ReturnCount:ManifestUuidTaskV2Compat.kt$internal fun createManifestUpdateTask( bugsnag: BugsnagPluginExtension, project: Project, variantName: String, variantOutput: VariantOutput ): TaskProvider<BugsnagManifestUuidTask>? + ReturnCount:NdkToolchain.kt$NdkToolchain.Companion$private fun getBugsnagAndroidNDKVersion(variant: ApkVariant): VersionNumber? SpreadOperator:DexguardCompat.kt$(buildDir, *path, variant.dirName, outputDir, "mapping.txt") + SwallowedException:NdkToolchain.kt$NdkToolchain.Companion$catch (e: Exception) { return null } SwallowedException:NdkToolchain.kt$NdkToolchain.Companion$catch (e: Exception) { return@provider extensions.getByType(BaseExtension::class.java).ndkDirectory.absoluteFile } TooGenericExceptionCaught:AbstractSoMappingTask.kt$AbstractSoMappingTask$e: Exception TooGenericExceptionCaught:BugsnagHttpClientHelper.kt$exc: Throwable diff --git a/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt b/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt index cc013a88..1d8818f5 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt @@ -11,23 +11,11 @@ import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.StopExecutionException import org.gradle.util.VersionNumber import java.io.File -private val osName = when { - Os.isFamily(Os.FAMILY_MAC) -> "darwin-x86_64" - Os.isFamily(Os.FAMILY_UNIX) -> "linux-x86_64" - Os.isFamily(Os.FAMILY_WINDOWS) -> { - when { - "x86" == System.getProperty("os.arch") -> "windows" - else -> "windows-x86_64" - } - } - - else -> null -} - abstract class NdkToolchain { @get:Input abstract val baseDir: Property @@ -38,7 +26,38 @@ abstract class NdkToolchain { @get:Input abstract val overrides: MapProperty + @get:Input + @get:Optional + abstract val bugsnagNdkVersion: Property + fun preferredMappingTool(): MappingTool { + var legacyUploadRequired = bugsnagNdkVersion.orNull + ?.let { VersionNumber.parse(it) } + ?.let { it < MIN_BUGSNAG_ANDROID_VERSION } + if (legacyUploadRequired == null) { +// logger.warn( +// "Cannot detect Bugsnag SDK version for variant ${variant.name}, assuming a modern version is " + +// "being used. This can cause problems with NDK symbols if older versions are being used. " + +// "Please either specify the Bugsnag SDK version for ${variant.name} directly." + +// "See https://docs.bugsnag.com/api/ndk-symbol-mapping-upload/ for details." +// ) + + legacyUploadRequired = false + } + + if (!useLegacyNdkSymbolUpload.get() && legacyUploadRequired) { + throw StopExecutionException( + "Your Bugsnag SDK configured for variant does not support the new NDK " + + "symbols upload mechanism. Please set legacyNDKSymbolsUpload or upgrade your " + + "Bugsnag SDK. See https://docs.bugsnag.com/api/ndk-symbol-mapping-upload/ for details." + ) +// throw StopExecutionException( +// "Your Bugsnag SDK configured for variant ${variant.name} does not support the new NDK " + +// "symbols upload mechanism. Please set legacyNDKSymbolsUpload or upgrade your " + +// "Bugsnag SDK. See https://docs.bugsnag.com/api/ndk-symbol-mapping-upload/ for details." +// ) + } + // useLegacyNdkSymbolUpload force overrides any defaults or options if (useLegacyNdkSymbolUpload.get()) { return MappingTool.OBJDUMP @@ -46,7 +65,7 @@ abstract class NdkToolchain { val ndkVersion = version.get() return when { - ndkVersion >= VersionNumber.version(23, 0) -> MappingTool.OBJCOPY + ndkVersion >= MIN_NDK_OBJCOPY_VERSION -> MappingTool.OBJCOPY else -> MappingTool.OBJDUMP } } @@ -58,6 +77,7 @@ abstract class NdkToolchain { baseDir.set(other.baseDir) overrides.set(other.overrides) useLegacyNdkSymbolUpload.set(other.useLegacyNdkSymbolUpload) + bugsnagNdkVersion.set(other.bugsnagNdkVersion) } private fun executableName(cmdName: String): String { @@ -75,12 +95,21 @@ abstract class NdkToolchain { } fun objcopyForAbi(abi: Abi): File { - val objdumpOverrides = overrides.get() + val objcopyOverrides = overrides.get() - return objdumpOverrides[abi]?.let { File(it) } ?: File( - baseDir.get(), - "toolchains/llvm/prebuilt/$osName/bin/${executableName("llvm-objcopy")}" - ) + return objcopyOverrides[abi]?.let { File(it) } + ?: locateObjcopy(abi) + } + + private fun locateObjcopy(abi: Abi): File { + val relativeExecutablePath = when { + isLLVMPreferred.get() -> "toolchains/llvm/prebuilt/$osName/bin/${executableName("llvm-objcopy")}" + else -> + "toolchains/${abi.toolchainPrefix}-4.9/prebuilt/" + + "$osName/bin/${abi.objdumpPrefix}-${executableName("objcopy")}" + } + + return File(baseDir.get(), relativeExecutablePath) } enum class MappingTool { @@ -89,6 +118,35 @@ abstract class NdkToolchain { } companion object { + /** + * Minimum `bugsnag-android` version where the new symbol uploading is available, using `objcopy` to produce + * the symbol files instead of `objdump` + */ + internal val MIN_BUGSNAG_ANDROID_VERSION = VersionNumber.version(5, 26) + + /** + * The minimum NDK version where we will use `objcopy` instead of `objdump` to produce the symbol files + */ + internal val MIN_NDK_OBJCOPY_VERSION = VersionNumber.version(21) + + /** + * The minimum NDK version where we will use the LLVM toolchain instead of the GNU toolchain + */ + internal val MIN_NDK_LLVM_VERSION = VersionNumber.version(23) + + private val osName = when { + Os.isFamily(Os.FAMILY_MAC) -> "darwin-x86_64" + Os.isFamily(Os.FAMILY_UNIX) -> "linux-x86_64" + Os.isFamily(Os.FAMILY_WINDOWS) -> { + when { + "x86" == System.getProperty("os.arch") -> "windows" + else -> "windows-x86_64" + } + } + + else -> null + } + /* * SdkComponents.ndkDirectory * https://developer.android.com/reference/tools/gradle-api/7.2/com/android/build/api/dsl/SdkComponents#ndkDirectory() @@ -110,13 +168,20 @@ abstract class NdkToolchain { } } - private fun isLegacyMappingRequired(variant: ApkVariant): Boolean? { - val bugsnagAndroidCoreVersion = variant.compileConfiguration.allDependencyConstraints - .find { it.group == "com.bugsnag" && it.name == "bugsnag-plugin-android-ndk" } - ?.version - ?: return null - - return VersionNumber.parse(bugsnagAndroidCoreVersion) < VersionNumber.version(5, 26) + private fun getBugsnagAndroidNDKVersion(variant: ApkVariant): String? { + try { + val bugsnagAndroidCoreVersion = variant.compileConfiguration.resolvedConfiguration.resolvedArtifacts + .find { + it.moduleVersion.id.group == "com.bugsnag" && + it.moduleVersion.id.name == "bugsnag-plugin-android-ndk" + } + ?.moduleVersion?.id?.version + ?: return null + + return bugsnagAndroidCoreVersion + } catch (e: Exception) { + return null + } } fun configureNdkToolkit( @@ -125,36 +190,18 @@ abstract class NdkToolchain { variant: ApkVariant ): NdkToolchain { val useLegacyNdkSymbolUpload = bugsnag.useLegacyNdkSymbolUpload.get() - var legacyUploadRequired = isLegacyMappingRequired(variant) - - if (legacyUploadRequired == null) { - project.logger.warn( - "Cannot detect Bugsnag SDK version for variant ${variant.name}, assuming a modern version is " + - "being used. This can cause problems with NDK symbols if older versions are being used. " + - "Please either specify the Bugsnag SDK version for ${variant.name} directly." + - "See https://docs.bugsnag.com/api/ndk-symbol-mapping-upload/ for details." - ) - - legacyUploadRequired = false - } - - if (!useLegacyNdkSymbolUpload && legacyUploadRequired) { - throw StopExecutionException( - "Your Bugsnag SDK configured for variant ${variant.name} does not support the new NDK " + - "symbols upload mechanism. Please set legacyNDKSymbolsUpload or upgrade your " + - "Bugsnag SDK. See https://docs.bugsnag.com/api/ndk-symbol-mapping-upload/ for details." - ) - } - val overrides = bugsnag.objdumpPaths.map { it.mapKeys { (abi, _) -> Abi.findByName(abi)!! } } + val ndkToolchain = project.objects.newInstance() ndkToolchain.baseDir.set(ndkToolchainDirectoryFor(project)) ndkToolchain.useLegacyNdkSymbolUpload.set(useLegacyNdkSymbolUpload) ndkToolchain.overrides.set(overrides) + ndkToolchain.bugsnagNdkVersion.set(project.provider { getBugsnagAndroidNDKVersion(variant) }) return ndkToolchain } } } -val NdkToolchain.version get() = baseDir.map { VersionNumber.parse(it.name) } +private val NdkToolchain.version get() = baseDir.map { VersionNumber.parse(it.name) } +private val NdkToolchain.isLLVMPreferred get() = version.map { it >= NdkToolchain.MIN_NDK_LLVM_VERSION } diff --git a/src/test/kotlin/com/bugsnag/android/gradle/internal/NdkToolchainTest.kt b/src/test/kotlin/com/bugsnag/android/gradle/internal/NdkToolchainTest.kt new file mode 100644 index 00000000..8072de48 --- /dev/null +++ b/src/test/kotlin/com/bugsnag/android/gradle/internal/NdkToolchainTest.kt @@ -0,0 +1,104 @@ +package com.bugsnag.android.gradle.internal + +import com.bugsnag.android.gradle.Abi +import org.gradle.api.Transformer +import org.gradle.api.internal.provider.DefaultProvider +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import java.io.File +import org.mockito.Mockito.`when` as whenMock + +class NdkToolchainTest { + @Test + fun ndk19() { + val toolchain = TestNdkToolchainImpl(File("/19.2.5345600/"), true) + assertEquals(NdkToolchain.MappingTool.OBJDUMP, toolchain.preferredMappingTool()) + } + + @Test + fun ndk21Legacy() { + val toolchain = TestNdkToolchainImpl(File("/21.1.6352462/"), true) + assertEquals(NdkToolchain.MappingTool.OBJDUMP, toolchain.preferredMappingTool()) + } + + @Test + fun ndk21() { + val toolchain = TestNdkToolchainImpl(File("/21.1.6352462/"), false) + assertEquals(NdkToolchain.MappingTool.OBJCOPY, toolchain.preferredMappingTool()) + + val objcopyPath = toolchain.objcopyForAbi(Abi.ARM64_V8A).toString() + assertTrue( + "expected GNU objcopy path, but got: $objcopyPath", + objcopyPath.contains("/aarch64-linux-android-objcopy") + ) + } + + @Test + fun ndk23() { + val toolchain = TestNdkToolchainImpl(File("/23.0.7599858/"), false) + assertEquals(NdkToolchain.MappingTool.OBJCOPY, toolchain.preferredMappingTool()) + + val objcopyPath = toolchain.objcopyForAbi(Abi.ARM64_V8A).toString() + assertTrue( + "expected LLVM objcopy, but got: $objcopyPath", + objcopyPath.contains("/llvm-objcopy") + ) + } + + @Test + fun objcopyOverrides() { + val toolchain = TestNdkToolchainImpl( + File("/23.0.7599858/"), false, mapOf(Abi.ARM64_V8A to "arm64-objcopy") + ) + + assertEquals(NdkToolchain.MappingTool.OBJCOPY, toolchain.preferredMappingTool()) + + assertEquals("arm64-objcopy", toolchain.objcopyForAbi(Abi.ARM64_V8A).toString()) + assertNotEquals("arm64-objcopy", toolchain.objcopyForAbi(Abi.X86).toString()) + assertNotEquals("arm64-objcopy", toolchain.objcopyForAbi(Abi.X86_64).toString()) + assertNotEquals("arm64-objcopy", toolchain.objcopyForAbi(Abi.ARMEABI).toString()) + } + + @Test + fun objdumpOverrides() { + val toolchain = TestNdkToolchainImpl( + File("/19.2.5345600/"), true, mapOf(Abi.ARM64_V8A to "arm64-objcopy") + ) + + assertEquals(NdkToolchain.MappingTool.OBJDUMP, toolchain.preferredMappingTool()) + + assertEquals("arm64-objcopy", toolchain.objdumpForAbi(Abi.ARM64_V8A).toString()) + assertNotEquals("arm64-objcopy", toolchain.objdumpForAbi(Abi.X86).toString()) + assertNotEquals("arm64-objcopy", toolchain.objdumpForAbi(Abi.X86_64).toString()) + assertNotEquals("arm64-objcopy", toolchain.objdumpForAbi(Abi.ARMEABI).toString()) + } +} + +private class TestNdkToolchainImpl( + baseDir: File, + useLegacyNdkSymbolUpload: Boolean, + overrides: Map = emptyMap() +) : NdkToolchain() { + override val baseDir: Property = Mockito.mock(Property::class.java) as Property + override val useLegacyNdkSymbolUpload: Property = Mockito.mock(Property::class.java) as Property + override val overrides: MapProperty = Mockito.mock(MapProperty::class.java) as MapProperty + override val bugsnagNdkVersion: Property = Mockito.mock(Property::class.java) as Property + + init { + whenMock(this.baseDir.get()).thenReturn(baseDir) + whenMock(this.baseDir.map(any>())) + .thenAnswer { + DefaultProvider { + (it.arguments.first() as Transformer).transform(baseDir) + } + } + whenMock(this.useLegacyNdkSymbolUpload.get()).thenReturn(useLegacyNdkSymbolUpload) + whenMock(this.overrides.get()).thenReturn(overrides) + } +} From 1e5f54c1a8b2dd5649763e34dd6f2c960807751b Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 8 Sep 2022 10:32:51 +0100 Subject: [PATCH 06/12] feat(ndk): disable notifier version check for Unity --- detekt-baseline.xml | 5 +-- features/steps/gradle_plugin_steps.rb | 10 ++++- .../BugsnagGenerateUnitySoMappingTask.kt | 28 ++++++++++++ .../bugsnag/android/gradle/BugsnagPlugin.kt | 33 ++------------ .../android/gradle/internal/NdkToolchain.kt | 44 ++++++++++++------- .../android/gradle/PluginExtensionTest.kt | 8 ++-- .../gradle/internal/NdkToolchainTest.kt | 1 + 7 files changed, 74 insertions(+), 55 deletions(-) diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 6834545b..7435c5bc 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -9,12 +9,11 @@ MagicNumber:BugsnagReleasesTask.kt$BugsnagReleasesTask$200 MagicNumber:MappingFileProvider.kt$9 MaxLineLength:NdkToolchain.kt$NdkToolchain.Companion$/* * SdkComponents.ndkDirectory * https://developer.android.com/reference/tools/gradle-api/7.2/com/android/build/api/dsl/SdkComponents#ndkDirectory() * sometimes fails to resolve when ndkPath is not defined (Cannot query the value of this property because it has * no value available.). This means that even `map` and `isPresent` will break. * * So we also fall back use the old BaseExtension if it appears broken */ - ReturnCount:BugsnagPlugin.kt$BugsnagPlugin$ @Suppress("SENSELESS_COMPARISON") internal fun isUnityLibraryUploadEnabled( bugsnag: BugsnagPluginExtension, android: BaseExtension ): Boolean + ReturnCount:BugsnagGenerateUnitySoMappingTask.kt$BugsnagGenerateUnitySoMappingTask.Companion$ @Suppress("SENSELESS_COMPARISON") internal fun isUnityLibraryUploadEnabled( bugsnag: BugsnagPluginExtension, android: BaseExtension ): Boolean ReturnCount:BugsnagPlugin.kt$BugsnagPlugin$ private fun registerUploadSourceMapTask( project: Project, variant: BaseVariant, output: BaseVariantOutput, bugsnag: BugsnagPluginExtension, manifestInfoProvider: Provider<RegularFile> ): TaskProvider<out BugsnagUploadJsSourceMapTask>? ReturnCount:ManifestUuidTaskV2Compat.kt$internal fun createManifestUpdateTask( bugsnag: BugsnagPluginExtension, project: Project, variantName: String, variantOutput: VariantOutput ): TaskProvider<BugsnagManifestUuidTask>? - ReturnCount:NdkToolchain.kt$NdkToolchain.Companion$private fun getBugsnagAndroidNDKVersion(variant: ApkVariant): VersionNumber? SpreadOperator:DexguardCompat.kt$(buildDir, *path, variant.dirName, outputDir, "mapping.txt") - SwallowedException:NdkToolchain.kt$NdkToolchain.Companion$catch (e: Exception) { return null } + SwallowedException:NdkToolchain.kt$NdkToolchain.Companion$catch (e: Exception) { null } SwallowedException:NdkToolchain.kt$NdkToolchain.Companion$catch (e: Exception) { return@provider extensions.getByType(BaseExtension::class.java).ndkDirectory.absoluteFile } TooGenericExceptionCaught:AbstractSoMappingTask.kt$AbstractSoMappingTask$e: Exception TooGenericExceptionCaught:BugsnagHttpClientHelper.kt$exc: Throwable diff --git a/features/steps/gradle_plugin_steps.rb b/features/steps/gradle_plugin_steps.rb index aa9cdaed..5098d0bf 100644 --- a/features/steps/gradle_plugin_steps.rb +++ b/features/steps/gradle_plugin_steps.rb @@ -66,7 +66,7 @@ def setup_and_run_script(module_config, bugsnag_config, script_path, variant = n end When("I build the failing {string} on AGP {string} using the {string} bugsnag config") do |module_config, agp_version, bugsnag_config| -steps %Q{ + steps %Q{ When I set environment variable "AGP_VERSION" to "#{agp_version}" And I build the failing "#{module_config}" using the "#{bugsnag_config}" bugsnag config } @@ -197,6 +197,14 @@ def valid_android_ndk_mapping_api?(request_body) def valid_android_so_symbol_mapping_api?(request_body) valid_mapping_api?(request_body) assert_not_nil(request_body['soFile']) + + gzipped_part = request_body['soFile'] + archive = Zlib::GzipReader.new(StringIO.new(gzipped_part)) + + # check that decompressed this is a valid ELF file: + # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header + header = archive.read(4) + assert_equal("\x7f\x45\x4c\x46", header, 'not a valid ELF file') end def valid_android_unity_ndk_mapping_api?(request_body) diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateUnitySoMappingTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateUnitySoMappingTask.kt index 573e1605..e9e9722b 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateUnitySoMappingTask.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagGenerateUnitySoMappingTask.kt @@ -1,5 +1,6 @@ package com.bugsnag.android.gradle +import com.android.build.gradle.BaseExtension import com.android.build.gradle.api.ApkVariantOutput import com.bugsnag.android.gradle.internal.AbstractSoMappingTask import com.bugsnag.android.gradle.internal.NdkToolchain @@ -205,6 +206,33 @@ internal abstract class BugsnagGenerateUnitySoMappingTask @Inject constructor( return extensionMatch && nameMatch } + /** + * Determines whether SO mapping files should be generated for the + * libunity.so file in Unity projects. + */ + @Suppress("SENSELESS_COMPARISON") + internal fun isUnityLibraryUploadEnabled( + bugsnag: BugsnagPluginExtension, + android: BaseExtension + ): Boolean { + val enabled = bugsnag.uploadNdkUnityLibraryMappings.orNull + return when { + enabled != null -> enabled + else -> { + // workaround to avoid exception as noCompress was null until AGP 4.1 + runCatching { + val clz = android.aaptOptions.javaClass + val method = clz.getMethod("getNoCompress") + val noCompress = method.invoke(android.aaptOptions) + if (noCompress is Collection<*>) { + return noCompress.contains(".unity3d") + } + } + return false + } + } + } + override fun taskNameFor(variantOutputName: String) = "generateBugsnagUnity${variantOutputName.capitalize()}Mapping" } diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt index cec82541..29403778 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt @@ -203,7 +203,7 @@ class BugsnagPlugin : Plugin { } val jvmMinificationEnabled = project.isJvmMinificationEnabled(variant) val ndkEnabled = isNdkUploadEnabled(bugsnag, android) - val unityEnabled = isUnityLibraryUploadEnabled(bugsnag, android) + val unityEnabled = BugsnagGenerateUnitySoMappingTask.isUnityLibraryUploadEnabled(bugsnag, android) val reactNativeEnabled = isReactNativeUploadEnabled(bugsnag) // register bugsnag tasks @@ -576,40 +576,13 @@ class BugsnagPlugin : Plugin { } } - /** - * Determines whether SO mapping files should be generated for the - * libunity.so file in Unity projects. - */ - @Suppress("SENSELESS_COMPARISON") - internal fun isUnityLibraryUploadEnabled( - bugsnag: BugsnagPluginExtension, - android: BaseExtension - ): Boolean { - val enabled = bugsnag.uploadNdkUnityLibraryMappings.orNull - return when { - enabled != null -> enabled - else -> { - // workaround to avoid exception as noCompress was null until AGP 4.1 - runCatching { - val clz = android.aaptOptions.javaClass - val method = clz.getMethod("getNoCompress") - val noCompress = method.invoke(android.aaptOptions) - if (noCompress is Collection<*>) { - return noCompress.contains(".unity3d") - } - } - return false - } - } - } - internal fun isNdkUploadEnabled( bugsnag: BugsnagPluginExtension, android: BaseExtension ): Boolean { val usesCmake = android.externalNativeBuild.cmake.path != null val usesNdkBuild = android.externalNativeBuild.ndkBuild.path != null - val unityEnabled = isUnityLibraryUploadEnabled(bugsnag, android) + val unityEnabled = BugsnagGenerateUnitySoMappingTask.isUnityLibraryUploadEnabled(bugsnag, android) val default = usesCmake || usesNdkBuild || unityEnabled return bugsnag.uploadNdkMappings.getOrElse(default) } @@ -634,7 +607,7 @@ class BugsnagPlugin : Plugin { android: BaseExtension ): List { val searchPaths = bugsnag.sharedObjectPaths.get().toMutableList() - val unityEnabled = isUnityLibraryUploadEnabled(bugsnag, android) + val unityEnabled = BugsnagGenerateUnitySoMappingTask.isUnityLibraryUploadEnabled(bugsnag, android) val ndkEnabled = isNdkUploadEnabled(bugsnag, android) if (unityEnabled && ndkEnabled) { diff --git a/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt b/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt index 1d8818f5..bbc53f76 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/internal/NdkToolchain.kt @@ -4,9 +4,12 @@ import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.gradle.BaseExtension import com.android.build.gradle.api.ApkVariant import com.bugsnag.android.gradle.Abi +import com.bugsnag.android.gradle.BugsnagGenerateUnitySoMappingTask import com.bugsnag.android.gradle.BugsnagPluginExtension import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.Project +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider @@ -30,32 +33,32 @@ abstract class NdkToolchain { @get:Optional abstract val bugsnagNdkVersion: Property + @get:Input + abstract val variantName: Property + + private val logger: Logger = Logging.getLogger(this::class.java) + fun preferredMappingTool(): MappingTool { var legacyUploadRequired = bugsnagNdkVersion.orNull ?.let { VersionNumber.parse(it) } ?.let { it < MIN_BUGSNAG_ANDROID_VERSION } if (legacyUploadRequired == null) { -// logger.warn( -// "Cannot detect Bugsnag SDK version for variant ${variant.name}, assuming a modern version is " + -// "being used. This can cause problems with NDK symbols if older versions are being used. " + -// "Please either specify the Bugsnag SDK version for ${variant.name} directly." + -// "See https://docs.bugsnag.com/api/ndk-symbol-mapping-upload/ for details." -// ) + logger.warn( + "Cannot detect Bugsnag SDK version for variant ${variantName.get()}, assuming a modern version is " + + "being used. This can cause problems with NDK symbols if older versions are being used. " + + "Please either specify the Bugsnag SDK version for ${variantName.get()} directly." + + "See https://docs.bugsnag.com/api/ndk-symbol-mapping-upload/ for details." + ) legacyUploadRequired = false } if (!useLegacyNdkSymbolUpload.get() && legacyUploadRequired) { throw StopExecutionException( - "Your Bugsnag SDK configured for variant does not support the new NDK " + + "Your Bugsnag SDK configured for variant ${variantName.get()} does not support the new NDK " + "symbols upload mechanism. Please set legacyNDKSymbolsUpload or upgrade your " + "Bugsnag SDK. See https://docs.bugsnag.com/api/ndk-symbol-mapping-upload/ for details." ) -// throw StopExecutionException( -// "Your Bugsnag SDK configured for variant ${variant.name} does not support the new NDK " + -// "symbols upload mechanism. Please set legacyNDKSymbolsUpload or upgrade your " + -// "Bugsnag SDK. See https://docs.bugsnag.com/api/ndk-symbol-mapping-upload/ for details." -// ) } // useLegacyNdkSymbolUpload force overrides any defaults or options @@ -78,6 +81,7 @@ abstract class NdkToolchain { overrides.set(other.overrides) useLegacyNdkSymbolUpload.set(other.useLegacyNdkSymbolUpload) bugsnagNdkVersion.set(other.bugsnagNdkVersion) + variantName.set(other.variantName) } private fun executableName(cmdName: String): String { @@ -169,18 +173,17 @@ abstract class NdkToolchain { } private fun getBugsnagAndroidNDKVersion(variant: ApkVariant): String? { - try { + return try { val bugsnagAndroidCoreVersion = variant.compileConfiguration.resolvedConfiguration.resolvedArtifacts .find { it.moduleVersion.id.group == "com.bugsnag" && it.moduleVersion.id.name == "bugsnag-plugin-android-ndk" } ?.moduleVersion?.id?.version - ?: return null - return bugsnagAndroidCoreVersion + bugsnagAndroidCoreVersion } catch (e: Exception) { - return null + null } } @@ -196,7 +199,14 @@ abstract class NdkToolchain { ndkToolchain.baseDir.set(ndkToolchainDirectoryFor(project)) ndkToolchain.useLegacyNdkSymbolUpload.set(useLegacyNdkSymbolUpload) ndkToolchain.overrides.set(overrides) - ndkToolchain.bugsnagNdkVersion.set(project.provider { getBugsnagAndroidNDKVersion(variant) }) + + // we disable the bugsnag-android version check if Unity is enabled otherwise we end up with mutation errors + if (!BugsnagGenerateUnitySoMappingTask + .isUnityLibraryUploadEnabled(bugsnag, project.extensions.findByType(BaseExtension::class.java)!!) + ) { + ndkToolchain.bugsnagNdkVersion.set(project.provider { getBugsnagAndroidNDKVersion(variant) }) + } + ndkToolchain.variantName.set(variant.name) return ndkToolchain } diff --git a/src/test/kotlin/com/bugsnag/android/gradle/PluginExtensionTest.kt b/src/test/kotlin/com/bugsnag/android/gradle/PluginExtensionTest.kt index 61b1c2bf..8995a7dc 100644 --- a/src/test/kotlin/com/bugsnag/android/gradle/PluginExtensionTest.kt +++ b/src/test/kotlin/com/bugsnag/android/gradle/PluginExtensionTest.kt @@ -79,7 +79,7 @@ class PluginExtensionTest { // ndk/unity upload defaults to false val plugin = proj.plugins.findPlugin(BugsnagPlugin::class.java)!! - assertFalse(plugin.isUnityLibraryUploadEnabled(bugsnag, android)) + assertFalse(BugsnagGenerateUnitySoMappingTask.isUnityLibraryUploadEnabled(bugsnag, android)) assertFalse(plugin.isNdkUploadEnabled(bugsnag, android)) assertFalse(plugin.isReactNativeUploadEnabled(bugsnag)) assertEquals(emptyList(), plugin.getSharedObjectSearchPaths(proj, bugsnag, android)) @@ -143,7 +143,7 @@ class PluginExtensionTest { // ndk/unity upload overridden to true val plugin = proj.plugins.findPlugin(BugsnagPlugin::class.java)!! - assertTrue(plugin.isUnityLibraryUploadEnabled(bugsnag, android)) + assertTrue(BugsnagGenerateUnitySoMappingTask.isUnityLibraryUploadEnabled(bugsnag, android)) assertTrue(plugin.isNdkUploadEnabled(bugsnag, android)) assertTrue(plugin.isReactNativeUploadEnabled(bugsnag)) assertEquals("http://localhost:1234", bugsnag.endpoint.get()) @@ -168,7 +168,7 @@ class PluginExtensionTest { `when`(cmake.path).thenReturn(File("/users/sdk/cmake")) // ndk/unity upload overridden to true - assertTrue(plugin.isUnityLibraryUploadEnabled(bugsnag, android)) + assertTrue(BugsnagGenerateUnitySoMappingTask.isUnityLibraryUploadEnabled(bugsnag, android)) assertTrue(plugin.isNdkUploadEnabled(bugsnag, android)) val expected = listOf( File(proj.projectDir, "src/main/jniLibs"), @@ -198,7 +198,7 @@ class PluginExtensionTest { `when`(aaptOptions.noCompress).thenReturn(mutableListOf(".unity3d")) // ndk/unity uploads overridden to true - assertTrue(plugin.isUnityLibraryUploadEnabled(bugsnag, android)) + assertTrue(BugsnagGenerateUnitySoMappingTask.isUnityLibraryUploadEnabled(bugsnag, android)) assertTrue(plugin.isNdkUploadEnabled(bugsnag, android)) val expected = listOf( File(proj.projectDir, "src/main/jniLibs"), diff --git a/src/test/kotlin/com/bugsnag/android/gradle/internal/NdkToolchainTest.kt b/src/test/kotlin/com/bugsnag/android/gradle/internal/NdkToolchainTest.kt index 8072de48..e84005f3 100644 --- a/src/test/kotlin/com/bugsnag/android/gradle/internal/NdkToolchainTest.kt +++ b/src/test/kotlin/com/bugsnag/android/gradle/internal/NdkToolchainTest.kt @@ -89,6 +89,7 @@ private class TestNdkToolchainImpl( override val useLegacyNdkSymbolUpload: Property = Mockito.mock(Property::class.java) as Property override val overrides: MapProperty = Mockito.mock(MapProperty::class.java) as MapProperty override val bugsnagNdkVersion: Property = Mockito.mock(Property::class.java) as Property + override val variantName: Property = Mockito.mock(Property::class.java) as Property init { whenMock(this.baseDir.get()).thenReturn(baseDir) From b1309f4d797b35329262001698b6a8996ab1d577 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 31 Oct 2022 10:58:00 +0000 Subject: [PATCH 07/12] fix: overwrite existing rescued Hermes source bundles instead of failing --- CHANGELOG.md | 7 +++++++ .../bugsnag/android/gradle/BugsnagUploadJsSourceMapTask.kt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4ad5b3d..c36cce1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## TBD + +# Bug Fixes + +* Fix FileAlreadyExistsException errors when building ReactNative projects with Hermes + [#482](https://github.com/bugsnag/bugsnag-android-gradle-plugin/pull/482) + ## 7.3.1 (2022-10-05) * Fixed a bug where using ndkBuild generated empty some mapping files which could not be uploaded diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadJsSourceMapTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadJsSourceMapTask.kt index 2bdbfa77..2fdd0f1f 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadJsSourceMapTask.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadJsSourceMapTask.kt @@ -196,7 +196,7 @@ open class BugsnagUploadJsSourceMapTask @Inject constructor( actions.add( indexToInsert, Action { - rnSourceBundle.copyTo(rnRescuedSourceBundle) + rnSourceBundle.copyTo(rnRescuedSourceBundle, overwrite = true) } ) From db3307848a63717e46f10602a5b18021a6e4e00b Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 7 Nov 2022 16:32:15 +0000 Subject: [PATCH 08/12] fix: merged the upload tasks to avoid configuring AGP early --- detekt-baseline.xml | 1 + .../bugsnag/android/gradle/BugsnagPlugin.kt | 23 +- .../gradle/BugsnagUploadSharedObjectTask.kt | 219 ------------------ .../android/gradle/BugsnagUploadSoSymTask.kt | 76 +++++- 4 files changed, 80 insertions(+), 239 deletions(-) delete mode 100644 src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSharedObjectTask.kt diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 7435c5bc..6b6325ec 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -5,6 +5,7 @@ ComplexCondition:AndroidManifestParser.kt$AndroidManifestParser$apiKey == null || "" == apiKey || versionCode == null || buildUuid == null || versionName == null || applicationId == null ComplexCondition:BugsnagPlugin.kt$BugsnagPlugin$!jvmMinificationEnabled && !ndkEnabled && !unityEnabled && !reactNativeEnabled LongParameterList:BugsnagGenerateNdkSoMappingTask.kt$BugsnagGenerateNdkSoMappingTask.Companion$( project: Project, variant: BaseVariant, output: ApkVariantOutput, ndk: NdkToolchain, searchPaths: List<File>, soMappingOutputPath: String ) + LongParameterList:BugsnagUploadSoSymTask.kt$BugsnagUploadSoSymTask.Companion$( project: Project, variant: BaseVariantOutput, ndkToolchain: NdkToolchain, uploadType: UploadType, generateTaskProvider: TaskProvider<out AbstractSoMappingTask>, httpClientHelperProvider: Provider<out BugsnagHttpClientHelper>, ndkUploadClientProvider: Provider<out UploadRequestClient>, ) MagicNumber:BugsnagPluginExtension.kt$BugsnagPluginExtension$60000 MagicNumber:BugsnagReleasesTask.kt$BugsnagReleasesTask$200 MagicNumber:MappingFileProvider.kt$9 diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt index 29403778..6cca8cb1 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagPlugin.kt @@ -257,21 +257,11 @@ class BugsnagPlugin : Plugin { else -> null } val uploadNdkMappingProvider = when { - ndkEnabled && generateNdkMappingProvider != null - && ndkToolchain.preferredMappingTool() == NdkToolchain.MappingTool.OBJDUMP -> { - BugsnagUploadSharedObjectTask.registerUploadNdkTask( - project, - output, - httpClientHelperProvider, - ndkUploadClientProvider, - generateNdkMappingProvider, - ndkSoMappingOutput - ) - } - ndkEnabled && generateNdkMappingProvider != null -> BugsnagUploadSoSymTask.register( project, output, + ndkToolchain, + BugsnagUploadSoSymTask.UploadType.NDK, generateNdkMappingProvider, httpClientHelperProvider, ndkUploadClientProvider @@ -296,13 +286,14 @@ class BugsnagPlugin : Plugin { } val uploadUnityMappingProvider = when { unityEnabled && generateUnityMappingProvider != null -> { - BugsnagUploadSharedObjectTask.registerUploadUnityTask( + BugsnagUploadSoSymTask.register( project, output, - httpClientHelperProvider, - unityUploadClientProvider, + ndkToolchain, + BugsnagUploadSoSymTask.UploadType.UNITY, generateUnityMappingProvider, - unityMappingDir + httpClientHelperProvider, + unityUploadClientProvider ) } diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSharedObjectTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSharedObjectTask.kt deleted file mode 100644 index 3122c81c..00000000 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSharedObjectTask.kt +++ /dev/null @@ -1,219 +0,0 @@ -package com.bugsnag.android.gradle - -import com.android.build.gradle.api.BaseVariantOutput -import com.bugsnag.android.gradle.internal.BugsnagHttpClientHelper -import com.bugsnag.android.gradle.internal.UploadRequestClient -import com.bugsnag.android.gradle.internal.dependsOn -import com.bugsnag.android.gradle.internal.forBuildOutput -import com.bugsnag.android.gradle.internal.intermediateForNdkSoRequest -import com.bugsnag.android.gradle.internal.intermediateForUnitySoRequest -import com.bugsnag.android.gradle.internal.md5HashCode -import com.bugsnag.android.gradle.internal.property -import com.bugsnag.android.gradle.internal.taskNameForUploadNdkMapping -import com.bugsnag.android.gradle.internal.taskNameForUploadUnityMapping -import okhttp3.RequestBody.Companion.asRequestBody -import org.gradle.api.DefaultTask -import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.RegularFile -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.Property -import org.gradle.api.provider.Provider -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.Internal -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction -import org.gradle.api.tasks.TaskProvider -import java.io.File -import javax.inject.Inject - -/** - * Task that uploads shared object mapping files to Bugsnag. - */ -internal open class BugsnagUploadSharedObjectTask @Inject constructor( - objects: ObjectFactory, -) : DefaultTask(), AndroidManifestInfoReceiver, BugsnagFileUploadTask { - - enum class UploadType(private val path: String, val uploadKey: String) { - NDK("so-symbol", "soSymbolFile"), - UNITY("so-symbol-table", "soSymbolTableFile"); - - fun endpoint(base: String): String { - return "$base/$path" - } - } - - init { - group = BugsnagPlugin.GROUP_NAME - description = "Uploads SO mapping files to Bugsnag" - } - - @get:InputFile - override val manifestInfo: RegularFileProperty = objects.fileProperty() - - @get:Internal - internal val uploadRequestClient: Property = objects.property() - - @get:Internal - override val httpClientHelper: Property = objects.property() - - @Input - val projectRoot: Property = objects.property() - - @get:OutputFile - val requestOutputFile: RegularFileProperty = objects.fileProperty() - - @get:InputDirectory - val intermediateOutputDir: DirectoryProperty = objects.directoryProperty() - - @get:Input - override val failOnUploadError: Property = objects.property() - - @get:Input - override val overwrite: Property = objects.property() - - @get:Input - override val endpoint: Property = objects.property() - - @get:Input - override val retryCount: Property = objects.property() - - @get:Input - override val timeoutMillis: Property = objects.property() - - @get:Input - val uploadType: Property = objects.property() - - @TaskAction - fun upload() { - val rootDir = intermediateOutputDir.asFile.get() - val abiDirs = rootDir.listFiles().filter { it.isDirectory } - logger.info("Bugsnag: Found shared object files for upload: $abiDirs") - - abiDirs.forEach { abiDir -> - val arch = abiDir.name - abiDir.listFiles() - .filter { it.extension == "gz" } - .forEach { sharedObjectFile -> - uploadSymbols(sharedObjectFile, arch) - } - } - } - - /** - * Uploads the given shared object mapping information - * @param mappingFile the file to upload - * @param arch the arch that is being uploaded - */ - private fun uploadSymbols(mappingFile: File, arch: String) { - // 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) { - logger.warn("Bugsnag: Skipping upload of empty/invalid mapping file: $mappingFile") - return - } - val sharedObjectName = mappingFile.nameWithoutExtension - val requestEndpoint = uploadType.get().endpoint(endpoint.get()) - val soUploadKey = uploadType.get().uploadKey - - val request = BugsnagMultiPartUploadRequest.from(this, requestEndpoint) - val manifestInfo = parseManifestInfo() - val mappingFileHash = mappingFile.md5HashCode() - val response = uploadRequestClient.get().makeRequestIfNeeded(manifestInfo, mappingFileHash) { - logger.lifecycle( - "Bugsnag: Uploading SO mapping file for $sharedObjectName ($arch) from $mappingFile" - ) - request.uploadMultipartEntity(retryCount.get()) { builder -> - builder.addAndroidManifestInfo(manifestInfo) - builder.addFormDataPart(soUploadKey, mappingFile.name, mappingFile.asRequestBody()) - builder.addFormDataPart("arch", arch) - builder.addFormDataPart("sharedObjectName", sharedObjectName) - builder.addFormDataPart("projectRoot", projectRoot.get()) - } - } - requestOutputFile.asFile.get().writeText(response) - } - - companion object { - private const val VALID_SO_FILE_THRESHOLD = 1024 - - @Suppress("LongParameterList") - fun registerUploadNdkTask( - project: Project, - output: BaseVariantOutput, - httpClientHelperProvider: Provider, - ndkUploadClientProvider: Provider, - generateTaskProvider: TaskProvider, - soMappingOutputDir: String - ): TaskProvider { - return register( - project, - generateTaskProvider, - httpClientHelperProvider, - BugsnagManifestUuidTask.manifestInfoForOutput(project, output), - ndkUploadClientProvider, - taskNameForUploadNdkMapping(output), - intermediateForNdkSoRequest(project, output), - UploadType.NDK, - soMappingOutputDir - ).dependsOn(BugsnagManifestUuidTask.forBuildOutput(project, output)) - } - - @Suppress("LongParameterList") - fun registerUploadUnityTask( - project: Project, - output: BaseVariantOutput, - httpClientHelperProvider: Provider, - ndkUploadClientProvider: Provider, - generateTaskProvider: TaskProvider, - mappingFileOutputDir: String - ): TaskProvider { - return register( - project, - generateTaskProvider, - httpClientHelperProvider, - BugsnagManifestUuidTask.manifestInfoForOutput(project, output), - ndkUploadClientProvider, - taskNameForUploadUnityMapping(output), - intermediateForUnitySoRequest(project, output), - UploadType.UNITY, - mappingFileOutputDir - ).dependsOn(BugsnagManifestUuidTask.forBuildOutput(project, output)) - } - - @Suppress("LongParameterList") - fun register( - project: Project, - generateTaskProvider: TaskProvider, - httpClientHelperProvider: Provider, - manifestInfoProvider: Provider, - ndkUploadClientProvider: Provider, - taskName: String, - requestOutputFile: Provider, - uploadType: UploadType, - intermediateOutputPath: String - ): TaskProvider { - val bugsnag = project.extensions.getByType(BugsnagPluginExtension::class.java) - // Create a Bugsnag task to upload NDK mapping file(s) - return project.tasks.register(taskName, BugsnagUploadSharedObjectTask::class.java) { task -> - // upload task requires SO mapping generation to occur first - task.dependsOn(generateTaskProvider) - task.usesService(httpClientHelperProvider) - task.usesService(ndkUploadClientProvider) - - task.requestOutputFile.set(requestOutputFile) - task.uploadType.set(uploadType) - task.projectRoot.set(bugsnag.projectRoot.getOrElse(project.projectDir.toString())) - task.httpClientHelper.set(httpClientHelperProvider) - task.manifestInfo.set(manifestInfoProvider) - task.uploadRequestClient.set(ndkUploadClientProvider) - task.intermediateOutputDir.set(project.layout.buildDirectory.dir(intermediateOutputPath)) - task.configureWith(bugsnag) - } - } - } -} diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt index 339b95fc..dd2333f0 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt @@ -3,6 +3,7 @@ package com.bugsnag.android.gradle import com.android.build.gradle.api.BaseVariantOutput import com.bugsnag.android.gradle.internal.AbstractSoMappingTask import com.bugsnag.android.gradle.internal.BugsnagHttpClientHelper +import com.bugsnag.android.gradle.internal.NdkToolchain import com.bugsnag.android.gradle.internal.UploadRequestClient import com.bugsnag.android.gradle.internal.md5HashCode import com.bugsnag.android.gradle.internal.taskNameSuffix @@ -17,6 +18,8 @@ import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.StopExecutionException import org.gradle.api.tasks.TaskAction @@ -24,7 +27,7 @@ import org.gradle.api.tasks.TaskProvider import java.io.File import java.net.HttpURLConnection.HTTP_NOT_FOUND -abstract class BugsnagUploadSoSymTask : DefaultTask(), AndroidManifestInfoReceiver, BugsnagFileUploadTask { +internal abstract class BugsnagUploadSoSymTask : DefaultTask(), AndroidManifestInfoReceiver, BugsnagFileUploadTask { @get:InputDirectory abstract val symbolFilesDir: DirectoryProperty @@ -35,6 +38,13 @@ abstract class BugsnagUploadSoSymTask : DefaultTask(), AndroidManifestInfoReceiv @get:Input abstract val projectRoot: Property + @get:Nested + abstract val ndkToolchain: Property + + @get:Input + @get:Optional + abstract val uploadType: Property + @get:Internal internal abstract val uploadRequestClient: Property @@ -47,9 +57,23 @@ abstract class BugsnagUploadSoSymTask : DefaultTask(), AndroidManifestInfoReceiv fun upload() { val rootDir = symbolFilesDir.asFile.get() logger.info("Bugsnag: Found shared object files for upload: $rootDir") - rootDir.walkTopDown() - .filter { it.isFile && it.extension == "gz" && it.length() >= VALID_SO_FILE_THRESHOLD } - .forEach { uploadSymbols(it) } + + if (ndkToolchain.get().preferredMappingTool() == NdkToolchain.MappingTool.OBJDUMP) { + // uploadType == objdump + val abiDirs = rootDir.listFiles().filter { it.isDirectory } + abiDirs.forEach { abiDir -> + val arch = abiDir.name + abiDir.listFiles() + .filter { it.extension == "gz" } + .forEach { sharedObjectFile -> + uploadObjdump(sharedObjectFile, arch) + } + } + } else { + rootDir.walkTopDown() + .filter { it.isFile && it.extension == "gz" && it.length() >= VALID_SO_FILE_THRESHOLD } + .forEach { uploadSymbols(it) } + } } /** @@ -96,6 +120,36 @@ abstract class BugsnagUploadSoSymTask : DefaultTask(), AndroidManifestInfoReceiv requestOutputFile.asFile.get().writeText(response) } + private fun uploadObjdump(mappingFile: File, arch: String) { + // 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) { + logger.warn("Bugsnag: Skipping upload of empty/invalid mapping file: $mappingFile") + return + } + + val sharedObjectName = mappingFile.nameWithoutExtension + val requestEndpoint = uploadType.get().endpoint(endpoint.get()) + val soUploadKey = uploadType.get().uploadKey + + val request = BugsnagMultiPartUploadRequest.from(this, requestEndpoint) + val manifestInfo = parseManifestInfo() + val mappingFileHash = mappingFile.md5HashCode() + val response = uploadRequestClient.get().makeRequestIfNeeded(manifestInfo, mappingFileHash) { + logger.lifecycle( + "Bugsnag: Uploading SO mapping file for $sharedObjectName ($arch) from $mappingFile" + ) + request.uploadMultipartEntity(retryCount.get()) { builder -> + builder.addAndroidManifestInfo(manifestInfo) + builder.addFormDataPart(soUploadKey, mappingFile.name, mappingFile.asRequestBody()) + builder.addFormDataPart("arch", arch) + builder.addFormDataPart("sharedObjectName", sharedObjectName) + builder.addFormDataPart("projectRoot", projectRoot.get()) + } + } + requestOutputFile.asFile.get().writeText(response) + } + companion object { private const val ENDPOINT_SUFFIX = "/ndk-symbol" @@ -112,6 +166,8 @@ abstract class BugsnagUploadSoSymTask : DefaultTask(), AndroidManifestInfoReceiv fun register( project: Project, variant: BaseVariantOutput, + ndkToolchain: NdkToolchain, + uploadType: UploadType, generateTaskProvider: TaskProvider, httpClientHelperProvider: Provider, ndkUploadClientProvider: Provider, @@ -124,6 +180,9 @@ abstract class BugsnagUploadSoSymTask : DefaultTask(), AndroidManifestInfoReceiv task.endpoint.set(bugsnag.endpoint) + task.uploadType.set(uploadType) + task.ndkToolchain.set(ndkToolchain) + task.manifestInfo.set(BugsnagManifestUuidTask.manifestInfoForOutput(project, variant)) task.symbolFilesDir.set(generateTaskProvider.flatMap { it.outputDirectory }) task.requestOutputFile.set(requestOutputFileFor(project, variant)) @@ -136,4 +195,13 @@ abstract class BugsnagUploadSoSymTask : DefaultTask(), AndroidManifestInfoReceiv } } } + + enum class UploadType(private val path: String, val uploadKey: String) { + NDK("so-symbol", "soSymbolFile"), + UNITY("so-symbol-table", "soSymbolTableFile"); + + fun endpoint(base: String): String { + return "$base/$path" + } + } } From 8d07a303bd363fdb16a3f97bea9bb12726fee8a2 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 8 Nov 2022 14:09:05 +0000 Subject: [PATCH 09/12] dep: updated compile / target SDK version on react-native 0.65 fixture --- .../android/app/src/main/AndroidManifest.xml | 1 + .../rn-monorepo/abc/android/build.gradle | 21 ++++++++++++++----- .../android/app/src/main/AndroidManifest.xml | 1 + .../rn-monorepo/xyz/android/build.gradle | 21 ++++++++++++++----- .../android/app/src/main/AndroidManifest.xml | 1 + features/fixtures/rn065/android/build.gradle | 21 ++++++++++++++----- 6 files changed, 51 insertions(+), 15 deletions(-) diff --git a/features/fixtures/rn-monorepo/abc/android/app/src/main/AndroidManifest.xml b/features/fixtures/rn-monorepo/abc/android/app/src/main/AndroidManifest.xml index ad008b47..191aba6e 100644 --- a/features/fixtures/rn-monorepo/abc/android/app/src/main/AndroidManifest.xml +++ b/features/fixtures/rn-monorepo/abc/android/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ diff --git a/features/fixtures/rn-monorepo/abc/android/build.gradle b/features/fixtures/rn-monorepo/abc/android/build.gradle index 87ebf1fc..9b8c8841 100644 --- a/features/fixtures/rn-monorepo/abc/android/build.gradle +++ b/features/fixtures/rn-monorepo/abc/android/build.gradle @@ -4,8 +4,8 @@ buildscript { ext { buildToolsVersion = "30.0.2" minSdkVersion = 21 - compileSdkVersion = 30 - targetSdkVersion = 30 + compileSdkVersion = 31 + targetSdkVersion = 31 } repositories { mavenCentral() @@ -26,9 +26,20 @@ allprojects { repositories { mavenCentral() mavenLocal() - maven { - // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm - url("$rootDir/../../node_modules/react-native/android") + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + exclusiveContent { + // We get React Native's Android binaries exclusively through npm, + // from a local Maven repo inside node_modules/react-native/. + // (The use of exclusiveContent prevents looking elsewhere like Maven Central + // and potentially getting a wrong version.) + filter { + includeGroup "com.facebook.react" + } + forRepository { + maven { + url("$rootDir/../../node_modules/react-native/android") + } + } } maven { // Android JSC is installed from npm diff --git a/features/fixtures/rn-monorepo/xyz/android/app/src/main/AndroidManifest.xml b/features/fixtures/rn-monorepo/xyz/android/app/src/main/AndroidManifest.xml index 93dd554d..e5647203 100644 --- a/features/fixtures/rn-monorepo/xyz/android/app/src/main/AndroidManifest.xml +++ b/features/fixtures/rn-monorepo/xyz/android/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ diff --git a/features/fixtures/rn-monorepo/xyz/android/build.gradle b/features/fixtures/rn-monorepo/xyz/android/build.gradle index ed5a5684..8f622085 100644 --- a/features/fixtures/rn-monorepo/xyz/android/build.gradle +++ b/features/fixtures/rn-monorepo/xyz/android/build.gradle @@ -4,8 +4,8 @@ buildscript { ext { buildToolsVersion = "29.0.2" minSdkVersion = 16 - compileSdkVersion = 29 - targetSdkVersion = 29 + compileSdkVersion = 31 + targetSdkVersion = 31 } repositories { google() @@ -21,9 +21,20 @@ buildscript { allprojects { repositories { mavenLocal() - maven { - // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm - url("$rootDir/../node_modules/react-native/android") + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + exclusiveContent { + // We get React Native's Android binaries exclusively through npm, + // from a local Maven repo inside node_modules/react-native/. + // (The use of exclusiveContent prevents looking elsewhere like Maven Central + // and potentially getting a wrong version.) + filter { + includeGroup "com.facebook.react" + } + forRepository { + maven { + url("$rootDir/../node_modules/react-native/android") + } + } } maven { // Android JSC is installed from npm diff --git a/features/fixtures/rn065/android/app/src/main/AndroidManifest.xml b/features/fixtures/rn065/android/app/src/main/AndroidManifest.xml index afcb33f4..78032638 100644 --- a/features/fixtures/rn065/android/app/src/main/AndroidManifest.xml +++ b/features/fixtures/rn065/android/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ diff --git a/features/fixtures/rn065/android/build.gradle b/features/fixtures/rn065/android/build.gradle index d1afbe73..cda34a95 100644 --- a/features/fixtures/rn065/android/build.gradle +++ b/features/fixtures/rn065/android/build.gradle @@ -4,8 +4,8 @@ buildscript { ext { buildToolsVersion = "30.0.2" minSdkVersion = 21 - compileSdkVersion = 30 - targetSdkVersion = 30 + compileSdkVersion = 31 + targetSdkVersion = 31 ndkVersion = "20.1.5948944" } repositories { @@ -29,9 +29,20 @@ allprojects { repositories { mavenCentral() mavenLocal() - maven { - // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm - url("$rootDir/../node_modules/react-native/android") + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + exclusiveContent { + // We get React Native's Android binaries exclusively through npm, + // from a local Maven repo inside node_modules/react-native/. + // (The use of exclusiveContent prevents looking elsewhere like Maven Central + // and potentially getting a wrong version.) + filter { + includeGroup "com.facebook.react" + } + forRepository { + maven { + url("$rootDir/../node_modules/react-native/android") + } + } } maven { // Android JSC is installed from npm From 1d05182a1977d21c52ac0a31ff29e57695f36631 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 9 Nov 2022 08:23:45 +0000 Subject: [PATCH 10/12] fix(task): changed BugsnagUploadSoSymTask to be named based on the upload type --- .../com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt | 9 ++++++--- .../com/bugsnag/android/gradle/internal/BugsnagTasks.kt | 6 ------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt index dd2333f0..7d3067b0 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/BugsnagUploadSoSymTask.kt @@ -155,8 +155,8 @@ internal abstract class BugsnagUploadSoSymTask : DefaultTask(), AndroidManifestI private const val VALID_SO_FILE_THRESHOLD = 1024 - fun taskNameFor(variant: BaseVariantOutput) = - "uploadBugsnag${variant.baseName.capitalize()}Symbols" + fun taskNameFor(variant: BaseVariantOutput, uploadType: UploadType) = + "uploadBugsnag${uploadType.name.toLowerCase().capitalize()}${variant.baseName.capitalize()}Mapping" internal fun requestOutputFileFor(project: Project, output: BaseVariantOutput): Provider { val path = "intermediates/bugsnag/requests/symFor${output.taskNameSuffix()}.json" @@ -173,7 +173,10 @@ internal abstract class BugsnagUploadSoSymTask : DefaultTask(), AndroidManifestI ndkUploadClientProvider: Provider, ): TaskProvider { val bugsnag = project.extensions.getByType(BugsnagPluginExtension::class.java) - return project.tasks.register(taskNameFor(variant), BugsnagUploadSoSymTask::class.java) { task -> + return project.tasks.register( + taskNameFor(variant, uploadType), + BugsnagUploadSoSymTask::class.java + ) { task -> task.dependsOn(generateTaskProvider) task.usesService(httpClientHelperProvider) task.usesService(ndkUploadClientProvider) diff --git a/src/main/kotlin/com/bugsnag/android/gradle/internal/BugsnagTasks.kt b/src/main/kotlin/com/bugsnag/android/gradle/internal/BugsnagTasks.kt index dcf81773..6adf4dda 100644 --- a/src/main/kotlin/com/bugsnag/android/gradle/internal/BugsnagTasks.kt +++ b/src/main/kotlin/com/bugsnag/android/gradle/internal/BugsnagTasks.kt @@ -9,12 +9,6 @@ internal const val TASK_JNI_LIBS = "bugsnagInstallJniLibsTask" internal fun taskNameForUploadJvmMapping(output: BaseVariantOutput) = "uploadBugsnag${output.taskNameSuffix()}Mapping" -internal fun taskNameForUploadNdkMapping(output: BaseVariantOutput) = - "uploadBugsnagNdk${output.taskNameSuffix()}Mapping" - -internal fun taskNameForUploadUnityMapping(output: BaseVariantOutput) = - "uploadBugsnagUnity${output.taskNameSuffix()}Mapping" - internal fun taskNameForUploadRelease(output: BaseVariantOutput) = "bugsnagRelease${output.taskNameSuffix()}Task" From 318e0e7ab82f1647127a5f6fc866f32cefca8d05 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 9 Nov 2022 18:16:37 +0000 Subject: [PATCH 11/12] doc: added `objcopy` functionality to CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c36cce1f..cb8b49e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * Fix FileAlreadyExistsException errors when building ReactNative projects with Hermes [#482](https://github.com/bugsnag/bugsnag-android-gradle-plugin/pull/482) +* Support using objcopy instead of objdump to extract native symbols (when supported by the current NDK). + [#484](https://github.com/bugsnag/bugsnag-android-gradle-plugin/pull/484) ## 7.3.1 (2022-10-05) From 794eda25f3cdd3101c96411d2dfeaf89a282630d Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 10 Nov 2022 08:27:46 +0000 Subject: [PATCH 12/12] v7.4.0 --- CHANGELOG.md | 11 +++++++---- gradle.properties | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb8b49e3..c1d893d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,15 @@ -## TBD +## 7.4.0 (2022-11-10) -# Bug Fixes +### Enhancements -* Fix FileAlreadyExistsException errors when building ReactNative projects with Hermes - [#482](https://github.com/bugsnag/bugsnag-android-gradle-plugin/pull/482) * Support using objcopy instead of objdump to extract native symbols (when supported by the current NDK). [#484](https://github.com/bugsnag/bugsnag-android-gradle-plugin/pull/484) +### Bug Fixes + +* Fix FileAlreadyExistsException errors when building ReactNative projects with Hermes + [#482](https://github.com/bugsnag/bugsnag-android-gradle-plugin/pull/482) + ## 7.3.1 (2022-10-05) * Fixed a bug where using ndkBuild generated empty some mapping files which could not be uploaded diff --git a/gradle.properties b/gradle.properties index a7c28f92..7d192e18 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ POM_NAME=Bugsnag Android Gradle Plugin POM_ARTIFACT_ID=bugsnag-android-gradle-plugin POM_PACKAGING=jar GROUP=com.bugsnag -VERSION_NAME=7.3.1 +VERSION_NAME=7.4.0 POM_DESCRIPTION=Gradle plugin to automatically upload ProGuard mapping files to Bugsnag. POM_URL=https://github.com/bugsnag/bugsnag-android-gradle-plugin/ POM_SCM_URL=https://github.com/bugsnag/bugsnag-android-gradle-plugin/