├── gradle.properties ├── settings.gradle ├── .gitignore ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── publish.gradle ├── src ├── main │ ├── resources │ │ └── META-INF │ │ │ └── gradle-plugins │ │ │ └── me.champeau.buildscan-recipes.properties │ └── groovy │ │ └── me │ │ └── champeau │ │ └── gradle │ │ └── buildscans │ │ ├── Recipe.groovy │ │ ├── RecipesPlugin.groovy │ │ ├── RecipeExtension.groovy │ │ ├── Recipes.groovy │ │ └── RecipeCompiler.java └── recipes │ ├── git-status.groovy │ ├── git-commit.groovy │ ├── travis-ci.groovy │ ├── teamcity.groovy │ ├── remote.groovy │ ├── README.adoc │ ├── gist.groovy │ ├── gc-stats.groovy │ ├── disk-usage.groovy │ ├── git-diff-to-gist.groovy │ └── file-leak-detection.groovy ├── recipes-config.groovy ├── .travis.yml ├── gradlew.bat ├── gradlew ├── README.adoc ├── LICENSE └── LICENSE.txt /gradle.properties: -------------------------------------------------------------------------------- 1 | version=0.2.4-SNAPSHOT 2 | 3 | 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'buildscan-recipes-plugin' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/build/ 2 | **/.gradle/ 3 | .idea/ 4 | *.iml 5 | *.ipr 6 | *.iws 7 | out/ 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melix/gradle-buildscan-recipes/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/META-INF/gradle-plugins/me.champeau.buildscan-recipes.properties: -------------------------------------------------------------------------------- 1 | implementation-class=me.champeau.gradle.buildscans.RecipesPlugin 2 | 3 | -------------------------------------------------------------------------------- /src/recipes/git-status.groovy: -------------------------------------------------------------------------------- 1 | try { 2 | def status = 'git status --porcelain'.execute().text 3 | if (status) { 4 | buildScan.tag "dirty" 5 | buildScan.value "Git Status", status 6 | } 7 | } catch (ignore) { 8 | // ignore 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Oct 13 22:28:09 CEST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip 7 | -------------------------------------------------------------------------------- /src/main/groovy/me/champeau/gradle/buildscans/Recipe.groovy: -------------------------------------------------------------------------------- 1 | package me.champeau.gradle.buildscans 2 | 3 | import com.gradle.scan.plugin.BuildScanExtension 4 | import org.gradle.api.invocation.Gradle 5 | 6 | interface Recipe { 7 | void apply(Gradle gradle, BuildScanExtension buildScan, Map params) 8 | } -------------------------------------------------------------------------------- /recipes-config.groovy: -------------------------------------------------------------------------------- 1 | import groovy.transform.CompileStatic 2 | 3 | def cl = new URLClassLoader(new File('build/classpath.txt').text.split(':').collect {new File(it).toURI().toURL()} as URL[]) 4 | 5 | withConfig(configuration) { 6 | ast(CompileStatic) 7 | } 8 | configuration.addCompilationCustomizers(cl.loadClass('me.champeau.gradle.buildscans.RecipeCompiler').newInstance()) 9 | -------------------------------------------------------------------------------- /src/recipes/git-commit.groovy: -------------------------------------------------------------------------------- 1 | def commitId 2 | try { 3 | commitId = ['git', 'rev-parse' ,'--verify' , 'HEAD'].execute().text.trim() 4 | } catch (ignore) { 5 | // ignore 6 | } 7 | 8 | if (commitId) { 9 | buildScan.value "Git Commit ID", commitId 10 | if (params.baseUrl) { 11 | buildScan.link 'Sources', "${params.baseUrl}/$commitId" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - oraclejdk7 3 | before_install: 4 | - sudo apt-get update -qq 5 | - sudo apt-get install oracle-java7-installer 6 | cache: 7 | directories: 8 | - $HOME/.m2 9 | - $HOME/.gradle 10 | before_script: 11 | - ./gradlew --version 12 | script: ./gradlew --no-daemon -s -PbuildInfo.build.number=$TRAVIS_BUILD_NUMBER -PbuildInfo.buildUrl=https://travis-ci.org/${TRAVIS_REPO_SLUG}/builds/${TRAVIS_JOB_ID} 13 | -PbuildInfo.buildAgent.name=$USER -PbuildInfo.principal=$USER -Dscan clean jar 14 | -------------------------------------------------------------------------------- /src/recipes/travis-ci.groovy: -------------------------------------------------------------------------------- 1 | def env = System.getenv() 2 | 3 | if (env.CI && env.TRAVIS) { 4 | buildScan.tag 'CI' 5 | buildScan.link 'Travis Build', "https://travis-ci.org/${env.TRAVIS_REPO_SLUG}/builds/${env.TRAVIS_JOB_ID}" 6 | buildScan.value 'Travis Branch', env.TRAVIS_BRANCH 7 | buildScan.value 'Travis Commit', env.TRAVIS_COMMIT 8 | buildScan.value 'Travis Pull Request', env.TRAVIS_PULL_REQUEST 9 | if (env.TRAVIS_TAG) { 10 | buildScan.value 'Travis Tag', env.TRAVIS_TAG 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/main/groovy/me/champeau/gradle/buildscans/RecipesPlugin.groovy: -------------------------------------------------------------------------------- 1 | package me.champeau.gradle.buildscans 2 | 3 | import com.gradle.scan.plugin.BuildScanExtension 4 | import groovy.transform.CompileStatic 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | 8 | @CompileStatic 9 | class RecipesPlugin implements Plugin { 10 | void apply(Project project) { 11 | def buildScanExtension = (BuildScanExtension) project.extensions.getByName('buildScan') 12 | project.extensions.create('buildScanRecipes', RecipeExtension, buildScanExtension, project.gradle) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/recipes/teamcity.groovy: -------------------------------------------------------------------------------- 1 | def env = System.getenv() 2 | 3 | if (env.TEAMCITY_VERSION) { 4 | def version = env.TEAMCITY_VERSION 5 | buildScan.tag 'CI' 6 | def guest = params.guest?'&guest=1':'' 7 | def project = gradle.rootProject 8 | def buildId = System.getProperty('teamcity.agent.dotnet.build_id', null) 9 | if (!buildId && project.hasProperty('teamcity')) { 10 | buildId = ((Map)project.property('teamcity'))['build.id'] 11 | } 12 | if (env.BUILD_URL) { 13 | buildScan.link "TeamCity $version build", env.BUILD_URL 14 | } else if (params.baseUrl && buildId) { 15 | buildScan.link "TeamCity $version build", "${params.baseUrl}/viewLog.html?buildId=$buildId$guest" 16 | } 17 | buildScan.value "Teamcity $version build name", env.TEAMCITY_BUILDCONF_NAME 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /gradle/publish.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'com.gradle.plugin-publish' 3 | 4 | publishing { 5 | publications { 6 | mavenJava(MavenPublication) { 7 | from components.java 8 | } 9 | } 10 | } 11 | 12 | pluginBundle { 13 | website = 'https://github.com/melix/gradle-buildscan-recipes' 14 | vcsUrl = 'https://github.com/melix/gradle-buildscan-recipes' 15 | description = 'Various recipes for Gradle Build Scans' 16 | tags = ['build scans'] 17 | 18 | plugins { 19 | buildScanRecipesPlugin { 20 | id = 'me.champeau.buildscan-recipes' 21 | displayName = 'Gradle Build Scan Recipes' 22 | } 23 | } 24 | 25 | mavenCoordinates { 26 | groupId = project.group 27 | artifactId = project.name 28 | version = project.version 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/recipes/remote.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * A meta-recipe which will download another recipe from any remote URL. 3 | * 4 | * This recipe will fetch a remote URL, compile it as a recipe, and execute it. It accepts the following parameters: 5 | * 6 | * @param url URL of the recipe. Must be a .groovy file. 7 | * @param cache cache the compiled script, avoiding to fetch it again next time ("true" or "false") 8 | * 9 | * Any additional parameter will be passed to the remote recipe. 10 | * 11 | * Example usage: 12 | * recipe 'remote', 13 | * url: 'https://gist.githubusercontent.com/melix/5944cb701d6c9650ecaccccd4642ea5f/raw//my-recipe.groovy', 14 | * cache: 'false', 15 | * // remote recipe attributes 16 | * name: 'Bob' 17 | */ 18 | 19 | import me.champeau.gradle.buildscans.RecipeCompiler 20 | 21 | def url = new URL(params.url) 22 | String recipe = (url.file - '.groovy').replaceAll('.+[/]', '') 23 | 24 | boolean cache = Boolean.valueOf(params.rev?:'false') 25 | 26 | def recipeClass = RecipeCompiler.compileOrGetFromCache(gradle, recipe, url, cache) 27 | recipeClass.newInstance().apply(gradle, buildScan, params) 28 | -------------------------------------------------------------------------------- /src/recipes/README.adoc: -------------------------------------------------------------------------------- 1 | == Writing new recipes 2 | 3 | Build Scan recipes are written using the http://groovy-lang.org/[Groovy language]. Despite appearances, all the 4 | files in this directory are _statically compiled_. 5 | 6 | They have 3 injected variables: 7 | 8 | * `buildScan` is an instance of `com.gradle.scan.plugin.BuildScanExtension`. This extension offers 3 methods that 9 | are interesting for recipes: 10 | ** `link(String label, String url)` allows the creation of a link in the build scan. 11 | ** `tag(String tag)` adds a tag to the build scan 12 | ** `value(String name, String value)` adds a custom value 13 | * `gradle` in an instance of `org.gradle.api.invocation.Gradle`. It should be unused in most cases, but can be helpful 14 | if you need to add custom values, tags, ... at a different time than when the plugin is applied (for example, once 15 | the task graph is ready, ...) 16 | * `params` is an instance of `Map` and can be used to configure the recipe. 17 | 18 | The name of the recipe is directly used in the configuration of the plugin. If you are interested in understanding how 19 | a recipe can be statically compiled, look at the `me.champeau.gradle.buildscans.RecipeCompiler` class, as well as 20 | the `compileRecipes` task in the root Gradle file. -------------------------------------------------------------------------------- /src/main/groovy/me/champeau/gradle/buildscans/RecipeExtension.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.gradle.buildscans 17 | 18 | import com.gradle.scan.plugin.BuildScanExtension 19 | import groovy.transform.CompileStatic 20 | import org.gradle.api.invocation.Gradle 21 | 22 | @CompileStatic 23 | class RecipeExtension { 24 | private final Recipes recipes 25 | 26 | RecipeExtension(BuildScanExtension bse, Gradle gradle) { 27 | this.recipes = new Recipes(bse, gradle) 28 | } 29 | 30 | void recipe(Map params = [:], String recipeName) { 31 | recipes.apply(recipeName, params) 32 | } 33 | 34 | void recipes(String... recipeList) { 35 | recipeList.each { String recipe -> 36 | recipes.apply(recipe) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/groovy/me/champeau/gradle/buildscans/Recipes.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.gradle.buildscans 17 | 18 | import com.gradle.scan.plugin.BuildScanExtension 19 | import groovy.transform.CompileStatic 20 | import groovy.transform.TupleConstructor 21 | import org.gradle.api.invocation.Gradle 22 | 23 | @CompileStatic 24 | @TupleConstructor 25 | class Recipes { 26 | final BuildScanExtension buildScan 27 | final Gradle gradle 28 | private final Map recipes = [:].withDefault { 29 | (Recipe) Class.forName(RecipeCompiler.recipeClassName((String)it)).newInstance() 30 | } 31 | 32 | void apply(final String recipeName, Map params = [:]) { 33 | recipes[recipeName].apply(gradle, buildScan, params?:Collections.emptyMap()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/recipes/gist.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * A meta-recipe which will download another recipe from Gist. 3 | * 4 | * This recipe will fetch a Gist, compile it as a recipe, and execute it. It accepts the following parameters: 5 | * 6 | * @param user username of the gist owner 7 | * @param id id of the gist 8 | * @param recipe name of the file containing the gist 9 | * @param rev revision of the gist. If absent, the compiled recipe will not be cached. 10 | * 11 | * Any additional parameter will be passed to the remote recipe. 12 | * 13 | * Example usage. For the gist at: 14 | * the name of the recipe is: my-recipe (my-recipe.groovy) 15 | * the id is 5944cb701d6c9650ecaccccd4642ea5f 16 | * and the revision can be found when clicking on "raw": 4b40b45559929ee2baaa7599e29dd78e51c3843a 17 | * 18 | * https://gist.githubusercontent.com/melix/5944cb701d6c9650ecaccccd4642ea5f/raw/4b40b45559929ee2baaa7599e29dd78e51c3843a/my-recipe.groovy 19 | * 20 | * recipe 'gist', 21 | * user: 'melix', 22 | * id: '5944cb701d6c9650ecaccccd4642ea5f', 23 | * rev: '4b40b45559929ee2baaa7599e29dd78e51c3843a', 24 | * recipe: 'my-recipe', 25 | * // remote recipe attributes 26 | * name: 'Bob' 27 | */ 28 | 29 | import me.champeau.gradle.buildscans.RecipeCompiler 30 | 31 | def user = params.user 32 | def id = params.id 33 | def rev = params.rev 34 | def recipe = params.recipe 35 | 36 | boolean cache = rev 37 | 38 | if (!cache) { 39 | println "Warning, recipe $recipe will not be cached because revision isn't set" 40 | } 41 | 42 | def url = "https://gist.githubusercontent.com/$user/$id/raw/$rev/${recipe}.groovy" 43 | 44 | def recipeClass = RecipeCompiler.compileOrGetFromCache(gradle, recipe, new URL(url), cache) 45 | recipeClass.newInstance().apply(gradle, buildScan, params) 46 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/recipes/gc-stats.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * This recipe provides information about GC collection time, number of GC events, during a build. 3 | * Original code contributed by Hans Docker (https://github.com/hansd) 4 | */ 5 | import groovy.transform.Canonical 6 | 7 | import java.lang.management.GarbageCollectorMXBean 8 | import java.lang.management.ManagementFactory 9 | 10 | def gcValues = new GcValues() 11 | def gcStart = gcValues.gcTimes 12 | 13 | buildScan.buildFinished { 14 | def gcResult = GcValues.getGcDelta(gcStart, gcValues.gcTimes) 15 | [GcValues.PS_SCAVENGE, GcValues.PS_MARK_SWEEP, GcValues.UNKNOWN].each { event -> 16 | buildScan.value("GC - ${event} Count:", "${gcResult[event].count}") 17 | buildScan.value("GC - ${event} Time (s):", "${gcResult[event].time / 1000d}") 18 | } 19 | } 20 | 21 | class GcValues { 22 | public static final String PS_SCAVENGE = "PS Scavenge" 23 | public static final String PS_MARK_SWEEP = "PS MarkSweep" 24 | public static final String UNKNOWN = "Unknown" 25 | List mxBeans = ManagementFactory.getGarbageCollectorMXBeans() 26 | 27 | public Map getGcTimes() { 28 | Map result = [:].withDefault { new GcActivity(0,0) } 29 | for (GarbageCollectorMXBean gc : mxBeans) { 30 | if (gc.name == PS_SCAVENGE) { 31 | result[PS_SCAVENGE] = new GcActivity(gc.collectionCount, gc.collectionTime) 32 | } else if (gc.name == PS_MARK_SWEEP) { 33 | result[PS_MARK_SWEEP] = new GcActivity(gc.collectionCount, gc.collectionTime) 34 | } else { 35 | result[UNKNOWN] = new GcActivity(gc.collectionCount, gc.collectionTime) 36 | } 37 | } 38 | 39 | result 40 | } 41 | 42 | public static Map getGcDelta(Map startData, Map endData) { 43 | def result = [:] 44 | result[PS_SCAVENGE] = new GcActivity(endData[PS_SCAVENGE].count - startData[PS_SCAVENGE].count, endData[PS_SCAVENGE].time - startData[PS_SCAVENGE].time) 45 | result[PS_MARK_SWEEP] = new GcActivity(endData[PS_MARK_SWEEP].count - startData[PS_MARK_SWEEP].count, endData[PS_MARK_SWEEP].time - startData[PS_MARK_SWEEP].time) 46 | result[UNKNOWN] = new GcActivity(endData[UNKNOWN].count - startData[UNKNOWN].count, endData[UNKNOWN].time - startData[UNKNOWN].time) 47 | 48 | result 49 | } 50 | 51 | @Canonical 52 | public static class GcActivity { 53 | long count 54 | long time 55 | } 56 | } -------------------------------------------------------------------------------- /src/recipes/disk-usage.groovy: -------------------------------------------------------------------------------- 1 | // WARNING: This recipe will scan your $HOME/.gradle directory at the 2 | // beginning of each build, which can, depending on your drive type or 3 | // FS be very slow 4 | 5 | import org.gradle.internal.os.OperatingSystem // remember kids, depending on internal API is WRONG! 6 | 7 | def os = OperatingSystem.current() 8 | if (os.unix || os.macOsX) { 9 | def home = System.getProperty("user.home") 10 | def wrapperCount = new File("$home/.gradle/wrapper/dists").listFiles().size() 11 | def projectDir = gradle.rootProject.projectDir 12 | 13 | List measurements = [] 14 | 15 | def directoriesToMeasure = ['caches': 'Caches', 'wrapper': "Wrappers ($wrapperCount versions)"] 16 | 17 | directoriesToMeasure.each { dir -> 18 | File dirKey = gradle.rootProject.file("$home/.gradle/$dir.key") 19 | measurements << new Measurement(dirKey, "Disk usage ($dir.value)") 20 | } 21 | def projectDirMeasurement = new Measurement(projectDir, 'Disk usage (Project)') 22 | def projectDirDotGradle = new Measurement(gradle.rootProject.file("$projectDir/.gradle"), 'Disk usage (Project .gradle)') 23 | measurements << projectDirMeasurement 24 | measurements << projectDirDotGradle 25 | 26 | buildScan.buildFinished { 27 | measurements*.update() 28 | projectDirMeasurement.initialSize -= projectDirDotGradle.initialSize 29 | projectDirMeasurement.buildFinishedSize -= projectDirDotGradle.buildFinishedSize 30 | measurements.each { measurement -> 31 | buildScan.value measurement.label, measurement.formattedSize 32 | } 33 | } 34 | } 35 | 36 | class Measurement { 37 | final File dir 38 | final String label 39 | long initialSize 40 | long buildFinishedSize 41 | 42 | Measurement(File dir, String label) { 43 | this.dir = dir 44 | this.label = label 45 | this.initialSize = diskUsage(dir.absolutePath) 46 | } 47 | 48 | long getDelta() { 49 | buildFinishedSize - initialSize 50 | } 51 | 52 | void update() { 53 | this.buildFinishedSize = diskUsage(dir.absolutePath) 54 | } 55 | 56 | private static long diskUsage(String dir) { 57 | Long.valueOf(['du', '-ks' , dir].execute().text.trim().split('\\s')[0]?:'0') 58 | } 59 | 60 | private static String format(long amount) { 61 | if (amount<1024) { 62 | return "${amount}kB" 63 | } 64 | amount /= 1024 65 | if (amount<1024) { 66 | return "${amount}MB" 67 | } 68 | amount /= 1024 69 | if (amount<1024) { 70 | return "${amount}GB" 71 | } 72 | amount /= 1024 73 | return "${amount}TB" 74 | } 75 | 76 | public String getFormattedSize() { 77 | String deltaStr = delta==0?'':" (${delta<0?'':'+'}${format(delta)})" 78 | "${format(buildFinishedSize)}$deltaStr" 79 | } 80 | } -------------------------------------------------------------------------------- /src/recipes/git-diff-to-gist.groovy: -------------------------------------------------------------------------------- 1 | import com.gradle.scan.plugin.BuildScanExtension 2 | import groovy.json.JsonBuilder 3 | import groovy.json.JsonSlurper 4 | import groovy.transform.CompileDynamic 5 | import org.gradle.api.Project 6 | 7 | /** 8 | * This recipe will take the result of `git diff` and publish it to a Gist. 9 | * It requires authentication on GitHub, which can be achieved by creating a 10 | * personal access token. For security reasons, you should create a token 11 | * specifically for this recipe, and limited to "gist" access rights. 12 | * 13 | * @param public set to true if the Gist should be public. By default, false. 14 | * @param user your GitHub username. If not specified explicitly, will look for a project property named "gistUsername" 15 | * @param token your API token, created on https://github.com/settings/tokens If not specified explicitly, will look for a project property named "gistToken" 16 | * @param publish if you want to avoid publishing for each build, you can set this parameter to "false" 17 | * 18 | */ 19 | 20 | if (gradle.rootProject.hasProperty('noGist')) { 21 | return 22 | } 23 | 24 | try { 25 | def diff = ['git', 'diff'].execute().text 26 | if (diff) { 27 | def baseUrl = new URL('https://api.github.com/gists') 28 | String credentials = "${params.user ?: gradle.rootProject.findProperty('gistUsername')}:${params.token ?: gradle.rootProject.findProperty('gistToken')}" 29 | String basicAuth = "Basic ${credentials.bytes.encodeBase64()}" 30 | try { 31 | HttpURLConnection connection = (HttpURLConnection) baseUrl.openConnection() 32 | connection.with { 33 | setRequestProperty("Authorization", basicAuth) 34 | doOutput = true 35 | requestMethod = 'POST' 36 | outputStream.withWriter { writer -> 37 | jsonRequest(writer, gradle.rootProject, params.public ?: "false", diff) 38 | } 39 | createLink(content, buildScan) 40 | } 41 | } catch (ex) { 42 | gradle.rootProject.logger.warn("Unable to publish diff to Gist", ex) 43 | } 44 | } 45 | } catch (ignore) { 46 | // ignore 47 | } 48 | 49 | @CompileDynamic 50 | static void jsonRequest(Writer out, Project project, String isPublic, String diff) { 51 | def builder = new JsonBuilder() 52 | builder { 53 | description("Git diff for $project.name") 54 | 'public'(isPublic) 55 | files { 56 | "${project.name}.diff" { 57 | content diff 58 | } 59 | } 60 | } 61 | builder.writeTo(out) 62 | } 63 | 64 | @CompileDynamic 65 | static void createLink(Object content, BuildScanExtension buildScan) { 66 | def parser = new JsonSlurper() 67 | def url = parser.parse(content.text.bytes).html_url 68 | buildScan.link('Git diff', url) 69 | } -------------------------------------------------------------------------------- /src/recipes/file-leak-detection.groovy: -------------------------------------------------------------------------------- 1 | import groovy.transform.CompileDynamic 2 | 3 | import java.lang.management.ManagementFactory 4 | import org.gradle.api.invocation.Gradle 5 | import org.gradle.api.Task 6 | 7 | /** 8 | * This recipe will try to find which tasks leak file handles. Leaking file handles will often prevent the daemon from working 9 | * properly, because Java will not automatically close files after a build. Instead, subsequent builds trying to access the same 10 | * file would fail (typically on Windows, with things like "cannot delete file"). 11 | * 12 | */ 13 | 14 | if (gradle.rootProject.hasProperty('disableLeakDetection')) { 15 | return 16 | } 17 | 18 | Class loadAgent(Gradle gradle) { 19 | def agent = gradle.rootProject.dependencies.create('org.kohsuke:file-leak-detector:1.8:jar-with-dependencies@jar') 20 | def config = gradle.rootProject.configurations.detachedConfiguration(agent) 21 | String currentVMName = ManagementFactory.runtimeMXBean.name 22 | String pid = currentVMName.substring(0, currentVMName.indexOf('@')) 23 | def vmClazz = Class.forName('com.sun.tools.attach.VirtualMachine') 24 | def vm = vmClazz.invokeMethod('attach', pid) 25 | vm.invokeMethod('loadAgent', config.asPath) 26 | vm.invokeMethod('detach', null) 27 | Class.forName('org.kohsuke.file_leak_detector.Listener', false, ClassLoader.systemClassLoader) 28 | } 29 | 30 | @CompileDynamic 31 | Set listOpenFiles(Class listener) { 32 | (listener.getCurrentOpenFiles()).collect { 33 | File file = it.class.name == 'org.kohsuke.file_leak_detector.Listener$FileRecord'?it.'file' : null 34 | new Record((Exception)it.'stackTrace', (String) it.'threadName', (Long) it.'time', file) 35 | }.findAll { Record it -> 36 | // filter out Gradle files which are closed after build is finished 37 | !it.stackTrace.stackTrace.any { 38 | it.className.startsWith('org.gradle.api.internal') || 39 | it.className.startsWith('org.gradle.cache.internal') 40 | } 41 | } as Set 42 | } 43 | 44 | def listener = loadAgent(gradle) 45 | def tagged = false 46 | Map> taskStart = [:] 47 | Map> potentiallyLeaking = [:] 48 | 49 | gradle.taskGraph.beforeTask { Task it -> 50 | taskStart[it.path] = listOpenFiles(listener) 51 | } 52 | 53 | gradle.taskGraph.afterTask { Task task -> 54 | def openFiles = listOpenFiles(listener) - taskStart[task.path] 55 | if (openFiles) { 56 | potentiallyLeaking[task.path] = openFiles 57 | } 58 | taskStart.remove(task.path) 59 | } 60 | 61 | buildScan.buildFinished { 62 | def id = System.currentTimeMillis() 63 | int cpt = 0 64 | def openFiles = listOpenFiles(listener) 65 | potentiallyLeaking.each { String task, Set leaking -> 66 | Set actuallyLeaking = leaking.findAll { openFiles.contains(it ) } 67 | if (actuallyLeaking) { 68 | if (!tagged) { 69 | buildScan.tag('LEAKS FILE HANDLES') 70 | tagged = true 71 | } 72 | def details = gradle.rootProject.file("${gradle.rootProject.buildDir}/leaking-${id}-${cpt++}") 73 | buildScan.value("Leaking file handles", "Task $task leaks file handles: ${actuallyLeaking*.file}. See $details for details.") 74 | details.withWriter('utf-8') { wrt -> 75 | actuallyLeaking.each { 76 | wrt.println("File $it.file") 77 | wrt.println("-------------------------------------------") 78 | it.stackTrace.printStackTrace(new PrintWriter(wrt)) 79 | wrt.println("") 80 | } 81 | } 82 | } 83 | } 84 | potentiallyLeaking.clear() 85 | } 86 | 87 | @groovy.transform.Canonical 88 | class Record { 89 | Exception stackTrace 90 | String threadName 91 | long time 92 | File file 93 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 165 | if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then 166 | cd "$(dirname "$0")" 167 | fi 168 | 169 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 170 | -------------------------------------------------------------------------------- /src/main/groovy/me/champeau/gradle/buildscans/RecipeCompiler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.gradle.buildscans; 17 | 18 | import com.gradle.scan.plugin.BuildScanExtension; 19 | import groovy.lang.GroovyClassLoader; 20 | import org.codehaus.groovy.ast.ClassHelper; 21 | import org.codehaus.groovy.ast.ClassNode; 22 | import org.codehaus.groovy.ast.GenericsType; 23 | import org.codehaus.groovy.ast.MethodNode; 24 | import org.codehaus.groovy.ast.Parameter; 25 | import org.codehaus.groovy.classgen.GeneratorContext; 26 | import org.codehaus.groovy.control.CompilationFailedException; 27 | import org.codehaus.groovy.control.CompilePhase; 28 | import org.codehaus.groovy.control.CompilerConfiguration; 29 | import org.codehaus.groovy.control.SourceUnit; 30 | import org.codehaus.groovy.control.customizers.CompilationCustomizer; 31 | import org.codehaus.groovy.runtime.ResourceGroovyMethods; 32 | import org.gradle.api.invocation.Gradle; 33 | 34 | import java.io.File; 35 | import java.io.IOException; 36 | import java.io.UnsupportedEncodingException; 37 | import java.lang.reflect.Modifier; 38 | import java.math.BigInteger; 39 | import java.net.MalformedURLException; 40 | import java.net.URISyntaxException; 41 | import java.net.URL; 42 | import java.net.URLClassLoader; 43 | import java.security.MessageDigest; 44 | import java.security.NoSuchAlgorithmException; 45 | import java.util.Map; 46 | import java.util.concurrent.TimeUnit; 47 | 48 | public class RecipeCompiler extends CompilationCustomizer { 49 | private static final byte VERSION = 1; 50 | 51 | private static final ClassNode BUILDSCAN_TYPE = ClassHelper.make(BuildScanExtension.class); 52 | private static final ClassNode GRADLE_TYPE = ClassHelper.make(Gradle.class); 53 | private static final ClassNode MAP_TYPE = ClassHelper.make(Map.class); 54 | 55 | public RecipeCompiler() { 56 | super(CompilePhase.CONVERSION); 57 | } 58 | 59 | @Override 60 | public void call(final SourceUnit sourceUnit, final GeneratorContext generatorContext, final ClassNode classNode) throws CompilationFailedException { 61 | if (classNode.isScript()) { 62 | classNode.setName(recipeClassName(classNode.getName())); 63 | classNode.addInterface(ClassHelper.make(Recipe.class)); 64 | classNode.setSuperClass(ClassHelper.OBJECT_TYPE); 65 | MethodNode run = classNode.getMethods("run").get(0); 66 | classNode.getMethods().remove(run); 67 | ClassNode mapType = MAP_TYPE.getPlainNodeReference(); 68 | mapType.setGenericsTypes(new GenericsType[]{ 69 | new GenericsType(ClassHelper.STRING_TYPE), 70 | new GenericsType(ClassHelper.STRING_TYPE), 71 | }); 72 | MethodNode apply = new MethodNode("apply", 73 | Modifier.PUBLIC, 74 | ClassHelper.VOID_TYPE, 75 | new Parameter[]{ 76 | new Parameter(GRADLE_TYPE, "gradle"), 77 | new Parameter(BUILDSCAN_TYPE, "buildScan"), 78 | new Parameter(mapType, "params"), 79 | }, 80 | ClassNode.EMPTY_ARRAY, 81 | run.getCode()); 82 | classNode.addMethod(apply); 83 | } 84 | } 85 | 86 | public static String recipeClassName(String recipe) { 87 | return "me.champeau.gradle.buildscans.GeneratedRecipe_" + recipe.replaceAll("[^a-zA-Z0-9]", "_"); 88 | } 89 | 90 | public static Class compileOrGetFromCache(Gradle gradle, String recipeName, URL url, boolean cache) throws NoSuchAlgorithmException, 91 | URISyntaxException, IOException, ClassNotFoundException { 92 | File recipeDir = recipeDir(gradle, url); 93 | if (!cache && recipeDir.exists()) { 94 | // cleanup 95 | ResourceGroovyMethods.deleteDir(recipeDir); 96 | } 97 | if (!cache || recipeDir.mkdir()) { 98 | long sd = System.nanoTime(); 99 | try { 100 | return compileToDir(recipeName, url, recipeDir, cache); 101 | } finally { 102 | gradle.getRootProject().getLogger().debug("Compilation took " + (TimeUnit.MILLISECONDS.convert(System.nanoTime()-sd, TimeUnit.NANOSECONDS)) + "ms"); 103 | } 104 | } 105 | return loadFromDir(recipeName, recipeDir); 106 | } 107 | 108 | @SuppressWarnings("unchecked") 109 | private static Class loadFromDir(final String recipeName, final File recipeDir) throws MalformedURLException, ClassNotFoundException { 110 | URLClassLoader ucl = new URLClassLoader(new URL[] {recipeDir.toURI().toURL()}, RecipeCompiler.class.getClassLoader()); 111 | return (Class) ucl.loadClass(recipeClassName(recipeName)); 112 | } 113 | 114 | @SuppressWarnings("unchecked") 115 | private static Class compileToDir(final String recipeName, final URL url, final File recipeDir, final boolean cache) throws IOException { 116 | System.out.println("Compiling recipe " + recipeName +"..."); 117 | CompilerConfiguration config = new CompilerConfiguration(); 118 | if (cache) { 119 | config.setTargetDirectory(recipeDir); 120 | } 121 | config.addCompilationCustomizers(new RecipeCompiler()); 122 | GroovyClassLoader gcl = new GroovyClassLoader(RecipeCompiler.class.getClassLoader(), config); 123 | return (Class) gcl.parseClass(ResourceGroovyMethods.getText(url, "UTF-8"), recipeName + ".groovy"); 124 | } 125 | 126 | private static File recipeDir(final Gradle gradle, final URL url) throws NoSuchAlgorithmException, UnsupportedEncodingException, URISyntaxException { 127 | MessageDigest digest = MessageDigest.getInstance("SHA1"); 128 | digest.update(url.toURI().toString().getBytes("utf-8")); 129 | digest.update(VERSION); 130 | File gradleUserHomeDir = gradle.getGradleUserHomeDir(); 131 | File recipesHomeDir = new File(gradleUserHomeDir, "buildScanRecipes"); 132 | if (!recipesHomeDir.exists()) { 133 | recipesHomeDir.mkdirs(); 134 | } 135 | String hex = new BigInteger(1, digest.digest()).toString(16); 136 | return new File(recipesHomeDir, hex); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Build Scans Recipes Gradle Plugin 2 | :buildscan-version: 1.10.1 3 | :plugin-version: 0.2.3 4 | 5 | image:http://img.shields.io/travis/melix/gradle-buildscan-recipes/master.svg["Build Status (travis)", link="https://travis-ci.org/melix/gradle-buildscan-recipes"] 6 | image:http://img.shields.io/badge/license-ASF2-blue.svg["Apache License 2", link="http://www.apache.org/licenses/LICENSE-2.0.txt"] 7 | 8 | This plugin enhances the information published in https://scans.gradle.com[Build Scans] with custom values and tags. 9 | 10 | This https://scans.gradle.com/s/wjgfuwn447g2o[sample Build Scan] was generated using this plugin, for example, on Travis CI. 11 | This https://scans.gradle.com/s/sburrg2uho64k[other one] was generated locally, using this plugin too: you will notice 12 | that it is marked as `dirty` and shows the git status. 13 | 14 | == Usage 15 | 16 | Build script snippet for use in all Gradle versions: 17 | [source,groovy] 18 | [subs="attributes"] 19 | .build.gradle 20 | ---- 21 | buildscript { 22 | repositories { 23 | maven { 24 | url "https://plugins.gradle.org/m2/" 25 | } 26 | } 27 | 28 | dependencies { 29 | classpath "com.gradle:build-scan-plugin:{buildscan-version}" 30 | classpath 'me.champeau.gradle:buildscan-recipes-plugin:{plugin-version}' 31 | } 32 | } 33 | 34 | apply plugin: 'com.gradle.build-scan' 35 | apply plugin: me.champeau.gradle.buildscans.RecipesPlugin 36 | 37 | buildScan { 38 | licenseAgreementUrl = 'https://gradle.com/terms-of-service' 39 | licenseAgree = 'yes' 40 | } 41 | 42 | buildScanRecipes { 43 | recipes 'git-commit', 'git-status', 'teamcity', 'gc-stats' 44 | } 45 | ---- 46 | 47 | Build script snippet for new, incubating, plugin mechanism introduced in Gradle 2.1: 48 | [source,groovy] 49 | [subs="attributes"] 50 | .build.gradle 51 | ---- 52 | plugins { 53 | id 'com.gradle.build-scan' version '{buildscan-version}' 54 | id 'me.champeau.buildscan-recipes' version '{plugin-version}' 55 | } 56 | ---- 57 | 58 | == Configuration 59 | 60 | The plugin provides a number of _recipes_ that can be used to automatically add tags and custom values to Build Scans. 61 | The following recipes are available: 62 | 63 | * `git-commit` : Tries to determine the Git commit ID for the build 64 | * `git-status` : Outputs the result of `git status`, and tags the build as `dirty` if there are uncommitted or changes files locally 65 | * `git-diff-to-gist` : Pushes the result of `git diff` to a Gist and creates a link in the build scan to this diff 66 | * `teamcity` : Detects a build running under https://www.jetbrains.com/teamcity/[TeamCity], tags it as `CI`, and adds a link to the build 67 | * `travis-ci` : Detects a build running under https://travis-ci.org[Travis CI], tags it as `CI` and outputs information about the Travis environment 68 | * `disk-usage` : Shows information about Gradle disk usage. It will scan your `$HOME/.gradle` directory as well as your project directory. Note that depending on your disk type, OS, disk usage and file system, this can turn to be very slow and add a non negligeable time at build start. You should therefore use this recipe with care. This recipe only works on Linux and Mac OS. 69 | * `gc-stats` : Shows statistics about garbage collection happening during your build. 70 | * `gist` : Allows to use a recipe found in a GitHub Gist 71 | * `remote` : Allows to use a recipe found in any remote URL 72 | * `file-leak-detection` : Attemps to identify tasks which leak file handles (experimental) 73 | 74 | === Recipe specific parameters 75 | 76 | ==== `git-commit` 77 | 78 | The `git-commit` recipe accepts an optional `baseUrl` parameter. If specified, a link to the commit will also be created. For example: 79 | 80 | [source,groovy] 81 | ---- 82 | buildScanRecipes { 83 | recipe 'git-commit', baseUrl: 'https://github.com/melix/gradle-buildscan-recipes/tree' 84 | } 85 | ---- 86 | 87 | ==== `git-diff-to-gist` 88 | 89 | This recipe will take the result of the `git diff` command and paste it as a Gist. By default, this will create a private 90 | Gist, but still be aware than anyone with the link can view the diff. For this recipe to work, you need a GitHub account 91 | and an https://github.com/settings/tokens[API Token]. We *strongly* recommend that you create a token specifically for 92 | this recipe, limited to the `gist` access rights. The recipe takes 3 optional parameters: 93 | 94 | * `user` is your GitHub username 95 | * `token` is your API token. If not specified explicitly, will look for a project property named "gistToken" 96 | * `public` (by default `"false"`) tells if the Gist should be public (`"true"`) or private (`"false"`). 97 | 98 | All those arguments are optional. For security reasons, we recommend not setting the `user` and `token` directly in your 99 | build file. For this reason, the recipe will by default search for a project property `gistUsername` for the username 100 | and `gistToken` for the token. You can set those properties in your home directory `gradle.properties` file: 101 | 102 | .~/gradle.properties 103 | ``` 104 | gistUsername=buildscansrulez 105 | gistToken=github-generated-api-token 106 | ``` 107 | 108 | If you do this, the simples configuration looks like this: 109 | 110 | ``` 111 | buildScanRecipes { 112 | ... 113 | recipes 'git-diff-to-gist' 114 | } 115 | ``` 116 | 117 | In addition, if you don't want to systematically publish the diff, you can run with `-PnoGist`: 118 | 119 | ``` 120 | ./gradlew -PnoGist someTask 121 | ``` 122 | 123 | ==== `teamcity` 124 | 125 | The `teamcity` recipe accepts 2 optional parameters: 126 | 127 | * `baseUrl` needs to be set to the CI server base URL 128 | * `guest`, if set, will generate a URL with anonymous login 129 | 130 | ==== `file-leak-detection` 131 | 132 | This recipe will attempt to detect which tasks leak file handles (a leaking file handle is often referred to a file which is open 133 | but never closed). If a file is opened during a build, but never closed, subsequent builds may fail, dependending on your operating 134 | system, because of those. Typically, your build could fail removing a jar file because the previous build opened it, but never closed 135 | it. This is problematic in Gradle if you use the daemon, which is now on by default. So it is important to track which tasks 136 | do not properly close their files, so that you can either report to the plugin author (for tasks defined in a plugin) or fix your 137 | build. 138 | 139 | The recipe works by loading http://file-leak-detector.kohsuke.org/[a Java agent] that will track file handles. If leaking files 140 | are detected, the build scan will report a `LEAKING FILE HANDLES` tag, and in custom values you will see each file that leaks a 141 | file, with the files and details as to where it happens (for debugging). 142 | 143 | For example, imagine the following build file: 144 | 145 | [source,groovy] 146 | ---- 147 | task foo1 { 148 | doLast { 149 | def fos = file('/tmp/foo1.txt').newOutputStream() 150 | } 151 | } 152 | 153 | task foo2 { 154 | doLast { 155 | def fos = file('/tmp/foo2.txt').newOutputStream() 156 | fos.close() 157 | } 158 | } 159 | ---- 160 | 161 | The `foo1` task is opening a file, but never closes it. The `foo2` task, on the contrary, closes it properly. Here's the 162 | resulting https://scans.gradle.com/s/dpvxomrln43bo/custom-values[build scan]. The details file contains the stack trace to 163 | the file open, so you could see: 164 | 165 | ``` 166 | ...snip... 167 | at build_733t8o56jehucx5ms8s6ul86j$_run_closure6$_closure11.doCall(/home/cchampeau/DEV/PROJECTS/GITHUB/buildscan-recipes-plugin/test/build.gradle:67) 168 | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 169 | ...snip... 170 | 171 | ``` 172 | 173 | And therefore find out where the file is opened and never closed. 174 | 175 | This recipe is still experimental, as it could potentially detect files opened by Gradle, which are closed _after_ a build is finished. 176 | 177 | If the recipe is applied, it is still possible to avoid loading the agent and paying the overhead of detection by using `-PdisableLeakDetection`. 178 | 179 | === External recipes 180 | 181 | In addition to the bundled recipes, it is possible to use recipes on external resources. The first recipe allows to 182 | reference another recipe found in a GitHub Gist: 183 | 184 | ==== `gist` 185 | 186 | ``` 187 | buildScanRecipes { 188 | recipe 'gist', user: 'melix', 189 | id: '5944cb701d6c9650ecaccccd4642ea5f', 190 | rev: '4b40b45559929ee2baaa7599e29dd78e51c3843a', 191 | recipe: 'my-recipe', 192 | // external recipe parameters 193 | name: 'Bob' 194 | } 195 | ``` 196 | 197 | The `gist` recipe accepts the following parameters: 198 | 199 | - `user`: username of the gist owner 200 | - `id` id of the gist 201 | - `recipe` name of the file containing the gist 202 | - `rev` revision of the gist. If absent, the compiled recipe will not be cached. 203 | 204 | Any additional parameter will be passed to the remote recipe (here, the `name` parameter). 205 | 206 | If the `rev` parameter is present, we're pointing at 207 | a specific version of the Gist, so the recipe will fetch it only once, compile it and cache it in the 208 | `$USER_HOME/.gradle/buildScanRecipes` directory. If it is absent, it is going to point to `HEAD`, meaning that 209 | each time the recipe is called, it's going to fetch it remotely, compile it, but it will *not* cache the result. 210 | 211 | ==== `remote` 212 | 213 | As an alternative to the `gist` recipe, you can simply reference any remote URL, using the `remote` recipe: 214 | 215 | ``` 216 | recipe 'remote', 217 | url: 'https://gist.githubusercontent.com/melix/5944cb701d6c9650ecaccccd4642ea5f/raw//my-recipe.groovy', 218 | cache: 'true', 219 | // external recipe parameters below: 220 | name: 'Bob' 221 | ``` 222 | 223 | This recipe will fetch the remote recipe, compile it, and cache it if and only if the `cache` flag is set to `true`. The 224 | recipe accepts 2 parameters: 225 | 226 | * `url` : the URL of the script, pointing at a Groovy recipe script 227 | * `cache` : if `true`, the URL will only be fetched the first time, then it will compile the script and subsequent 228 | executions will reuse the result, avoiding a network call and compile phase. 229 | 230 | Any additional parameter will be passed to the remote recipe (here, the `name` parameter). 231 | 232 | It's worth noting that this recipe can be used to compile local recipes too, or to test recipes before you publish 233 | them on a Gist or anywhere else: 234 | 235 | ``` 236 | buildScanRecipes { 237 | recipe 'remote', 238 | url: file('recipes/my-awesome-recipe.groovy').toURL(), 239 | // local recipe parameters below 240 | name: 'Bob' 241 | } 242 | ``` 243 | 244 | == Adding recipes 245 | 246 | Recipes are written in Groovy and can be found in the https://github.com/melix/gradle-buildscan-recipes/tree/master/src/recipes[recipes] directory. Note that the rules are _statically compiled_ and expose 2 variables: 247 | 248 | * `buildScan`, of type `BuildScanExtension`, providing ability to tag a build scan, add a link, or add custom values 249 | * `gradle`, giving access to the `Gradle` instance of the build 250 | * `params`, a `Map` of parameters (non-null, but maybe empty) 251 | 252 | Recipes are bundled with this plugin. 253 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright ${new Date().format('yyyy')} ${pluginAuthor} 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. --------------------------------------------------------------------------------