From 9fcf0735eb196465a413a0a0f10f4b884f943b25 Mon Sep 17 00:00:00 2001 From: Alexey Tsvetkov Date: Mon, 15 Feb 2021 10:33:48 +0300 Subject: [PATCH] Improve diagnosing problems with external tools * Fix passing 'compose.desktop.verbose' to jlink/jpackage * Save external tools logs to files in non-verbose mode --- .../AbstractCheckNotarizationStatusTask.kt | 8 +- .../tasks/AbstractComposeDesktopTask.kt | 108 ++++++++++++++++++ .../application/tasks/AbstractJPackageTask.kt | 22 ++-- .../tasks/AbstractJvmToolOperationTask.kt | 38 ++---- .../tasks/AbstractNotarizationTask.kt | 12 +- .../tasks/AbstractRunDistributableTask.kt | 7 +- .../AbstractUploadAppForNotarizationTask.kt | 49 ++++---- 7 files changed, 155 insertions(+), 89 deletions(-) create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractComposeDesktopTask.kt diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt index faedf66024..8cbffdcd5a 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt @@ -36,15 +36,15 @@ abstract class AbstractCheckNotarizationStatusTask : AbstractNotarizationTask() for (request in requests.sortedBy { it.uploadTime }) { try { logger.quiet("Checking status of notarization request '${request.uuid}'") - execOperations.exec { exec -> - exec.executable = MacUtils.xcrun.absolutePath - exec.args( + runExternalTool( + tool = MacUtils.xcrun, + args = listOf( "altool", "--notarization-info", request.uuid, "--username", notarization.appleID, "--password", notarization.password ) - } + ) } catch (e: Exception) { logger.error("Could not check notarization request '${request.uuid}'", e) } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractComposeDesktopTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractComposeDesktopTask.kt new file mode 100644 index 0000000000..d19a12ea6b --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractComposeDesktopTask.kt @@ -0,0 +1,108 @@ +package org.jetbrains.compose.desktop.application.tasks + +import org.gradle.api.DefaultTask +import org.gradle.api.file.Directory +import org.gradle.api.internal.file.FileOperations +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.LocalState +import org.gradle.process.ExecOperations +import org.gradle.process.ExecResult +import org.jetbrains.compose.desktop.application.internal.ComposeProperties +import org.jetbrains.compose.desktop.application.internal.ioFile +import org.jetbrains.compose.desktop.application.internal.notNullProperty +import java.io.File +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.function.Consumer +import javax.inject.Inject + +abstract class AbstractComposeDesktopTask : DefaultTask() { + @get:Inject + protected abstract val objects: ObjectFactory + + @get:Inject + protected abstract val providers: ProviderFactory + + @get:Inject + protected abstract val execOperations: ExecOperations + + @get:Inject + protected abstract val fileOperations: FileOperations + + @get:LocalState + protected val logsDir: Provider = project.layout.buildDirectory.dir("compose/logs/$name") + + @get:Internal + val verbose: Property = objects.notNullProperty().apply { + set(providers.provider { + logger.isDebugEnabled || ComposeProperties.isVerbose(providers).get() + }) + } + + internal fun runExternalTool( + tool: File, + args: Collection, + environment: Map = emptyMap(), + workingDir: File? = null, + checkExitCodeIsNormal: Boolean = true, + processStdout: Function1? = null + ): ExecResult { + val logsDir = logsDir.ioFile + logsDir.mkdirs() + + val toolName = tool.nameWithoutExtension + val outFile = logsDir.resolve("${toolName}-${currentTimeStamp()}-out.txt") + val errFile = logsDir.resolve("${toolName}-${currentTimeStamp()}-out.txt") + + val result = outFile.outputStream().buffered().use { outStream -> + errFile.outputStream().buffered().use { errStream -> + execOperations.exec { spec -> + spec.executable = tool.absolutePath + spec.args(*args.toTypedArray()) + workingDir?.let { wd -> spec.workingDir(wd) } + spec.environment(environment) + // check exit value later + spec.isIgnoreExitValue = true + + if (!verbose.get()) { + spec.standardOutput = outStream + spec.errorOutput = errStream + } + } + } + } + + if (checkExitCodeIsNormal && result.exitValue != 0) { + val errMsg = buildString { + appendln("External tool execution failed:") + val cmd = (listOf(tool.absolutePath) + args).joinToString(", ") + appendln("* Command: [$cmd]") + appendln("* Working dir: [${workingDir?.absolutePath.orEmpty()}]") + appendln("* Exit code: ${result.exitValue}") + appendln("* Standard output log: ${outFile.absolutePath}") + appendln("* Error log: ${errFile.absolutePath}") + } + + error(errMsg) + } + + if (processStdout != null) { + processStdout(outFile.readText()) + } + + if (result.exitValue == 0) { + outFile.delete() + errFile.delete() + } + + return result + } + + private fun currentTimeStamp() = + LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")) +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt index cdc826da11..7281c455d1 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt @@ -7,7 +7,6 @@ import org.gradle.api.provider.Provider import org.gradle.api.tasks.* import org.gradle.api.tasks.Optional import org.gradle.process.ExecResult -import org.gradle.process.ExecSpec import org.gradle.work.ChangeType import org.gradle.work.InputChanges import org.jetbrains.compose.desktop.application.dsl.MacOSSigningSettings @@ -330,19 +329,16 @@ abstract class AbstractJPackageTask @Inject constructor( } } - override fun configureExec(exec: ExecSpec) { - super.configureExec(exec) - configureWixPathIfNeeded(exec) - } - - private fun configureWixPathIfNeeded(exec: ExecSpec) { - if (currentOS == OS.Windows) { - val wixDir = wixToolsetDir.ioFileOrNull ?: return - val wixPath = wixDir.absolutePath - val path = System.getenv("PATH") ?: "" - exec.environment("PATH", "$wixPath;$path") + override fun jvmToolEnvironment(): MutableMap = + super.jvmToolEnvironment().apply { + if (currentOS == OS.Windows) { + val wixDir = wixToolsetDir.ioFile + val wixPath = wixDir.absolutePath + val path = System.getenv("PATH") ?: "" + put("PATH", "$wixPath;$path") + } } - } + override fun checkResult(result: ExecResult) { super.checkResult(result) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt index 54a95153de..3110d993d4 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt @@ -1,35 +1,21 @@ package org.jetbrains.compose.desktop.application.tasks -import org.gradle.api.DefaultTask import org.gradle.api.file.Directory import org.gradle.api.file.DirectoryProperty -import org.gradle.api.internal.file.FileOperations -import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider -import org.gradle.api.provider.ProviderFactory import org.gradle.api.tasks.* -import org.gradle.process.ExecOperations import org.gradle.process.ExecResult import org.gradle.process.ExecSpec import org.gradle.work.InputChanges -import org.jetbrains.compose.desktop.application.internal.* import org.jetbrains.compose.desktop.application.internal.ComposeProperties +import org.jetbrains.compose.desktop.application.internal.executableName +import org.jetbrains.compose.desktop.application.internal.ioFile import org.jetbrains.compose.desktop.application.internal.notNullProperty import java.io.File -import javax.inject.Inject - -abstract class AbstractJvmToolOperationTask(private val toolName: String) : DefaultTask() { - @get:Inject - protected abstract val objects: ObjectFactory - @get:Inject - protected abstract val providers: ProviderFactory - @get:Inject - protected abstract val execOperations: ExecOperations - @get:Inject - protected abstract val fileOperations: FileOperations +abstract class AbstractJvmToolOperationTask(private val toolName: String) : AbstractComposeDesktopTask() { @get:LocalState protected val workingDir: Provider = project.layout.buildDirectory.dir("compose/tmp/$name") @@ -45,11 +31,6 @@ abstract class AbstractJvmToolOperationTask(private val toolName: String) : Defa set(providers.systemProperty("java.home")) } - @get:Internal - val verbose: Property = objects.notNullProperty().apply { - set(providers.provider { logger.isDebugEnabled }.orElse(ComposeProperties.isVerbose(providers))) - } - protected open fun prepareWorkingDir(inputChanges: InputChanges) { fileOperations.delete(workingDir) fileOperations.mkdir(workingDir) @@ -59,7 +40,8 @@ abstract class AbstractJvmToolOperationTask(private val toolName: String) : Defa freeArgs.orNull?.forEach { add(it) } } - protected open fun configureExec(exec: ExecSpec) {} + protected open fun jvmToolEnvironment(): MutableMap = + HashMap() protected open fun checkResult(result: ExecResult) { result.assertNormalExitValue() } @@ -85,11 +67,11 @@ abstract class AbstractJvmToolOperationTask(private val toolName: String) : Defa } try { - execOperations.exec { exec -> - configureExec(exec) - exec.executable = jtool.absolutePath - exec.setArgs(listOf("@${argsFile.absolutePath}")) - }.also { checkResult(it) } + runExternalTool( + tool = jtool, + args = listOf("@${argsFile.absolutePath}"), + environment = jvmToolEnvironment() + ).also { checkResult(it) } } finally { if (!ComposeProperties.preserveWorkingDir(providers).get()) { fileOperations.delete(workingDir) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNotarizationTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNotarizationTask.kt index b6c11222d8..2570b44c46 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNotarizationTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNotarizationTask.kt @@ -1,24 +1,14 @@ package org.jetbrains.compose.desktop.application.tasks -import org.gradle.api.DefaultTask -import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional -import org.gradle.process.ExecOperations import org.jetbrains.compose.desktop.application.dsl.MacOSNotarizationSettings import org.jetbrains.compose.desktop.application.internal.nullableProperty import org.jetbrains.compose.desktop.application.internal.validation.validate -import javax.inject.Inject - -abstract class AbstractNotarizationTask( -) : DefaultTask() { - @get:Inject - protected abstract val objects: ObjectFactory - @get:Inject - protected abstract val execOperations: ExecOperations +abstract class AbstractNotarizationTask : AbstractComposeDesktopTask() { @get:Input @get:Optional internal val nonValidatedBundleID: Property = objects.nullableProperty() diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractRunDistributableTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractRunDistributableTask.kt index 7938641c91..ec6ace8fa1 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractRunDistributableTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractRunDistributableTask.kt @@ -1,13 +1,11 @@ package org.jetbrains.compose.desktop.application.tasks -import org.gradle.api.DefaultTask import org.gradle.api.file.Directory import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskProvider -import org.gradle.process.ExecOperations import org.jetbrains.compose.desktop.application.internal.OS import org.jetbrains.compose.desktop.application.internal.currentOS import org.jetbrains.compose.desktop.application.internal.executableName @@ -18,9 +16,8 @@ import javax.inject.Inject // lazy configuration yet. Lazy configuration is needed to // calculate appImageDir after the evaluation of createApplicationImage abstract class AbstractRunDistributableTask @Inject constructor( - createApplicationImage: TaskProvider, - private val execOperations: ExecOperations -) : DefaultTask() { + createApplicationImage: TaskProvider +) : AbstractComposeDesktopTask() { @get:InputDirectory internal val appImageRootDir: Provider = createApplicationImage.flatMap { it.destinationDir } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt index e46694bb94..93ae0740a7 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt @@ -4,8 +4,7 @@ import org.gradle.api.file.DirectoryProperty import org.gradle.api.tasks.* import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.internal.* -import java.io.ByteArrayOutputStream -import java.io.PrintStream +import java.io.File import java.time.LocalDateTime import java.time.format.DateTimeFormatter import javax.inject.Inject @@ -27,32 +26,26 @@ abstract class AbstractUploadAppForNotarizationTask @Inject constructor( @TaskAction fun run() { val notarization = validateNotarization() - val inputFile = findOutputFileOrDir(inputDir.ioFile, targetFormat) - val file = inputFile.checkExistingFile() + val packageFile = findOutputFileOrDir(inputDir.ioFile, targetFormat).checkExistingFile() - logger.quiet("Uploading '${file.name}' for notarization (package id: '${notarization.bundleID}')") - val (res, output) = ByteArrayOutputStream().use { baos -> - PrintStream(baos).use { ps -> - val res = execOperations.exec { exec -> - exec.executable = MacUtils.xcrun.absolutePath - exec.args( - "altool", - "--notarize-app", - "--primary-bundle-id", notarization.bundleID, - "--username", notarization.appleID, - "--password", notarization.password, - "--file", file - ) - exec.standardOutput = ps - } - - res to baos.toString() + logger.quiet("Uploading '${packageFile.name}' for notarization (package id: '${notarization.bundleID}')") + runExternalTool( + tool = MacUtils.xcrun, + args = listOf( + "altool", + "--notarize-app", + "--primary-bundle-id", notarization.bundleID, + "--username", notarization.appleID, + "--password", notarization.password, + "--file", packageFile.absolutePath + ), + processStdout = { output -> + processUploadToolOutput(packageFile, output) } - } - if (res.exitValue != 0) { - logger.error("Uploading failed. Stdout: $output") - res.assertNormalExitValue() - } + ) + } + + private fun processUploadToolOutput(packageFile: File, output: String) { val m = "RequestUUID = ([A-Za-z0-9\\-]+)".toRegex().find(output) ?: error("Could not determine RequestUUID from output: $output") @@ -61,8 +54,8 @@ abstract class AbstractUploadAppForNotarizationTask @Inject constructor( val uploadTime = LocalDateTime.now() .format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")) val requestDir = requestsDir.ioFile.resolve("$uploadTime-${targetFormat.id}") - val packageCopy = requestDir.resolve(inputFile.name) - inputFile.copyTo(packageCopy) + val packageCopy = requestDir.resolve(packageFile.name) + packageFile.copyTo(packageCopy) val requestInfo = NotarizationRequestInfo(uuid = requestId, uploadTime = uploadTime) val requestInfoFile = requestDir.resolve(NOTARIZATION_REQUEST_INFO_FILE_NAME) requestInfo.saveTo(requestInfoFile)