18

I want to change standardOutput of one build task to file, because it will be parsed later by another task.

But also, I would like to have simultaneously output in the terminal to see what's going on in the build.

This is how I changed output of the task to the file:

task sampleTaskWithOutputToFile(type: Exec) {
    commandLine 'someCommand', 'param1'

    doFirst {
        standardOutput = new FileOutputStream('someFolder/someFile.out')
    }
} 

As I understand, I can write own OutputStream implementation with output to file and standard System.out simultaneously but I would like to use existing solution.

Also, I can not use unix tools like tee for that, because task can be launched from any OS (Mac OS, Some Linux or even Windows...)

Thanks!

Artem Zinnatullin
  • 4,305
  • 1
  • 29
  • 43

3 Answers3

21

Expounding on Peter N's comment regarding TeeOutputStream:

task sampleTaskWithOutputToFile(type: Exec) {
    commandLine 'someCommand', 'param1'

    doFirst {
        standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
            new FileOutputStream("someFolder/someFile.out"), System.out);
    }
}
Roberto
  • 321
  • 2
  • 6
2

Here's a solution that works with any type of Gradle Task, not only Exec.
Note: all the examples I provided are in build.gradle.kts format with Gradle lazy configuration syntax.

In your specific case you want either to use a memory cache:

tasks.register<Exec>("sampleTaskWithOutputToFile") {
    commandLine ("someCommand", "param1")

    val taskOutput = StringBuilder()
    logging.addStandardOutputListener { taskOutput.append(it) }
    doLast {
        project.file("foo.output.txt").writeText(taskOutput.toString())
    }
}

or directly write to the file:

tasks.register<Exec>("sampleTaskWithOutputToFile") {
    commandLine ("someCommand", "param1")

    lateinit var taskOutput: java.io.Writer
    doFirst {
        taskOutput = project.file("someFolder/someFile.out").writer()
    }
    logging.addStandardOutputListener { taskOutput.append(it) }
    doLast {
        // WARNING: if the task fails, this won't be executed and the file remains open.
        // The memory cache version doesn't have this problem.
        taskOutput.close()
    }
}

As a more general answer we could consider that an external plugin registered a foo task.

tasks.register("foo") {
    doLast {
        println("warning: some message")
    }
}

In the project build script it's possible to capture, and act on the output, even implement "fail-on-warning" pattern.

tasks.named("foo") {
    val taskOutput = StringBuilder()
    logging.addStandardOutputListener { taskOutput.append(it) }
    doLast {
        // Usage of taskOutput must be in doLast, after all other task actions have been done.
        // Be careful: if there's another `doLast {}` added after this, the output from that won't be considered.
        if ("warning:" in taskOutput) {
            throw Exception(
                """
                There was a problem executing foo, please fix.
                """.trimIndent()
            )
        }
    }
}
TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
  • there is no "build.gradle.kts format" ;) it's called "Kotlin DSL" – xeruf Dec 02 '20 at 18:15
  • 1
    @Xerus Um actually , Kotlin DSL is a set of APIs which are mostly used by Kotlin DSL scripts, whose file names end in `.gradle.kts`. A Kotlin DSL build script (`build.gradle.kts`) is a special one of those (others include init scripts, settings, included files) with some implicit variables/properties available for us: see last sentence [in this section](https://docs.gradle.org/6.7.1/userguide/kotlin_dsl.html#script_file_names). This is why `tasks` is available as used in the examples, because a `build.gradle.kts` file is backed by a `Project`. – TWiStErRob Dec 02 '20 at 18:33
0

This may be helpful for you, if you want a complete log from your plugin or Groovy/Kotlin/Java script.

I am using this my class to log Gradle to a file from my own plugin. You can use this class (it can be translated from Kotlin to another language as it is not so hard):

example:

gradle.useLogger(FileLogger(File(project.rootDir, "log.log")))
import org.gradle.BuildListener
import org.gradle.BuildResult
import org.gradle.api.Project
import org.gradle.api.ProjectEvaluationListener
import org.gradle.api.ProjectState
import org.gradle.api.Task
import org.gradle.api.execution.TaskActionListener
import org.gradle.api.execution.TaskExecutionGraph
import org.gradle.api.execution.TaskExecutionGraphListener
import org.gradle.api.execution.TaskExecutionListener
import org.gradle.api.initialization.Settings
import org.gradle.api.invocation.Gradle
import org.gradle.api.logging.StandardOutputListener
import org.gradle.api.tasks.TaskState
import org.gradle.internal.logging.events.OutputEvent
import org.gradle.internal.logging.events.OutputEventListener
import sk.kimbinogreen.gradle.extensions.deleteIfExists
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.PrintStream

@Suppress("UnstableApiUsage")
class FileLogger(val file: File) :
        OutputEventListener,
        StandardOutputListener,
        BuildListener,
        ProjectEvaluationListener,
        TaskExecutionGraphListener,
        TaskExecutionListener,
        TaskActionListener {
    init {
        file.parentFile.mkdirs()
        file.deleteIfExists()
        file.createNewFile()
        appendText("Build Started.")
    }

    override fun buildStarted(gradle: Gradle?) {
        gradle?.useLogger(this)
    }

    override fun beforeSettings(settings: Settings?) {
    }

    override fun settingsEvaluated(settings: Settings?) {
    }

    override fun projectsLoaded(gradle: Gradle?) {
        gradle?.useLogger(this)
    }

    override fun projectsEvaluated(gradle: Gradle?) {
        gradle?.useLogger(this)
    }

    override fun buildFinished(result: BuildResult?) {
        appendText("Build finished.")
        appendError(result?.failure)
    }

    override fun onOutput(p0: OutputEvent?) {
        p0?.let { appendText(it.toString()) }
    }

    override fun onOutput(p0: CharSequence?) {
        p0?.let {
            appendText(it.toString())
        }
    }

    override fun beforeExecute(task: Task) {
        appendText("Task [${task.project.name}:${task.name}] started.")
        task.logging.addStandardErrorListener(this)
        task.logging.addStandardOutputListener(this)
    }

    override fun afterExecute(task: Task, state: TaskState) {
        appendText("Task [${task.project.name}:${task.name}] finished.")
        appendError(state.failure)
        task.logging.removeStandardErrorListener(this)
        task.logging.removeStandardOutputListener(this)
    }

    override fun afterEvaluate(project: Project, state: ProjectState) {
        project.logging.removeStandardOutputListener(this)
        project.logging.removeStandardErrorListener(this)
    }

    override fun beforeEvaluate(project: Project) {
        project.logging.addStandardOutputListener(this)
        project.logging.addStandardErrorListener(this)
    }

    override fun graphPopulated(taskGraph: TaskExecutionGraph) {
    }

    override fun beforeActions(task: Task) {
        task.logging.addStandardErrorListener(this)
        task.logging.addStandardOutputListener(this)
    }

    override fun afterActions(task: Task) {
        task.logging.removeStandardErrorListener(this)
        task.logging.removeStandardOutputListener(this)
    }

    @Synchronized
    fun appendText(s: String?) {
        s?.split('\n')?.forEach {
            if(it.replace("\n", "").trim().isNotEmpty())
                file.appendText("$it\n")
        }
    }

    @Synchronized
    fun appendError(t: Throwable?) {
        t?.let { error ->
            ByteArrayOutputStream().let { os ->
                error.printStackTrace(PrintStream(os))
                String(os.toByteArray())
            }.let {
                appendText(it)
            }
        }
    }
}
aSemy
  • 5,485
  • 2
  • 25
  • 51
Milan Jurkulak
  • 557
  • 3
  • 6