All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bugsnag.android.gradle.BugsnagUploadNdkTask.groovy Maven / Gradle / Ivy

There is a newer version: 8.1.0
Show newest version
package com.bugsnag.android.gradle

import static groovy.io.FileType.FILES

import com.android.build.gradle.tasks.ProcessAndroidResources

import com.android.build.gradle.api.BaseVariantOutput
import com.android.build.gradle.tasks.ExternalNativeBuildTask
import org.apache.http.entity.mime.MultipartEntity
import org.apache.http.entity.mime.content.FileBody
import org.apache.http.entity.mime.content.StringBody
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.Project
import org.gradle.api.tasks.TaskAction

import java.util.zip.GZIPOutputStream

/**
 Task to upload shared object 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 shared object files are generated, so
 it is usually safe to have this be the absolute last task executed during
 a build.
 */
class BugsnagUploadNdkTask extends BugsnagMultiPartUploadTask {

    private static final int VALID_SO_FILE_THRESHOLD = 1024

    File symbolPath
    String variantName
    File projectDir
    File rootDir
    String sharedObjectPath

    BugsnagUploadNdkTask() {
        super()
        this.description = "Generates and uploads the NDK mapping file(s) to Bugsnag"
    }

    @TaskAction
    void upload() {
        super.readManifestFile()
        symbolPath = findSymbolPath(variantOutput)
        project.logger.lifecycle("Symbolpath: ${symbolPath}")

        boolean sharedObjectFound = false
        Closure processor = { String arch, File sharedObject ->
            project.logger.lifecycle("Found shared object file (${arch}) ${sharedObject}")
            sharedObjectFound = true

            File outputFile = generateSymbolsForSharedObject(sharedObject, arch)
            if (outputFile) {
                uploadSymbols(outputFile, arch, sharedObject.name)
            }
        }

        for (ExternalNativeBuildTask task : resolveExternalNativeBuildTasks()) {
            File objFolder = task.objFolder
            File soFolder = task.soFolder
            findSharedObjectFiles(objFolder, processor)
            findSharedObjectFiles(soFolder, processor)
        }

        if (sharedObjectPath) {
            File file = new File(projectDir.path, sharedObjectPath)
            findSharedObjectFiles(file, processor)
        }
        if (!sharedObjectFound) {
            project.logger.error("No shared objects found")
        }
    }

    private Collection resolveExternalNativeBuildTasks() {
        try {
            return variant.externalNativeBuildProviders
                .stream()
                .map({ it.get() })
                .collect()
        } catch (Throwable ignored) {
            return variant.externalNativeBuildTasks
        }
    }

    private static File findSymbolPath(BaseVariantOutput variantOutput) {
        ProcessAndroidResources resources = resolveProcessAndroidResources(variantOutput)
        File symbolPath = resources.textSymbolOutputFile

        if (symbolPath == null) {
            throw new IllegalStateException("Could not find symbol path")
        }
        symbolPath
    }

    private static ProcessAndroidResources resolveProcessAndroidResources(BaseVariantOutput variantOutput) {
        try {
            return variantOutput.processResourcesProvider.get()
        } catch (Throwable ignored) {
            return variantOutput.processResources
        }
    }
    /**
     * Searches the subdirectories of a given path and executes a block on
     * any shared object files
     * @param path The parent path to search. Each subdirectory should
     *                  represent an architecture
     * @param processor a closure to execute on each parent directory and shared
     *                  object file
     */
    void findSharedObjectFiles(File dir, Closure processor) {
        project.logger.lifecycle("Checking dir: ${dir}")

        if (dir.exists()) {
            dir.eachDir { arch ->
                arch.eachFileMatch FILES, ~/.*\.so$/, { processor(arch.name, it) }
            }
        }
    }

    /**
     * Uses objdump to create a symbols file for the given shared object file
     * @param sharedObject the shared object file
     * @param arch the arch of the file
     * @return the output file location, or null on error
     */
    File generateSymbolsForSharedObject(File sharedObject, String arch) {
        // Get the path the version of objdump to use to get symbols
        File objDumpPath = getObjDumpExecutable(arch)
        if (objDumpPath != null) {

            Reader outReader = null

            try {
                File outputDir = new File(project.buildDir, "bugsnag")

                if (!outputDir.exists()) {
                    outputDir.mkdir()
                }

                File outputFile = new File(outputDir, arch + ".gz")
                File errorOutputFile = new File(outputDir, arch + ".error.txt")
                project.logger.lifecycle("Creating symbol file at ${outputFile}")

                // Call objdump, redirecting output to the output file
                ProcessBuilder builder = new ProcessBuilder(objDumpPath.toString(),
                    "--dwarf=info", "--dwarf=rawline", sharedObject.toString())
                builder.redirectError(errorOutputFile)
                Process process = builder.start()

                // Output the file to a zip
                InputStream stdout = process.inputStream
                outputZipFile(stdout, outputFile)

                if (process.waitFor() == 0) {
                    return outputFile
                } else {
                    project.logger.error("failed to generate symbols for $arch, see "
                        + errorOutputFile.toString() + " for more details")
                    return null
                }
            } catch (Exception e) {
                project.logger.error("failed to generate symbols for $arch $e.message", e)
            } finally {
                if (outReader != null) {
                    outReader.close()
                }
            }
        } else {
            project.logger.error("Unable to upload NDK symbols: Could not find objdump location for " + arch)
        }
        null
    }

    /**
     * Outputs the contents of stdout into the gzip file output file
     *
     * @param stdout The input stream
     * @param outputFile The output file
     */
    static void outputZipFile(InputStream stdout, File outputFile) {
        GZIPOutputStream zipStream = null

        try {
            zipStream = new GZIPOutputStream(new FileOutputStream(outputFile))

            byte[] buffer = new byte[8192]
            int len
            while ((len = stdout.read(buffer)) != -1) {
                zipStream.write(buffer, 0, len)
            }

        } finally {
            if (zipStream != null) {
                zipStream.close()
            }

            stdout.close()
        }
    }

    /**
     * Uploads the given shared object mapping information
     * @param mappingFile the file to upload
     * @param arch the arch that is being uploaded
     * @param sharedObjectName the original shared object name
     */
    void uploadSymbols(File mappingFile, String arch, String sharedObjectName) {

        // a SO file may not contain debug info. if that's the case then the mapping file should be very small,
        // so we try and reject it here as otherwise the event-worker will reject it with a 400 status code.
        if (!mappingFile.exists() || mappingFile.length() < VALID_SO_FILE_THRESHOLD) {
            project.logger.warn("Skipping upload of empty/invalid mapping file: $mappingFile")
            return
        }

        MultipartEntity mpEntity = new MultipartEntity()
        mpEntity.addPart("soSymbolFile", new FileBody(mappingFile))
        mpEntity.addPart("arch", new StringBody(arch))
        mpEntity.addPart("sharedObjectName", new StringBody(sharedObjectName))

        String projectRoot = project.bugsnag.projectRoot ?: projectDir.toString()
        mpEntity.addPart("projectRoot", new StringBody(projectRoot))

        super.uploadMultipartEntity(mpEntity)
    }

    /**
     * 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
     */
    File getObjDumpExecutable(String arch) {
        try {
            String override = getObjDumpOverride(arch)
            File objDumpFile

            if (override != null) {
                objDumpFile = new File(override)
            } else {
                objDumpFile = findObjDump(project, arch)
            }

            if (!objDumpFile.exists() || !objDumpFile.canExecute()) {
                throw new IllegalStateException("Failed to find executable objdump at $objDumpFile")
            }
            return objDumpFile
        } catch (Throwable ex) {
            project.logger.error("Error attempting to calculate objdump location: " + ex.message)
        }
        null
    }

    private Object getObjDumpOverride(String arch) {
        Map paths = project.bugsnag.objdumpPaths
        paths != null ? paths[arch] : null
    }

    static File findObjDump(Project project, String arch) {
        Abi abi = Abi.findByName(arch)
        String ndkDir = project.android.ndkDirectory
        String osName = calculateOsName()

        if (abi == null) {
            throw new IllegalStateException("Failed to find ABI for $arch")
        }
        if (osName == null) {
            throw new IllegalStateException("Failed to calculate OS name")
        }
        calculateObjDumpLocation(ndkDir, abi, osName)
    }

    static File calculateObjDumpLocation(String ndkDir, Abi abi, String osName) {
        String executable = osName.startsWith("windows") ? "objdump.exe" : "objdump"
        new File("$ndkDir/toolchains/$abi.toolchainPrefix-4.9/prebuilt/" +
            "$osName/bin/$abi.objdumpPrefix-$executable")
    }

    static String calculateOsName() {
        if (Os.isFamily(Os.FAMILY_MAC)) {
            return "darwin-x86_64"
        } else if (Os.isFamily(Os.FAMILY_UNIX)) {
            return "linux-x86_64"
        } else if (Os.isFamily(Os.FAMILY_WINDOWS)) {
            return "x86" == System.getProperty("os.arch") ? "windows" : "windows-x86_64"
        } else {
            return null
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy