Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 188 additions & 31 deletions packages/react-native/android/upload-sourcemaps.gradle
Original file line number Diff line number Diff line change
@@ -1,48 +1,205 @@
import groovy.json.JsonBuilder
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters

import javax.inject.Inject

tasks.register("uploadSourceMapsToBacktrace") {
group = "backtrace"
description = "This task uploads source maps generated by the react-native builder to Backtrace based on the .backtracejsrc file."
abstract class BacktraceSourceMapUploadTask extends DefaultTask {
@Input
abstract Property<String> getVariantName()

doLast {
// Fetch the build variant (debug or release)
// Default to 'release' if not set
def buildVariant = project.hasProperty("buildVariant") ? project.buildVariant.toLowerCase() : "release"
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getSourceMap()

def debugIdPathEnv = System.getenv("DEBUG_ID_PATH")
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getDebugIdFile()

def debugIdFile = debugIdPathEnv ? file(debugIdPathEnv) : layout.buildDirectory
.dir("intermediates/sourcemaps/react/${buildVariant}/.backtrace-sourcemap-id")
.get()
.asFile
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getConfigFile()

if (!debugIdFile.exists()) {
println("Backtrace: Cannot find .backtrace-sourcemap-id file. Check if customSerializer has been set to Backtrace serializer in metro.config.js.")
@Internal
abstract DirectoryProperty getAppRoot()

@OutputFile
abstract RegularFileProperty getProcessedSourceMap()

@Inject
abstract ExecOperations getExecOperations()

BacktraceSourceMapUploadTask() {
outputs.upToDateWhen { task -> task.processedSourceMap.get().asFile.isFile() }
}

@TaskAction
void upload() {
def processed = processedSourceMap.get().asFile
processed.delete()

def variant = variantName.get()
def map = sourceMap.singleFile
def idFile = debugIdFile.singleFile
def config = configFile.singleFile
def debugId = idFile.isFile() ? idFile.getText("UTF-8").trim() : ""

if (!map.isFile()) {
logger.warn("Backtrace: Cannot find the ${variant} source map at ${map}. Hermes builds need -output-source-map in hermesFlags. Skipping the upload.")
return
}
if (!debugId) {
logger.warn("Backtrace: The ${variant} bundle has no debug id. Check that customSerializer is set to the Backtrace serializer in metro.config.js. Skipping the upload.")
return
}
if (!config.isFile()) {
logger.warn("Backtrace: Cannot find ${config}. Skipping the ${variant} source map upload.")
return
}

def sourcemapDestinationDirectory = layout.buildDirectory.dir("generated/sourcemaps/react").get().asFile
def mapFiles = fileTree(dir: sourcemapDestinationDirectory, include: '**/*.map').files
def mapFile = mapFiles ? mapFiles.iterator().next() : null
def mapData = new JsonSlurper().parse(map, "UTF-8")
mapData.debugId = debugId
processed.parentFile.mkdirs()
processed.setText(JsonOutput.toJson(mapData), "UTF-8")

if (!mapFile) {
println("Backtrace: Cannot find source map file.")
return
def command = ["npx", "--yes", "@backtrace/javascript-cli", "upload", "-p", processed.absolutePath]
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
command = ["cmd", "/c"] + command
}
def result = execOperations.exec { spec ->
spec.workingDir(appRoot.get().asFile)
spec.commandLine(command)
spec.ignoreExitValue = true
}
if (result.exitValue != 0) {
throw new GradleException("Backtrace: The ${variant} source map upload failed with exit code ${result.exitValue}. Pass -PbacktraceUploadSourceMaps=false to build without uploading.")
}
}
}

abstract class BacktraceDebugIdPathLock implements BuildService<BuildServiceParameters.None> {
}

def jsonSlurper = new JsonSlurper()
def mapData = jsonSlurper.parse(mapFile)
mapData.debugId = debugIdFile.text
def jsonBuilder = new JsonBuilder(mapData)
mapFile.text = jsonBuilder.toString()
def legacyTaskName = "uploadSourceMapsToBacktrace"

if (tasks.names.contains(legacyTaskName)) {
return
}

def mentionsLegacyTask = { Collection<String> taskPaths -> taskPaths.any { it.substring(it.lastIndexOf(":") + 1) == legacyTaskName } }
def legacyRequested = mentionsLegacyTask(gradle.startParameter.taskNames)
def uploadsEnabled = !mentionsLegacyTask(gradle.startParameter.excludedTaskNames) &&
!"false".equalsIgnoreCase(providers.gradleProperty("backtraceUploadSourceMaps").getOrElse("true"))
def legacyVariant = providers.gradleProperty("buildVariant").getOrElse("release")
def debugIdPath = providers.environmentVariable("DEBUG_ID_PATH").orNull
// Under the configuration cache, tasks of one project run in parallel, and every bundle writes the one DEBUG_ID_PATH file.
def debugIdPathLock = debugIdPath ? gradle.sharedServices.registerIfAbsent("backtraceDebugIdPath", BacktraceDebugIdPathLock) { it.maxParallelUsages.set(1) } : null
def sourceMapUploads = [:]
def uploadsByPackagingTask = [:]

def capitalize = { String name -> Character.toUpperCase(name.charAt(0)).toString() + name.substring(1) }
def sourceMapOf = { bundleTask -> new File(bundleTask.jsSourceMapsDir.get().asFile, bundleTask.bundleAssetName.get() + ".map") }
def debugIdFileOf = { bundleTask ->
if (debugIdPath) {
return bundleTask.root.file(debugIdPath).get().asFile
}
def packagerMapDir = bundleTask.hermesEnabled.get() ? bundleTask.jsIntermediateSourceMapsDir : bundleTask.jsSourceMapsDir
return new File(packagerMapDir.get().asFile, ".backtrace-sourcemap-id")
}

def isDebuggable = { variant ->
if (variant.hasProperty("debuggable")) {
return variant.debuggable
}
// Before AGP 8.5, and in Android Studio's analytics wrapper, only the build type exposes debuggable.
return extensions.getByName("android").buildTypes.getByName(variant.buildType).debuggable
}

def logDebuggableSkip = { String variantName, TaskProvider bundle ->
bundle.configure {
it.doLast { task -> task.logger.lifecycle("Backtrace: ${variantName} is debuggable, skipping its source map upload.") }
}
}

def registerSourceMapUpload = { String variantName, TaskProvider bundle ->
def capitalized = capitalize(variantName)
def outputDirectory = "intermediates/backtrace/sourcemaps/${variantName}"
def debugIdSnapshot = layout.buildDirectory.file("${outputDirectory}/.backtrace-sourcemap-id")

bundle.configure { bundleTask ->
def sourceMap = sourceMapOf(bundleTask)
def debugIdFile = debugIdFileOf(bundleTask)
def snapshot = debugIdSnapshot.get().asFile
bundleTask.outputs.file(snapshot)
bundleTask.doFirst { [sourceMap, debugIdFile, snapshot]*.delete() }
bundleTask.doLast {
if (debugIdFile.isFile()) {
snapshot.parentFile.mkdirs()
snapshot.bytes = debugIdFile.bytes
}
}
}

def upload = tasks.register(legacyTaskName + capitalized, BacktraceSourceMapUploadTask) { task ->
def bundleTask = bundle.get()
task.group = "backtrace"
task.description = "Uploads the ${variantName} source map to Backtrace."
task.dependsOn(bundle)
task.variantName.set(variantName)
task.sourceMap.from(sourceMapOf(bundleTask))
task.debugIdFile.from(debugIdSnapshot)
task.configFile.from(bundleTask.root.file(".backtracejsrc"))
task.appRoot.set(bundleTask.root)
task.processedSourceMap.set(layout.buildDirectory.file("${outputDirectory}/${bundleTask.bundleAssetName.get()}.map"))
}
sourceMapUploads[variantName] = upload

if (uploadsEnabled) {
["package" + capitalized, "package" + capitalized + "Bundle", "sign" + capitalized + "Bundle"].each { uploadsByPackagingTask[it] = upload }
}
}

tasks.configureEach { task ->
def upload = uploadsByPackagingTask[task.name]
if (upload) {
task.finalizedBy(upload)
}
}

tasks.register(legacyTaskName) { task ->
task.group = "backtrace"
task.description = "Uploads the -PbuildVariant source map (default: release) to Backtrace. Release builds upload on their own."
// Old documented hooks finalize debug builds with this task; it uploads only when run by name.
if (legacyRequested) {
task.dependsOn({
def upload = sourceMapUploads.find { it.key.equalsIgnoreCase(legacyVariant) }?.value
if (!upload) {
def available = sourceMapUploads.keySet().join(", ") ?: "none"
task.logger.warn("Backtrace: No release variant named '${legacyVariant}' has a React Native bundle to upload. Release variants: ${available}.")
}
return upload ?: []
})
}
}

// Execute Backtrace JS CLI to process upload processed source maps
def command = ["npx", "--yes", "@backtrace/javascript-cli", "upload", "-p", mapFile.absolutePath]
exec {
workingDir file("$rootProject.projectDir/..")
commandLine command
plugins.withId("com.facebook.react") {
plugins.withId("com.android.application") {
def androidComponents = extensions.getByName("androidComponents")
androidComponents.onVariants(androidComponents.selector().all()) { variant ->
def bundleTaskName = "createBundle" + capitalize(variant.name) + "JsAndAssets"
if (!tasks.names.contains(bundleTaskName)) {
return
}
def bundle = tasks.named(bundleTaskName)
if (debugIdPathLock) {
bundle.configure { it.usesService(debugIdPathLock) }
}
if (isDebuggable(variant)) {
logDebuggableSkip(variant.name, bundle)
} else {
registerSourceMapUpload(variant.name, bundle)
}
}
}
}
Loading