diff --git a/.gitignore b/.gitignore index 6a15e2a0..a680191b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,15 @@ .gradle annotations .idea/* -!.idea/modules.xml -buildScript +*.iml +nonBuildScript kobaltBuild -test-output local.properties classes libs .kobalt/ -build/ +out +.DS_Store +lib/kotlin-* +build +.history diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 82188f15..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 258899ce..00000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: java - -jdk: - - oraclejdk8 - -install: true - -script: ./kobaltw test --log 1 diff --git a/README.md b/README.md index 29bc3bd1..d5d7cbe0 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ # Kobalt +[](https://teamcity.jetbrains.com/project.html?projectId=OpenSourceProjects_Kobalt&tab=projectOverview) + + Kobalt is a universal build system. To build it: ``` -./kobaltw assemble +$ ./kobaltw assemble ``` Please see [the web site](http://beust.com/kobalt/) for the full documentation. + diff --git a/TODO.md b/TODO.md index 25d795b2..c4625308 100644 --- a/TODO.md +++ b/TODO.md @@ -1,36 +1,33 @@ To do: -- [ ] The test runner only selects classes with a parameterless constructor, which works for JUnit but not for TestNG - factories -- [ ] Add a "Auto complete Build.kt" menu in the plug-in -- [ ] "All artifacts successfully uploaded" is shown before the upload is actually done -- [ ] use groupId/artifactId +Android: + + +General + +- [ ] KFiles.findSourceFiles should cache its results +- [ ] createCompilerAction should calculate source files once and for all, including contributors and interceptors +- [ ] Need a -resolve with no dependency which gives the tree for the whole project +- [ ] If jitpack specified with http and not https, 301 is not handled correctly +- [ ] Apt should run from serviceloader - [ ] Console mode with watch service, so recompilation can occur as soon as a source file is modified - [ ] ProjectGenerator: support migration from pom.xml (starting with dependencies) - [ ] Specify where to upload snapshots -- [ ] Upload in a thread pool - [ ] repos() must appear before plugins(): fix that -- [ ] Support version ranges - [ ] Generate .idea and other IDEA files -- [ ] logs for users should not show any timestamp, class file or thread id, this should only be in --dev mode - [ ] Fetch .pom with DynamicGraph - [ ] Centralize all the executors - [ ] Archetypes (e.g. "--initWith kobalt-plug-in") -- [ ] Compile TestNG (including generating Version.java and OSGi headers) +- [ ] Compile TestNG (last piece missing: OSGi headers) - [ ] Support additional .kt files in ~/.kobalt/src -- [ ] generateArchive() should use Path instead of File - [ ] --init: import dependencies from build.gradle - [ ] --init: also extract kobalt.bat (or generate it along with kobaltw) -- [ ] Bug: --tasks displays multiple tasks when there are multiple projects -- [ ] Bug: ./kobaltw --dryRun kobalt:uploadJcenter runs "kobalt-wrapper:clean" twice -- [ ] Replace File with java.nio.Files and Path -- [ ] Create a wiki page for plugins -- [ ] Make kobaltw executable in the zip file - [ ] Encapsulate ProcessBuilder code -- [ ] --resolve Done: +- [x] Dex dependencies into kobaltBuild/intermediates/pre-dexed and preserve those across builds +- [x] Compile with javax.tool - [x] Android: multiple -source/-target flags - [x] Dokka: allow multiple format outputs e.g `outputFormat("html", "javadoc")` - [x] Finish abstracting everything in `JvmCompilerPlugin` @@ -70,5 +67,24 @@ just a straight Java class with minimal dependencies for fast start up - [x] Upload non maven files - [x] Jar packaging: include the jar in the jar (not supported by JarFile) - [x] Encapsulate BuildFile for better log messages +- [x] The test runner only selects classes with a parameterless constructor, which works for JUnit but not for TestNG +- [x] Add a "Auto complete Build.kt" menu in the plug-in +- [x] "All artifacts successfully uploaded" is shown before the upload is actually done +- [x] use groupId/artifactId + factories +- [x] Support version ranges +- [x] Upload in a thread pool +- [x] logs for users should not show any timestamp, class file or thread id, this should only be in --dev mode +- [x] Bug: --tasks displays multiple tasks when there are multiple projects +- [x] Bug: ./kobaltw --dryRun kobalt:uploadJcenter runs "kobalt-wrapper:clean" twice +- [x] Create a wiki page for plugins +- [x] Make kobaltw executable in the zip file +- [x] --resolve +- [x] Move the calculated applicationId back into the merged AndroidManifest.xml +- [x] Dex from android builder +- [x] Keep exploded aars between runs +- [x] aars keep being refetched +- [x] See if there is an android manifest file in builder +- [x] Auto add variant diff --git a/build-travis.sh b/build-travis.sh new file mode 100755 index 00000000..c7cb1152 --- /dev/null +++ b/build-travis.sh @@ -0,0 +1,8 @@ +ulimit -s 1082768 + +#java -Xmx2048m -jar $(dirname $0)/kobalt/wrapper/kobalt-wrapper.jar $* clean +java -Xmx2048m -jar $(dirname $0)/kobalt/wrapper/kobalt-wrapper.jar $* clean assemble test --parallel + + + + diff --git a/build.gradle b/build.gradle index f1f94066..3f0053cc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,90 +1,58 @@ -buildscript { - ext.kotlin_version = '1.0.0-beta-2423' +allprojects { + group = 'com.beust' + version = '1.1.0' +} + +subprojects { + apply plugin: 'java' + apply plugin: 'maven-publish' + + ext { + bndlib = '3.5.0' + findbugs = '3.0.2' + groovy = '2.4.12' + gson = '2.8.2' + guice = '4.2.2' + inject = '1' + jaxb = '2.3.0' + jcommander = '1.72' + kotlin = '1.2.71' + maven = '3.5.2' + mavenResolver = '1.1.0' + okhttp = '3.9.1' + okio = '1.13.0' + retrofit = '2.3.0' + slf4j = '1.7.3' + spark = '2.6.0' + testng = '6.12' + + junit = '4.12' + junitJupiter = '5.1.0' + junitPlatform = '1.1.0' + } repositories { + mavenCentral() + mavenLocal() jcenter() - } - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlin_version}" - } -} + maven { + url = 'https://dl.bintray.com/cbeust/maven' + } -plugins { - id "com.jfrog.bintray" version "1.2" -} - -version = '0.121' - -//apply plugin: 'java' -apply plugin: 'kotlin' -apply plugin: 'com.jfrog.bintray' - -apply from: 'gradle/publishing.gradle' - -repositories { - jcenter() -} - -dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}", - "org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlin_version}", - 'org.jetbrains.kotlinx:kotlinx.dom:0.0.4', - 'org.jetbrains.dokka:dokka-fatjar:0.9.2', -// "org.jetbrains.kotlin:kotlin-compiler:${kotlin_version}", - - 'com.beust:jcommander:1.48', - 'com.squareup.okhttp:okhttp:2.5.0', - 'org.jsoup:jsoup:1.8.3', - 'com.google.inject:guice:4.0', - 'com.google.inject.extensions:guice-assistedinject:4.0', - 'javax.inject:javax.inject:1', - 'com.google.guava:guava:19.0-rc2', - 'org.apache.maven:maven-model:3.3.3', - 'com.github.spullara.mustache.java:compiler:0.9.1', - 'io.reactivex:rxjava:1.0.16', - 'com.google.code.gson:gson:2.4', - 'com.squareup.retrofit:retrofit:1.9.0', - 'com.squareup.okio:okio:1.6.0', - project("modules/wrapper") - -// compile files("/Users/beust/.kobalt/repository/com/beust/kobalt-example-plugin/build/libs/kobalt-example-plugin.jar") - testCompile 'org.testng:testng:6.9.9' -// testCompile 'junit:junit:4.12' -} - -task sourceJar(type: Jar) { - group 'Build' - description 'An archive of the source code' - classifier 'sources' - from sourceSets.main.allSource -} - -artifacts { - file('build/libs/kobalt.jar') - sourceJar -} - -test { - useTestNG() - beforeTest { descriptor -> - logger.lifecycle(" Running test: " + descriptor) - } -} - -compileKotlin { - kotlinOptions.suppressWarnings = true -} - -apply plugin: 'application' -mainClassName = 'com.beust.kobalt.KobaltPackage' - -jar { - manifest { - attributes "Main-Class": "$mainClassName" + maven { + url = 'https://repo.maven.apache.org/maven2' + } } - from { - configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + sourceCompatibility = '1.7' + + task sourcesJar(type: Jar) { + from sourceSets.main.allJava + archiveClassifier = 'sources' + } + + task javadocJar(type: Jar) { + from javadoc + archiveClassifier = 'javadoc' } } - diff --git a/dist/kobaltw b/dist/kobaltw new file mode 100755 index 00000000..333738df --- /dev/null +++ b/dist/kobaltw @@ -0,0 +1,11 @@ +#!/usr/bin/env sh + +case "$(uname)" in + CYGWIN*) DIRNAME=$(cygpath -d "$(dirname "$(readlink -f "$0")")");; + Darwin*) DIRNAME=$(dirname "$(readlink "$0")");; + *) DIRNAME=$(dirname "$(readlink -f "$0")");; +esac +if [ "$DIRNAME" = "." ]; then + DIRNAME="$(dirname "$0")" +fi +java -jar "${DIRNAME}/../kobalt/wrapper/kobalt-wrapper.jar" $* \ No newline at end of file diff --git a/dist/kobaltw.bat b/dist/kobaltw.bat new file mode 100644 index 00000000..2d95345e --- /dev/null +++ b/dist/kobaltw.bat @@ -0,0 +1,4 @@ +@echo off +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +java -jar "%~dp0/../kobalt/wrapper/kobalt-wrapper.jar" %* diff --git a/gradle/buildWithTravis.sh b/gradle/buildWithTravis.sh deleted file mode 100644 index 9f0ddf10..00000000 --- a/gradle/buildWithTravis.sh +++ /dev/null @@ -1 +0,0 @@ -../gradlew check diff --git a/gradle/publishing.gradle b/gradle/publishing.gradle deleted file mode 100644 index 3e9e7c42..00000000 --- a/gradle/publishing.gradle +++ /dev/null @@ -1,60 +0,0 @@ -import java.text.SimpleDateFormat - -Date buildTimeAndDate = new Date() -ext { - buildTime = new SimpleDateFormat('yyyy-MM-dd').format(buildTimeAndDate) - buildDate = new SimpleDateFormat('HH:mm:ss.SSSZ').format(buildTimeAndDate) -} - -apply plugin: 'maven-publish' -apply plugin: 'com.jfrog.bintray' - -jar { - manifest { - attributes( - 'Built-By': System.properties['user.name'], - 'Created-By': System.properties['java.version'] + " (" + System.properties['java.vendor'] + " " + System.getProperty("java.vm.version") + ")", - 'Build-Date': project.buildTime, - 'Build-Time': project.buildDate, - 'Specification-Title': project.name, - 'Specification-Version': project.version, - ) - } -} - -publishing { - publications { - mavenCustom(MavenPublication) { - from components.java - artifact sourceJar - - groupId 'com.beust' - artifactId 'kobalt' - version project.version - } - } -} - -task install(dependsOn: publishToMavenLocal) - -Properties properties = new Properties() -if (new File('local.properties').exists()) { - properties.load(project.rootProject.file('local.properties').newDataInputStream()) -} - -bintray { - user = properties.getProperty("bintray.user") - key = properties.getProperty("bintray.apikey") - publications = ['mavenCustom'] - pkg { - repo = 'maven' - name = 'klaxon' - desc = 'JSON parsing for Kotlin' - licenses = ['Apache-2.0'] - labels = ['kotlin'] - - version { - name = project.version //Bintray logical version name - } - } -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 30d399d8..5c2d1cf0 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5724a1d2..838e6bc8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Sun Oct 04 21:38:45 EDT 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-all.zip diff --git a/gradlew b/gradlew index 91a7e269..b0d6d0ab 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,20 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## ## @@ -6,47 +22,6 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -61,9 +36,49 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -90,7 +105,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -114,6 +129,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` @@ -154,11 +170,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 8a0b282a..9991c503 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,14 +24,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +62,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +75,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/kobalt-incremental-tasks.md b/kobalt-incremental-tasks.md new file mode 100644 index 00000000..f799a81a --- /dev/null +++ b/kobalt-incremental-tasks.md @@ -0,0 +1,78 @@ +Kobalt's incremental task algorithm is not based on timestamps but on checksums. + +You make a task incremental by declaring it `@IncrementalTask` instead of `@Task`. The only other difference is that instead of returning a `TaskResult`, incremental tasks return an `IncrementalTaskInfo`: + +``` +class IncrementalTaskInfo( + val inputChecksum: String?, + val outputChecksum: String?, + val task: (Project) -> TaskResult) +``` + +This class contains three fields: + +- A task closure, which is your effective task: `(Project) -> TaskResult` +- An input checksum (`String?`) +- An output checksum (`String?`) + +These checksums are numbers that each task calculates for their input and output. For example, the `"compile"` task calculates an MD5 checksum of all the source files. Similarly, the output checksum is for produced artifacts, e.g. checksum of `.class` files or `.jar` files, etc... + +Example of an incremental task: + +``` + @IncrementalTask(name = JvmCompilerPlugin.TASK_COMPILE, description = "Compile the project") + fun taskCompile(project: Project) : IncrementalTaskInfo { + val inputChecksum = Md5.toMd5Directories(project.sourceDirectories.map { + File(project.directory, it) + }) + return IncrementalTaskInfo( + inputChecksum = inputChecksum, + outputChecksum = "1", + task = { project -> doTaskCompile(project) } + ) + } +``` + +The advantage of checksums is that they take care of all the scenarios that would cause that task to run: + +- A file was modified +- A file was added +- A file was removed + +The output checksum covers the case where the input is unchanged but the output files were deleted or modified. If +both the input and output checksums match the previous run, it's extremely likely that the task has nothing to do. + +Another advantage of checksums is that they are generic and not necessarily tied to files. For example, a Kobalt task might perform some network operations and return a checksum based on a network result to avoid performing a more expensive operation (e.g. don't download a file from a server if it hasn't changed). + +Internally, Kobalt maintains information about all the checksums and tasks that it has seen in a file `.kobalt/build-info.json`. Whenever an incremental task is about to run, Kobalt compares its input and output checksums to the ones from the previous run and if any differs, that task is run. Otherwise, it's skipped. + +Example timings for Kobalt: + +| Task | First run | Second run | +| ---- | --------- | ---------- | +| kobalt-wrapper:compile | 627 ms | 22 ms | +| kobalt-wrapper:assemble | 9 ms | 9 ms | +| kobalt-plugin-api:compile | 10983 ms | 54 ms | +| kobalt-plugin-api:assemble | 1763 ms | 154 ms | +| kobalt:compile | 11758 ms | 11 ms | +| kobalt:assemble | 42333 ms | 2130 ms | +| | 70 seconds | 2 seconds | + +Android (u2020): + +| Task | First run | Second run | +| ---- | --------- | ---------- | +| u2020:generateRInternalDebug | 32350 ms | 1652 ms | +| u2020:compileInternalDebug | 3629 ms | 24 ms | +| u2020:retrolambdaInternalDebug | 668 ms | 473 ms | +| u2020:generateDexInternalDebug | 6130 ms |55 ms | +| u2020:signApkInternalDebug | 449 ms | 404 ms | +| u2020:assembleInternalDebug | 0 ms | 0 ms | +| | 43 seconds | 2 seconds | + + + + + + + diff --git a/kobalt.iml b/kobalt.iml deleted file mode 100644 index 80485be5..00000000 --- a/kobalt.iml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/kobalt/src/Build.kt b/kobalt/src/Build.kt index d8ecc25b..0d09844a 100644 --- a/kobalt/src/Build.kt +++ b/kobalt/src/Build.kt @@ -1,23 +1,61 @@ import com.beust.kobalt.* -import com.beust.kobalt.api.* +import com.beust.kobalt.api.Project import com.beust.kobalt.api.annotation.Task import com.beust.kobalt.plugin.application.application -import com.beust.kobalt.plugin.java.* -import com.beust.kobalt.plugin.kotlin.* +import com.beust.kobalt.plugin.java.javaCompiler +import com.beust.kobalt.plugin.kotlin.kotlinCompiler import com.beust.kobalt.plugin.packaging.assemble +import com.beust.kobalt.plugin.publish.autoGitTag +import com.beust.kobalt.plugin.publish.bintray import com.beust.kobalt.plugin.publish.github -import com.beust.kobalt.plugin.publish.jcenter -import com.beust.kobalt.test +import org.apache.maven.model.Developer +import org.apache.maven.model.License +import org.apache.maven.model.Model +import org.apache.maven.model.Scm import java.io.File import java.nio.file.Files import java.nio.file.Paths import java.nio.file.StandardCopyOption +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream -val r = repos("http://dl.bintray.com/kotlin/kotlinx.dom") +val bs = buildScript { + repos("https://dl.bintray.com/cbeust/maven") +} -val wrapper = javaProject { +object Versions { + val kotlin = "1.2.71" + val okhttp = "3.9.1" + val okio = "1.13.0" + val retrofit = "2.3.0" + val gson = "2.8.2" + val guice = "4.2.2" + val maven = "3.5.2" + val mavenResolver = "1.1.0" + val slf4j = "1.7.3" + val aether = "1.0.2.v20150114" + val testng = "6.12" + val jcommander = "1.72" + + // JUnit 5 + val junit = "4.12" + val junitPlatform = "1.1.0" + val junitJupiter = "5.1.0" +} + +fun mavenResolver(vararg m: String) + = m.map { "org.apache.maven.resolver:maven-resolver-$it:${Versions.mavenResolver}" } + .toTypedArray() + +fun aether(vararg m: String) + = m.map { "org.eclipse.aether:aether-$it:${Versions.aether}" } + .toTypedArray() + +val wrapper = project { name = "kobalt-wrapper" + group = "com.beust" + artifactId = name version = readVersion() directory = "modules/wrapper" @@ -26,6 +64,7 @@ val wrapper = javaProject { } assemble { + jar { } jar { name = projectName + ".jar" manifest { @@ -34,53 +73,63 @@ val wrapper = javaProject { } } - productFlavor("dev") { - } - - buildType("debug") { - } - application { mainClass = "com.beust.kobalt.wrapper.Main" } + + bintray { + publish = true + sign = true + } + + pom = createPom(name, "Wrapper for Kobalt") } -val kobalt = kotlinProject(wrapper) { - name = "kobalt" +val kobaltPluginApi = project { + name = "kobalt-plugin-api" group = "com.beust" artifactId = name version = readVersion() + directory = "modules/kobalt-plugin-api" description = "A build system in Kotlin" - url = "http://beust.com/kobalt" - licenses = arrayListOf(License("Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0")) - scm = Scm(url = "http://github.com/cbeust/kobalt", - connection = "https://github.com/cbeust/kobalt.git", - developerConnection = "git@github.com:cbeust/kobalt.git") + url = "https://beust.com/kobalt" - dependenciesTest { - compile("org.testng:testng:6.9.9") - } + pom = createPom(name, "A build system in Kotlin") dependencies { - compile("org.jetbrains.kotlin:kotlin-stdlib:1.0.0-beta-2423", - "org.jetbrains.kotlin:kotlin-compiler-embeddable:1.0.0-beta-2423", - "org.jetbrains.dokka:dokka-fatjar:0.9.2", - "org.jetbrains.kotlinx:kotlinx.dom:0.0.4", - - "com.beust:jcommander:1.48", - "com.squareup.okhttp:okhttp:2.5.0", - "org.jsoup:jsoup:1.8.3", - "com.google.inject:guice:4.0", - "com.google.inject.extensions:guice-assistedinject:4.0", + compile( + "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}", + "com.google.inject:guice:${Versions.guice}", + "com.google.inject.extensions:guice-assistedinject:4.1.0", "javax.inject:javax.inject:1", - "com.google.guava:guava:19.0-rc2", - "org.apache.maven:maven-model:3.3.3", - "com.github.spullara.mustache.java:compiler:0.9.1", - "io.reactivex:rxjava:1.0.16", - "com.google.code.gson:gson:2.4", - "com.squareup.retrofit:retrofit:1.9.0", - "com.squareup.okio:okio:1.6.0" - ) + "com.google.guava:guava:27.0.1-jre", + "org.apache.maven:maven-model:${Versions.maven}", + "io.reactivex:rxjava:1.3.3", + "com.squareup.okio:okio:${Versions.okio}", + "com.google.code.gson:gson:${Versions.gson}", + "com.squareup.okhttp3:okhttp:${Versions.okhttp}", + "com.squareup.retrofit2:retrofit:${Versions.retrofit}", + "com.squareup.retrofit2:converter-gson:${Versions.retrofit}", + "com.beust:jcommander:${Versions.jcommander}", + "org.eclipse.jgit:org.eclipse.jgit:4.9.0.201710071750-r", + "org.slf4j:slf4j-simple:${Versions.slf4j}", + *mavenResolver("api", "spi", "util", "impl", "connector-basic", "transport-http", "transport-file"), + "org.apache.maven:maven-aether-provider:3.3.9", + "org.testng.testng-remote:testng-remote:1.3.2", + "org.testng:testng:${Versions.testng}", + "org.junit.platform:junit-platform-surefire-provider:${Versions.junitPlatform}", + "org.junit.platform:junit-platform-runner:${Versions.junitPlatform}", + "org.junit.platform:junit-platform-engine:${Versions.junitPlatform}", + "org.junit.platform:junit-platform-console:${Versions.junitPlatform}", + "org.junit.jupiter:junit-jupiter-engine:${Versions.junitJupiter}", + "org.junit.vintage:junit-vintage-engine:${Versions.junitJupiter}", + "org.apache.commons:commons-compress:1.15", + "commons-io:commons-io:2.6", + + // Java 9 + "javax.xml.bind:jaxb-api:2.3.0" + ) + exclude(*aether("impl", "spi", "util", "api")) } @@ -91,64 +140,187 @@ val kobalt = kotlinProject(wrapper) { attributes("Main-Class", "com.beust.kobalt.MainKt") } } - zip { - include("kobaltw") - include(from("$buildDirectory/libs"), to("kobalt/wrapper"), - "$projectName-$version.jar") - include(from("modules/wrapper/$buildDirectory/libs"), to("kobalt/wrapper"), - "$projectName-wrapper.jar") - } - } - -// install { -// libDir = "lib-test" -// } - - test { - args("-log", "1", "src/test/resources/testng.xml") } kotlinCompiler { - args("-nowarn") + args("nowarn") } - dokka { - outputFormat = "markdown" + bintray { + publish = true + } +} + +val kobaltApp = project(kobaltPluginApi, wrapper) { + name = "kobalt" + group = "com.beust" + artifactId = name + version = readVersion() + + dependencies { + // Used by the plugins + compile("org.jetbrains.kotlin:kotlin-compiler-embeddable:${Versions.kotlin}") + + // Used by the main app + compile( + "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}", + "com.github.spullara.mustache.java:compiler:0.9.5", + "javax.inject:javax.inject:1", + "com.google.inject:guice:${Versions.guice}", + "com.google.inject.extensions:guice-assistedinject:${Versions.guice}", + "com.beust:jcommander:${Versions.jcommander}", + "org.apache.maven:maven-model:${Versions.maven}", + "com.google.code.findbugs:jsr305:3.0.2", + "com.google.code.gson:gson:${Versions.gson}", + "com.squareup.retrofit2:retrofit:${Versions.retrofit}", + "com.squareup.retrofit2:converter-gson:${Versions.retrofit}", +// "com.squareup.okhttp3:okhttp-ws:3.4.2", + "biz.aQute.bnd:biz.aQute.bndlib:3.5.0", + *mavenResolver("spi"), + + "com.squareup.okhttp3:logging-interceptor:3.9.0", + + "com.sparkjava:spark-core:2.6.0", + "org.codehaus.groovy:groovy:2.4.12", + + // Java 9 + "javax.xml.bind:jaxb-api:2.3.0", + "com.sun.xml.bind:jaxb-impl:2.3.0", + "com.sun.xml.bind:jaxb-core:2.3.0", + "com.sun.activation:javax.activation:1.2.0" + +// "org.eclipse.jetty:jetty-server:${Versions.jetty}", +// "org.eclipse.jetty:jetty-servlet:${Versions.jetty}", +// "org.glassfish.jersey.core:jersey-server:${Versions.jersey}", +// "org.glassfish.jersey.containers:jersey-container-servlet-core:${Versions.jersey}", +// "org.glassfish.jersey.containers:jersey-container-jetty-http:${Versions.jersey}", +// "org.glassfish.jersey.media:jersey-media-moxy:${Versions.jersey}", +// "org.wasabi:wasabi:0.1.182" + ) + } - dokka { - outputFormat = "html" + dependenciesTest { + compile("org.jetbrains.kotlin:kotlin-test:${Versions.kotlin}", + "org.testng:testng:${Versions.testng}", + "org.assertj:assertj-core:3.8.0", + *mavenResolver("util") + ) + } + + assemble { + mavenJars { + fatJar = true + manifest { + attributes("Main-Class", "com.beust.kobalt.MainKt") + } + } + zip { + val dir = "kobalt-$version" + val files = listOf( + "dist", "$dir/bin", "kobaltw", + "dist", "$dir/bin", "kobaltw.bat", + "$buildDirectory/libs", "$dir/kobalt/wrapper", "$projectName-$version.jar", + "modules/wrapper/$buildDirectory/libs", "$dir/kobalt/wrapper", "$projectName-wrapper.jar") + + (0 .. files.size - 1 step 3).forEach { i -> + include(from(files[i]), to(files[i + 1]), files[i + 2]) + } + + // Package the sources + val currentDir = Paths.get(".").toAbsolutePath().normalize().toString() + zipFolders("$currentDir/$buildDirectory/libs/all-sources/$projectName-$version-sources.jar", + "$currentDir/$directory/src/main/kotlin", + "$currentDir/${kobaltPluginApi.directory}/src/main/kotlin") + include(from("$buildDirectory/libs/all-sources"), to("$dir/kobalt/wrapper"), "$projectName-$version-sources.jar") + } + } + + kotlinCompiler { + args("nowarn") + } + + bintray { + publish = true } github { file("$buildDirectory/libs/$name-$version.zip", "$name/$version/$name-$version.zip") } - jcenter { - publish = true + test { + args("-log", "2", "src/test/resources/testng.xml") + } + + autoGitTag { + enabled = true + } +} + +fun zipFolders(zipFilePath: String, vararg foldersPath: String) { + val zip = Paths.get(zipFilePath) + Files.deleteIfExists(zip) + Files.createDirectories(zip.parent) + val zipPath = Files.createFile(zip) + ZipOutputStream(Files.newOutputStream(zipPath)).use { + foldersPath.map {Paths.get(it)}.forEach { folderPath -> + Files.walk(folderPath) + .filter { path -> !Files.isDirectory(path) } + .forEach { path -> + val zipEntry = ZipEntry(folderPath.relativize(path).toString()) + try { + it.putNextEntry(zipEntry) + Files.copy(path, it) + it.closeEntry() + } catch (e: Exception) { + } + } + } } } fun readVersion() : String { - val p = java.util.Properties() - var localFile = java.io.File("src/main/resources/kobalt.properties") - if (! localFile.exists()) { - localFile = File(homeDir("kotlin", "kobalt", "src/main/resources/kobalt.properties")) + val localFile = + listOf("src/main/resources/kobalt.properties", + homeDir("kotlin", "kobalt", "src/main/resources/kobalt.properties")).first { File(it).exists() } + with(java.util.Properties()) { + load(java.io.FileReader(localFile)) + return getProperty("kobalt.version") } - p.load(java.io.FileReader(localFile)) - return p.getProperty("kobalt.version") } -@Task(name = "copyVersionForWrapper", runBefore = arrayOf("assemble"), runAfter = arrayOf("compile"), description = "") +@Task(name = "copyVersionForWrapper", reverseDependsOn = arrayOf("assemble"), runAfter = arrayOf("clean")) fun taskCopyVersionForWrapper(project: Project) : TaskResult { if (project.name == "kobalt-wrapper") { val toString = "modules/wrapper/kobaltBuild/classes" File(toString).mkdirs() val from = Paths.get("src/main/resources/kobalt.properties") val to = Paths.get("$toString/kobalt.properties") - Files.copy(from, - to, - StandardCopyOption.REPLACE_EXISTING) + // Only copy if necessary so we don't break incremental compilation + if (! to.toFile().exists() || (from.toFile().readLines() != to.toFile().readLines())) { + Files.copy(from, + to, + StandardCopyOption.REPLACE_EXISTING) + } } return TaskResult() } + +fun createPom(projectName: String, projectDescription: String) = Model().apply { + name = projectName + description = projectDescription + url = "https://beust.com/kobalt" + licenses = listOf(License().apply { + name = "Apache-2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0" + }) + scm = Scm().apply { + url = "https://github.com/cbeust/kobalt" + connection = "https://github.com/cbeust/kobalt.git" + developerConnection = "git@github.com:cbeust/kobalt.git" + } + developers = listOf(Developer().apply { + name = "Cedric Beust" + email = "cedric@beust.com" + }) +} diff --git a/kobalt/wrapper/kobalt-wrapper.jar b/kobalt/wrapper/kobalt-wrapper.jar index e596833f..848fb463 100644 Binary files a/kobalt/wrapper/kobalt-wrapper.jar and b/kobalt/wrapper/kobalt-wrapper.jar differ diff --git a/kobalt/wrapper/kobalt-wrapper.properties b/kobalt/wrapper/kobalt-wrapper.properties index 73dc1b88..0ca8045f 100644 --- a/kobalt/wrapper/kobalt-wrapper.properties +++ b/kobalt/wrapper/kobalt-wrapper.properties @@ -1 +1 @@ -kobalt.version=0.307 \ No newline at end of file +kobalt.version=1.0.122 \ No newline at end of file diff --git a/kobaltw b/kobaltw index 1fd228db..c5186d5a 100755 --- a/kobaltw +++ b/kobaltw @@ -1,2 +1,2 @@ -#!/usr/bin/env bash -java -jar $(dirname $0)/kobalt/wrapper/kobalt-wrapper.jar $* +#!/usr/bin/env sh +java -jar "`dirname "$0"`/kobalt/wrapper/kobalt-wrapper.jar" $* diff --git a/kobaltw-test b/kobaltw-test new file mode 100755 index 00000000..2693c3aa --- /dev/null +++ b/kobaltw-test @@ -0,0 +1,8 @@ +#!/usr/bin/env sh +JAR=$(ls -1 -t kobaltBuild/libs/*.jar | grep -Ev "(sources|javadoc)" | head -1) +TEMPDIR=$(mktemp -d) +cp -pf "$JAR" "$TEMPDIR" +TEMPJAR=$TEMPDIR/$(basename "$JAR") +export KOBALT_JAR=$TEMPJAR +java -jar "$TEMPJAR" "$@" +rm -rf "$TEMPDIR" \ No newline at end of file diff --git a/kobaltw.bat b/kobaltw.bat index 8caea129..2887a567 100644 --- a/kobaltw.bat +++ b/kobaltw.bat @@ -1,2 +1,4 @@ -@echo off -java -jar %~dp0/kobalt/wrapper/kobalt-wrapper.jar %* +@echo off +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +java -jar "%DIRNAME%/kobalt/wrapper/kobalt-wrapper.jar" %* diff --git a/modules/kobalt-plugin-api/build.gradle b/modules/kobalt-plugin-api/build.gradle new file mode 100644 index 00000000..56085220 --- /dev/null +++ b/modules/kobalt-plugin-api/build.gradle @@ -0,0 +1,85 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.2.71' + id 'com.github.johnrengelman.shadow' version '5.0.0' +} + +dependencies { + implementation "biz.aQute.bnd:biz.aQute.bndlib:$bndlib" + implementation "com.google.code.findbugs:jsr305:$findbugs" + implementation "com.sparkjava:spark-core:$spark" + implementation "com.squareup.okhttp3:logging-interceptor:$okhttp" + implementation 'commons-io:commons-io:2.6' + implementation 'io.reactivex:rxjava:1.3.3' + implementation "javax.inject:javax.inject:$inject" + implementation "javax.xml.bind:jaxb-api:$jaxb" + implementation 'org.apache.commons:commons-compress:1.15' + implementation 'org.apache.maven:maven-aether-provider:3.3.9' + implementation "org.apache.maven.resolver:maven-resolver-api:$mavenResolver" + implementation "org.apache.maven.resolver:maven-resolver-connector-basic:$mavenResolver" + implementation "org.apache.maven.resolver:maven-resolver-impl:$mavenResolver" + implementation "org.apache.maven.resolver:maven-resolver-spi:$mavenResolver" + implementation "org.apache.maven.resolver:maven-resolver-transport-file:$mavenResolver" + implementation "org.apache.maven.resolver:maven-resolver-transport-http:$mavenResolver" + implementation "org.apache.maven.resolver:maven-resolver-util:$mavenResolver" + implementation "org.codehaus.groovy:groovy:$groovy" + implementation 'org.eclipse.jgit:org.eclipse.jgit:4.9.0.201710071750-r' + implementation "org.junit.jupiter:junit-jupiter-engine:$junitJupiter" + implementation "org.junit.platform:junit-platform-console:$junitPlatform" + implementation "org.junit.platform:junit-platform-engine:$junitPlatform" + implementation "org.junit.platform:junit-platform-runner:$junitPlatform" + implementation "org.junit.platform:junit-platform-surefire-provider:$junitPlatform" + implementation "org.junit.vintage:junit-vintage-engine:$junitJupiter" + implementation "org.slf4j:slf4j-simple:$slf4j" + implementation "org.testng:testng:$testng" + implementation 'org.testng.testng-remote:testng-remote:1.3.2' + implementation "com.beust:jcommander:$jcommander" + implementation "com.google.code.gson:gson:$gson" + implementation "com.google.inject:guice:$guice" + implementation "com.google.inject.extensions:guice-assistedinject:$guice" + implementation "com.squareup.okio:okio:$okio" + implementation "com.squareup.retrofit2:converter-gson:$retrofit" + implementation "com.squareup.retrofit2:retrofit:$retrofit" + implementation "org.apache.maven:maven-model:$maven" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin" +} + +shadowJar { + classifier = null +} + +test { + useTestNG() +} + +publishing { + publications { + shadow(MavenPublication) { publication -> + project.shadow.component(publication) + artifact sourcesJar + artifact javadocJar + + pom { + name = project.name + description = 'A build system in Kotlin' + url = 'https://beust.com/kobalt' + licenses { + license { + name = 'Apache-2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0' + } + } + developers { + developer { + name = 'Cedric Beust' + email = 'cedric@beust.com' + } + } + scm { + connection = 'scm:https://github.com/cbeust/kobalt.git' + developerConnection = 'scm:git@github.com:cbeust/kobalt.git' + url = 'https://github.com/cbeust/kobalt' + } + } + } + } +} diff --git a/modules/kobalt-plugin-api/pom.xml b/modules/kobalt-plugin-api/pom.xml new file mode 100644 index 00000000..f9026387 --- /dev/null +++ b/modules/kobalt-plugin-api/pom.xml @@ -0,0 +1,279 @@ + + 4.0.0 + + com.beust + kobalt-pom + 1.1.0 + ../.. + + + kobalt-plugin-api + jar + 1.1.0 + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.apache.maven + maven-aether-provider + 3.3.9 + + + org.eclipse.aether + impl + + + org.eclipse.aether + spi + + + org.eclipse.aether + util + + + org.eclipse.aether + api + + + + + org.apache.maven.resolver + maven-resolver-api + ${mavenresolver.version} + + + org.apache.maven.resolver + maven-resolver-spi + ${mavenresolver.version} + + + org.apache.maven.resolver + maven-resolver-util + ${mavenresolver.version} + + + org.apache.maven.resolver + maven-resolver-impl + ${mavenresolver.version} + + + org.apache.maven.resolver + maven-resolver-connector-basic + ${mavenresolver.version} + + + org.apache.maven.resolver + maven-resolver-transport-http + ${mavenresolver.version} + + + org.apache.maven.resolver + maven-resolver-transport-file + ${mavenresolver.version} + + + io.reactivex + rxjava + 1.3.3 + + + com.squareup.okio + okio + ${okio.version} + + + javax.inject + javax.inject + 1 + compile + + + com.google.inject + guice + 4.2.2 + + + com.google.inject.extensions + guice-assistedinject + 4.2.2 + + + com.beust + jcommander + 1.72 + + + org.apache.maven + maven-model + 3.5.2 + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + com.google.code.gson + gson + 2.8.2 + + + com.squareup.retrofit2 + retrofit + 2.3.0 + + + com.squareup.retrofit2 + converter-gson + 2.3.0 + + + biz.aQute.bnd + biz.aQute.bndlib + 3.5.0 + + + com.squareup.okhttp3 + logging-interceptor + ${okhttp3.version} + + + com.sparkjava + spark-core + 2.6.0 + + + org.codehaus.groovy + groovy + 2.4.12 + + + org.apache.commons + commons-compress + 1.15 + + + commons-io + commons-io + 2.6 + + + org.junit.platform + junit-platform-surefire-provider + ${junit.version} + + + org.junit.platform + junit-platform-runner + ${junit.version} + + + org.junit.platform + junit-platform-engine + ${junit.version} + + + org.junit.platform + junit-platform-console + ${junit.version} + + + org.junit.jupiter + junit-jupiter-engine + ${junitJupiter.version} + + + org.junit.vintage + junit-vintage-engine + ${junitJupiter.version} + + + org.testng.testng-remote + testng-remote + 1.3.2 + + + org.testng + testng + ${testng.version} + + + org.eclipse.jgit + org.eclipse.jgit + 4.9.0.201710071750-r + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + + javax.xml.bind + jaxb-api + 2.3.0 + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + + ${project.basedir}/src/main/kotlin + + + + + test-compile + test-compile + + + ${project.basedir}/src/test/kotlin + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + compile + + + java-test-compile + test-compile + testCompile + + + + + + \ No newline at end of file diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/ArchiveGenerator.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/ArchiveGenerator.kt new file mode 100644 index 00000000..8158c642 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/ArchiveGenerator.kt @@ -0,0 +1,21 @@ +package com.beust.kobalt + +import com.beust.kobalt.api.KobaltContext +import com.beust.kobalt.api.Project +import com.beust.kobalt.archive.Zip +import com.beust.kobalt.misc.KFiles +import java.io.File + +interface ArchiveGenerator { + fun findIncludedFiles(project: Project, context: KobaltContext, zip: Zip) : List + val suffix: String + fun generateArchive(project: Project, context: KobaltContext, zip: Zip, files: List) : File + + fun fullArchiveName(project: Project, context: KobaltContext, archiveName: String?) : File { + val fullArchiveName = context.variant.archiveName(project, archiveName, suffix) + val archiveDir = File(KFiles.libsDir(project)) + val result = File(archiveDir.path, fullArchiveName) + return result + } + +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Args.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Args.kt new file mode 100644 index 00000000..372f1ba1 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Args.kt @@ -0,0 +1,111 @@ +package com.beust.kobalt + +import com.beust.jcommander.Parameter + +class Args { + @Parameter + var targets: List = arrayListOf() + + @Parameter(names = arrayOf("-bf", "--buildFile"), description = "The build file") + var buildFile: String? = "kobalt/src/Build.kt" + + @Parameter(names = arrayOf("--checkVersions"), description = "Check if there are any newer versions of the " + + "dependencies") + var checkVersions = false + + @Parameter(names = arrayOf("--client")) + var client: Boolean = false + + @Parameter(names = arrayOf("--dev"), description = "Turn on dev mode, resulting in a more verbose log output") + var dev: Boolean = false + + @Parameter(names = arrayOf("--download"), description = "Force a download from the downloadUrl in the wrapper") + var download: Boolean = false + + @Parameter(names = arrayOf("--downloadSources"), + description = "Force a download of sources and javadocs when resolving dependencies") + var downloadSources: Boolean = false + + @Parameter(names = arrayOf("--dryRun"), description = "Display all the tasks that will get run without " + + "actually running them") + var dryRun: Boolean = false + + @Parameter(names = arrayOf("--force"), description = "Force a new server to be launched even if another one" + + " is already running") + var force: Boolean = false + + @Parameter(names = arrayOf("--gc"), description = "Delete old files") + var gc: Boolean = false + + @Parameter(names = arrayOf("--help", "--usage"), description = "Display the help") + var usage: Boolean = false + + @Parameter(names = arrayOf("-i", "--init"), description = "Invoke the templates named, separated by a comma") + var templates: String? = null + + @Parameter(names = arrayOf("--listTemplates"), description = "List the available templates") + var listTemplates: Boolean = false + + @Parameter(names = arrayOf("--log"), description = "Define the log level " + + "(${Constants.LOG_QUIET_LEVEL}-${Constants.LOG_MAX_LEVEL})") + var log: Int = Constants.LOG_DEFAULT_LEVEL + + @Parameter(names = arrayOf("--logTags"), + description = "Comma-separated list of tags to enable logging for") + var logTags: String = "" + + @Parameter(names = arrayOf("--forceIncremental"), + description = "Force the build to be incremental even if the build file was modified") + var forceIncremental: Boolean = false + + @Parameter(names = arrayOf("--noIncremental"), description = "Turn off incremental builds") + var noIncremental: Boolean = false + + @Parameter(names = arrayOf("--offline"), description = "Don't try to download dependencies even if there is no cached version") + var offline: Boolean = false + + @Parameter(names = arrayOf("--plugins"), description = "Comma-separated list of plug-in Maven id's") + var pluginIds: String? = null + + @Parameter(names = arrayOf("--pluginJarFiles"), description = "Comma-separated list of plug-in jar files") + var pluginJarFiles: String? = null + + @Parameter(names = arrayOf("--port"), description = "Port, if --server was specified") + var port: Int? = null + + @Parameter(names = arrayOf("--profiles"), description = "Comma-separated list of profiles to run") + var profiles: String? = null + + @Parameter(names = arrayOf("--profiling"), description = "Display task timings at the end of the build") + var profiling: Boolean = false + + @Parameter(names = arrayOf("--resolve"), + description = "Resolve the given dependency and display its tree") + var dependency: String? = null + + @Parameter(names = arrayOf("--projectInfo"), description = "Display information about the current projects") + var projectInfo: Boolean = false + + @Parameter(names = arrayOf("--noIncrementalKotlin"), description = "Disable incremental Kotlin compilation") + var noIncrementalKotlin: Boolean = false + + companion object { + const val SEQUENTIAL = "--sequential" + } + + @Parameter(names = arrayOf(Args.SEQUENTIAL), description = "Build all the projects in sequence") + var sequential: Boolean = false + + @Parameter(names = arrayOf("--server"), description = "Run in server mode") + var serverMode: Boolean = false + + @Parameter(names = arrayOf("--tasks"), description = "Display the tasks available for this build") + var tasks: Boolean = false + + @Parameter(names = arrayOf("--update"), description = "Update to the latest version of Kobalt") + var update: Boolean = false + + @Parameter(names = arrayOf("--version"), description = "Display the current version of Kobalt") + var version: Boolean = false +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/AsciiArt.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/AsciiArt.kt new file mode 100644 index 00000000..e138fabc --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/AsciiArt.kt @@ -0,0 +1,164 @@ +package com.beust.kobalt + +import java.util.* + +/** + * Make Kobalt's output awesome and unique. + * + * I spend so much time staring at build outputs I decided I might as well make them pretty. + * Note that I also experimented with colors but it's hard to come up with a color scheme that + * will work with all the various backgrounds developers use, so I decided to be conservative + * and stick to simple red/yellow for errors and warnings. + * + * @author Cedric Beust + * @since 10/1/2015 + */ +class AsciiArt { + companion object { + private val BANNERS = arrayOf( + " __ __ __ __ __ \n" + + " / //_/ ____ / /_ ____ _ / / / /_\n" + + " / ,< / __ \\ / __ \\ / __ `/ / / / __/\n" + + " / /| | / /_/ / / /_/ // /_/ / / / / /_ \n" + + " /_/ |_| \\____/ /_.___/ \\__,_/ /_/ \\__/ ", + + " _ __ _ _ _ \n" + + " | |/ / ___ | |__ __ _ | | | |_ \n" + + " | ' / / _ \\ | '_ \\ / _` | | | | __|\n" + + " | . \\ | (_) | | |_) | | (_| | | | | |_ \n" + + " |_|\\_\\ \\___/ |_.__/ \\__,_| |_| \\__| " + ) + + val banner : String get() = BANNERS[Random().nextInt(BANNERS.size)] + +// fun box(s: String) : List = box(listOf(s)) + + val horizontalSingleLine = "\u2500\u2500\u2500\u2500\u2500" + val horizontalDoubleLine = "\u2550\u2550\u2550\u2550\u2550" + val verticalBar = "\u2551" + +// fun horizontalLine(n: Int) = StringBuffer().apply { +// repeat(n, { append("\u2500") }) +// }.toString() + + // Repeat + fun r(n: Int, w: String) : String { + with(StringBuffer()) { + repeat(n, { append(w) }) + return toString() + } + } + + val h = "\u2550" + val ul = "\u2554" + val ur = "\u2557" + val bottomLeft = "\u255a" + val bottomRight = "\u255d" + + // Bottom left with continuation + val bottomLeft2 = "\u2560" + // Bottom right with continuation + val bottomRight2 = "\u2563" + + fun upperBox(max: Int) = ul + r(max + 2, h) + ur + fun lowerBox(max: Int, bl: String = bottomLeft, br : String = bottomRight) = bl + r(max + 2, h) + br + + private fun box(strings: List, bl: String = bottomLeft, br: String = bottomRight) : List { + val v = verticalBar + + val maxString: String = strings.maxBy { it.length } ?: "" + val max = maxString.length + val result = arrayListOf(upperBox(max)) + result.addAll(strings.map { "$v ${center(it, max - 2)} $v" }) + result.add(lowerBox(max, bl, br)) + return result + } + + fun logBox(strings: List, bl: String = bottomLeft, br: String = bottomRight, indent: Int = 0): String { + return buildString { + val boxLines = box(strings, bl, br) + boxLines.withIndex().forEach { iv -> + append(fill(indent)).append(iv.value) + if (iv.index < boxLines.size - 1) append("\n") + } + } + } + + fun logBox(s: String, bl: String = bottomLeft, br: String = bottomRight, indent: Int = 0) + = logBox(listOf(s), bl, br, indent) + + fun fill(n: Int) = buildString { repeat(n, { append(" ")})}.toString() + + fun center(s: String, width: Int) : String { + val diff = width - s.length + val spaces = diff / 2 + 1 + return fill(spaces) + s + fill(spaces + if (diff % 2 == 1) 1 else 0) + } + + const val RESET = "\u001B[0m" + const val BLACK = "\u001B[30m" + const val RED = "\u001B[31m" + const val GREEN = "\u001B[32m" + const val YELLOW = "\u001B[33m"; + const val BLUE = "\u001B[34m" + const val PURPLE = "\u001B[35m" + const val CYAN = "\u001B[36m" + const val WHITE = "\u001B[37m" + + fun wrap(s: CharSequence, color: String) = color + s + RESET + private fun blue(s: CharSequence) = wrap(s, BLUE) + private fun red(s: CharSequence) = wrap(s, RED) + private fun yellow(s: CharSequence) = wrap(s, YELLOW) + + fun taskColor(s: CharSequence) = s + fun errorColor(s: CharSequence) = red(s) + fun warnColor(s: CharSequence) = red(s) + } +} + +class AsciiTable { + class Builder { + private val headers = arrayListOf() + fun header(name: String) = headers.add(name) + fun headers(vararg names: String) = headers.addAll(names) + + private val widths = arrayListOf() + fun columnWidth(w: Int) : Builder { + widths.add(w) + return this + } + + private val rows = arrayListOf>() + fun addRow(row: List) = rows.add(row) + + private fun col(width: Int, s: String) : String { + val format = " %1\$-${width.toString()}s" + val result = String.format(format, s) + return result + } + + val vb = AsciiArt.verticalBar + + fun build() : String { + val formattedHeaders = + headers.mapIndexed { index, s -> + val s2 = col(widths[index], s) + s2 + }.joinToString(vb) + val result = StringBuffer().apply { + append(AsciiArt.logBox(formattedHeaders, AsciiArt.bottomLeft2, AsciiArt.bottomRight2)) + append("\n") + } + var lineLength = 0 + rows.forEachIndexed { _, row -> + val formattedRow = row.mapIndexed { i, s -> col(widths[i], s) }.joinToString(vb) + val line = "$vb $formattedRow $vb" + result.append(line).append("\n") + lineLength = line.length + } + result.append(AsciiArt.lowerBox(lineLength - 4)) + return result.toString() + } + + } +} diff --git a/src/main/kotlin/com/beust/kobalt/BasePluginTask.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/BasePluginTask.kt similarity index 64% rename from src/main/kotlin/com/beust/kobalt/BasePluginTask.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/BasePluginTask.kt index e54d73f5..e55f1b2a 100644 --- a/src/main/kotlin/com/beust/kobalt/BasePluginTask.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/BasePluginTask.kt @@ -3,11 +3,10 @@ package com.beust.kobalt import com.beust.kobalt.api.IPlugin import com.beust.kobalt.api.PluginTask import com.beust.kobalt.api.Project -import com.beust.kobalt.TaskResult -import com.beust.kobalt.internal.TaskResult2 -public abstract class BasePluginTask(override val plugin: IPlugin, +abstract class BasePluginTask(override val plugin: IPlugin, override val name: String, override val doc: String, + override val group: String, override val project: Project) : PluginTask() diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/BuildScript.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/BuildScript.kt new file mode 100644 index 00000000..4c35b9ed --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/BuildScript.kt @@ -0,0 +1,145 @@ +package com.beust.kobalt + +import com.beust.kobalt.api.IClasspathDependency +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.api.annotation.Directive +import com.beust.kobalt.internal.KobaltSettings +import com.beust.kobalt.internal.PluginInfo +import com.beust.kobalt.maven.DependencyManager +import com.beust.kobalt.maven.dependency.FileDependency +import com.beust.kobalt.misc.KobaltLogger +import org.eclipse.aether.repository.Proxy +import java.io.File +import java.net.InetSocketAddress + +var BUILD_SCRIPT_CONFIG : BuildScriptConfig? = null + +class BuildScriptConfig { + /** The list of repos used to locate plug-ins. */ + @Directive + fun repos(vararg r: String) = newRepos(*r) + + /** The list of plug-ins to use for this build file. */ + @Directive + fun plugins(vararg pl: String) = newPlugins(*pl) + + /** The build file classpath. */ + @Directive + fun buildFileClasspath(vararg bfc: String) = newBuildFileClasspath(*bfc) + + /** Options passed to Kobalt */ + @Directive + fun kobaltOptions(vararg options: String) = Kobalt.addKobaltOptions(options) + + /** Where to find additional build files */ + @Directive + fun buildSourceDirs(vararg dirs: String) = Kobalt.addBuildSourceDirs(dirs) + + // The following settings modify the compiler used to compile the build file, which regular users should + // probably never need to do. Projects should use kotlinCompiler { compilerVersion } to configure the + // Kotin compiler for their source files. + var kobaltCompilerVersion : String? = null + var kobaltCompilerRepo: String? = null + var kobaltCompilerFlags: String? = null +} + +@Directive +fun homeDir(vararg dirs: String) : String = SystemProperties.homeDir + + File.separator + dirs.toMutableList().joinToString(File.separator) + +@Directive +fun file(file: String) : String = FileDependency.PREFIX_FILE + file + +fun plugins(vararg dependency : IClasspathDependency) { + dependency.forEach { Plugins.addDynamicPlugin(it) } +} + +fun plugins(vararg dependencies : String) { + KobaltLogger.logger.warn("Build.kt", + "Invoking plugins() directly is deprecated, use the buildScript{} directive") + newPlugins(*dependencies) +} + +@Directive +fun newPlugins(vararg dependencies : String) { + val factory = Kobalt.INJECTOR.getInstance(DependencyManager::class.java) + dependencies.forEach { + Plugins.addDynamicPlugin(factory.create(it)) + } +} + +data class ProxyConfig(val host: String = "", val port: Int = 0, val type: String = "", val nonProxyHosts: String = "") { + fun toProxy() = java.net.Proxy(java.net.Proxy.Type.HTTP, InetSocketAddress(host, port)) + + fun toAetherProxy() = Proxy(type, host, port) // TODO make support for proxy auth +} + +data class HostConfig(var url: String = "", var name: String = HostConfig.createRepoName(url), + var username: String? = null, var password: String? = null) { + + companion object { + /** + * For repos specified in the build file (repos()) that don't have an associated unique name, + * create such a name from the URL. This is a requirement from Maven Resolver, and failing to do + * this leads to very weird resolution errors. + */ + private fun createRepoName(url: String) = url.replace("/", "_").replace("\\", "_").replace(":", "_") + } + + fun hasAuth() : Boolean { + return (! username.isNullOrBlank()) && (! password.isNullOrBlank()) + } + + override fun toString() : String { + return url + if (username != null) { + "username: $username, password: ***" + } else { + "" + } + } +} + +fun repos(vararg repos : String) { + KobaltLogger.logger.warn("Build.kt", + "Invoking repos() directly is deprecated, use the buildScript{} directive") + newRepos(*repos) +} + +fun newRepos(vararg repos: String) { + repos.forEach { Kobalt.addRepo(HostConfig(it)) } +} + +fun buildFileClasspath(vararg deps: String) { + KobaltLogger.logger.warn("Build.kt", + "Invoking buildFileClasspath() directly is deprecated, use the buildScript{} directive") + newBuildFileClasspath(*deps) +} + +fun newBuildFileClasspath(vararg deps: String) { + //FIXME newBuildFileClasspath called twice + deps.forEach { Kobalt.addBuildFileClasspath(it) } +} + +@Directive +fun authRepos(vararg repos : HostConfig) { + repos.forEach { Kobalt.addRepo(it) } +} + +@Directive +fun authRepo(init: HostConfig.() -> Unit) = HostConfig(name = "").apply { init() } + +@Directive +fun glob(g: String) : IFileSpec.GlobSpec = IFileSpec.GlobSpec(g) + +/** + * The location of the local Maven repository. + */ +@Directive +fun localMaven() : String { + val pluginInfo = Kobalt.INJECTOR.getInstance(PluginInfo::class.java) + val initial = Kobalt.INJECTOR.getInstance(KobaltSettings::class.java).localMavenRepo + val result = pluginInfo.localMavenRepoPathInterceptors.fold(initial) { current, interceptor -> + File(interceptor.repoPath(current.absolutePath)) + } + return result.toURI().toString() +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Constants.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Constants.kt new file mode 100644 index 00000000..8eb73c84 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Constants.kt @@ -0,0 +1,25 @@ +package com.beust.kobalt + +import com.beust.kobalt.misc.KFiles + +object Constants { + const val LOG_QUIET_LEVEL = 0 + const val LOG_DEFAULT_LEVEL = 1 + const val LOG_MAX_LEVEL = 3 + val BUILD_FILE_NAME = "Build.kt" + val BUILD_FILE_DIRECTORY = "kobalt/src" + val BUILD_FILE_PATH = KFiles.joinDir(BUILD_FILE_DIRECTORY, BUILD_FILE_NAME) + val KOTLIN_COMPILER_VERSION = "1.2.70" + + internal val DEFAULT_REPOS = listOf( + // "https://maven-central.storage.googleapis.com/", + HostConfig("https://repo1.maven.org/maven2/", "Maven"), + HostConfig("https://jcenter.bintray.com/", "JCenter") +// "https://repository.jetbrains.com/all/", // <-- contains snapshots + + // snapshots +// "https://oss.sonatype.org/content/repositories/snapshots/" +// , "https://repository.jboss.org/nexus/content/repositories/root_repository/" + ) + +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Directives.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Directives.kt new file mode 100644 index 00000000..93d9434c --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Directives.kt @@ -0,0 +1,39 @@ +package com.beust.kobalt + +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.api.Project +import com.beust.kobalt.api.annotation.Directive +import com.beust.kobalt.internal.JvmCompilerPlugin +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +@Directive +fun project(vararg projects: Project, init: Project.() -> Unit): Project { + return Project("").apply { + init() + (Kobalt.findPlugin(JvmCompilerPlugin.PLUGIN_NAME) as JvmCompilerPlugin) + .addDependentProjects(this, projects.toList()) + } +} + +@Directive +fun buildScript(init: BuildScriptConfig.() -> Unit): BuildScriptConfig { + val buildScriptConfig = BuildScriptConfig().apply { init() } + BUILD_SCRIPT_CONFIG = buildScriptConfig + return buildScriptConfig +} + +@Directive +fun profile(): ReadWriteProperty { + val result = object: ReadWriteProperty { + var value: Boolean = false + override operator fun getValue(thisRef: Nothing?, property: KProperty<*>): Boolean { + return value + } + + override operator fun setValue(thisRef: Nothing?, property: KProperty<*>, value: Boolean) { + this.value = value + } + } + return result +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Features.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Features.kt new file mode 100644 index 00000000..83e01827 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Features.kt @@ -0,0 +1,8 @@ +package com.beust.kobalt + +class Features { + companion object { + /** If true, uses timestamps to speed up the tasks */ + const val USE_TIMESTAMPS = true + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/FileSpec.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/FileSpec.kt new file mode 100644 index 00000000..1eb409f4 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/FileSpec.kt @@ -0,0 +1,102 @@ +package com.beust.kobalt + +import com.beust.kobalt.misc.kobaltLog +import java.io.File +import java.nio.file.* +import java.nio.file.attribute.BasicFileAttributes + +/** + * Subclasses of IFileSpec can be turned into a list of files. There are two kings: FileSpec (a single file) + * and GlobSpec (a spec defined by a glob, e.g. ** slash *Test.class) + */ +sealed class IFileSpec { + abstract fun toFiles(baseDir: String?, filePath: String, excludes: List = emptyList()): List + + class FileSpec(val spec: String) : IFileSpec() { + override fun toFiles(baseDir: String?, filePath: String, excludes: List) = listOf(File(spec)) + + override fun toString() = spec + } + + /** + * A glob matcher, see http://docs.oracle.com/javase/7/docs/api/java/nio/file/FileSystem.html#getPathMatcher%28java.lang.String%29 + */ + class GlobSpec(val spec: List) : IFileSpec() { + + constructor(spec: String) : this(arrayListOf(spec)) + + private fun isIncluded(includeMatchers: Glob, excludes: List, rel: Path) : Boolean { + excludes.forEach { + if (it.matches(rel)) { + kobaltLog(3, " Excluding ${rel.toFile()}") + return false + } + } + if (includeMatchers.matches(rel)) { + kobaltLog(3, " Including ${rel.toFile().path}") + return true + } + kobaltLog(2, " Excluding ${rel.toFile()} (not matching any include pattern") + return false + } + + override fun toFiles(baseDir: String?, filePath: String, excludes: List): List { + val result = arrayListOf() + val includes = Glob(*spec.toTypedArray()) + + if (File(baseDir, filePath).isDirectory) { + val orgRootDir = (if (File(filePath).isAbsolute) Paths.get(filePath) + else if (baseDir != null) Paths.get(baseDir, filePath) + else Paths.get(filePath)).run { normalize() } + // Paths.get(".").normalize() returns an empty string, which is not a valid file :-( + val rootDir = if (orgRootDir.toFile().path.isEmpty()) Paths.get("./") else orgRootDir + if (rootDir.toFile().exists()) { + Files.walkFileTree(rootDir, object : SimpleFileVisitor() { + override fun visitFile(p: Path, attrs: BasicFileAttributes): FileVisitResult { + val path = p.normalize() + val rel = orgRootDir.relativize(path) + if (isIncluded(includes, excludes, path)) { + kobaltLog(3, " including file " + rel.toFile() + " from rootDir $rootDir") + result.add(rel.toFile()) + } + return FileVisitResult.CONTINUE + } + }) + } else { + throw AssertionError("Directory \"$rootDir\" should exist") + } + } else { + if (isIncluded(includes, excludes, Paths.get(filePath))) { + result.add(File(filePath)) + } + } + + return result + } + + override fun toString(): String { + var result = "" + spec.apply { + if (!isEmpty()) { + result += "Included files: " + joinToString { ", " } + } + } + return result + } + } + +} + +/** + * A Glob is a simple file name matcher. + */ +class Glob(vararg specs: String) { + val matchers = prepareMatchers(specs.toList()) + + private fun prepareMatchers(specs: List): List = + specs.map { it -> FileSystems.getDefault().getPathMatcher("glob:$it") } + + fun matches(s: String) = matches(Paths.get(s)) + + fun matches(path: Path) = matchers.any { it.matches(path) } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/IncludeFromTo.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/IncludeFromTo.kt new file mode 100644 index 00000000..ea189851 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/IncludeFromTo.kt @@ -0,0 +1,43 @@ +package com.beust.kobalt + +import com.beust.kobalt.api.annotation.Directive +import java.io.File + +/** + * Base classes for directives that support install(from,to) (e.g. install{} or jar{}). + */ +open class IncludeFromTo { + /** + * Prefix path to be removed from the zip file. For example, if you add "build/lib/a.jar" to the zip + * file and the excludePrefix is "build/lib", then "a.jar" will be added at the root of the zip file. + */ + val includedFiles = arrayListOf() + + @Directive + fun from(s: String) = From(s) + + @Directive + fun to(s: String) = To(s) + + @Directive + fun copy(from: From, to: To) { + val dir = File(from.path).absoluteFile.parentFile + includedFiles.add(IncludedFile(from(dir.absolutePath), to, listOf(IFileSpec.FileSpec(from.path)))) + } + + @Directive + fun include(vararg files: String) { + includedFiles.add(IncludedFile(files.map { IFileSpec.FileSpec(it) })) + } + + @Directive + fun include(from: From, to: To, vararg specs: String) { + includedFiles.add(IncludedFile(from, to, specs.map { IFileSpec.FileSpec(it) })) + } + + @Directive + fun include(from: From, to: To, vararg specs: IFileSpec.GlobSpec) { + includedFiles.add(IncludedFile(from, to, listOf(*specs))) + } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/IncludedFile.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/IncludedFile.kt new file mode 100644 index 00000000..46dea15e --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/IncludedFile.kt @@ -0,0 +1,44 @@ +package com.beust.kobalt + +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.toString +import java.io.File +import java.nio.file.Paths + +class IncludedFile(val fromOriginal: From, val toOriginal: To, val specs: List, + val expandJarFiles: Boolean = false) { + constructor(specs: List, expandJarFiles: Boolean = false) : this(From(""), To(""), specs, expandJarFiles) + fun from(s: String) = File(if (fromOriginal.isCurrentDir()) s else KFiles.joinDir(from, s)) + val from: String get() = fromOriginal.path.replace("\\", "/") + fun to(s: String) = File(if (toOriginal.isCurrentDir()) s else KFiles.joinDir(to, s)) + val to: String get() = toOriginal.path.replace("\\", "/") + override fun toString() = toString("IncludedFile", + "files - ", specs.map { it.toString() }, + "from", from, + "to", to) + + fun allFromFiles(directory: String? = null): List { + val result = arrayListOf() + specs.forEach { spec -> +// val fullDir = if (directory == null) from else KFiles.joinDir(directory, from) + spec.toFiles(directory, from).forEach { source -> + result.add(if (source.isAbsolute) source else File(source.path)) + } + } + return result.map { Paths.get(it.path).normalize().toFile()} + } +} + +open class Direction(open val p: String) { + override fun toString() = path + fun isCurrentDir() = path == "./" + + val path: String get() = + if (p.isEmpty()) "./" + else if (p.startsWith("/") || p.endsWith("/")) p + else p + "/" +} + +class From(override val p: String) : Direction(p) + +class To(override val p: String) : Direction(p) diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/IncrementalTaskInfo.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/IncrementalTaskInfo.kt new file mode 100644 index 00000000..b1c56e5a --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/IncrementalTaskInfo.kt @@ -0,0 +1,17 @@ +package com.beust.kobalt + +import com.beust.kobalt.api.KobaltContext +import com.beust.kobalt.api.Project + +/** + * @param inputChecksum The checksum for the input to this task. It gets compared against the previous checksum + * calculated by Kobalt. If they differ, the task gets run. If they are equal, outputChecksums are then compared. + * @param outputChecksum The checksum for the output of this task. If null, the output is absent + * and the task will be run. If non null, it gets compared against the checksum of the previous run and + * if they differ, the task gets run. + * @param task The task to run. + */ +class IncrementalTaskInfo(val inputChecksum: () -> String?, + val outputChecksum: () -> String?, + val task: (Project) -> TaskResult, + val context: KobaltContext) diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/JarGenerator.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/JarGenerator.kt new file mode 100644 index 00000000..19bb52c6 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/JarGenerator.kt @@ -0,0 +1,162 @@ +package com.beust.kobalt + +import com.beust.kobalt.api.KobaltContext +import com.beust.kobalt.api.Project +import com.beust.kobalt.archive.Archives +import com.beust.kobalt.archive.MetaArchive +import com.beust.kobalt.archive.Zip +import com.beust.kobalt.maven.DependencyManager +import com.beust.kobalt.maven.aether.Scope +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.kobaltLog +import com.google.inject.Inject +import java.io.File +import java.io.FileInputStream +import java.nio.file.Paths +import java.util.jar.Manifest + +class JarGenerator @Inject constructor(val dependencyManager: DependencyManager) : ArchiveGenerator { + companion object { + fun findIncludedFiles(directory: String, files: List, excludes: List, + throwOnError: Boolean = true) + : List { + val result = arrayListOf() + files.forEach { includedFile -> + val includedSpecs = arrayListOf() + includedFile.specs.forEach { spec -> + val fromPath = includedFile.from + if (File(directory, fromPath).exists()) { + spec.toFiles(directory, fromPath).forEach { file -> + val fullFile = File(KFiles.joinDir(directory, fromPath, file.path)) + if (! fullFile.exists() && throwOnError) { + throw AssertionError("File should exist: $fullFile") + } + + if (!KFiles.isExcluded(fullFile, excludes)) { + val normalized = Paths.get(file.path).normalize().toFile().path + includedSpecs.add(IFileSpec.FileSpec(normalized)) + } else { + kobaltLog(2, "Not adding ${file.path} to jar file because it's excluded") + } + + } + } else { + kobaltLog(2, " Directory $fromPath doesn't exist, not including it in the jar") + } + } + if (includedSpecs.size > 0) { + kobaltLog(3, "Including specs $includedSpecs") + result.add(IncludedFile(From(includedFile.from), To(includedFile.to), includedSpecs)) + } + } + return result + } + } + + override val suffix = ".jar" + + override fun findIncludedFiles(project: Project, context: KobaltContext, zip: Zip) : List { + // + // Add all the applicable files for the current project + // + val buildDir = KFiles.buildDir(project) + val result = arrayListOf() + val classesDir = KFiles.makeDir(buildDir.path, "classes") + + if (zip.includedFiles.isEmpty()) { + // If no includes were specified, assume the user wants a simple jar file made of the + // classes of the project, so we specify a From("build/classes/"), To("") and + // a list of files containing everything under it + val relClassesDir = Paths.get(project.directory).relativize(Paths.get(classesDir.path)) + val prefixPath = Paths.get(project.directory).relativize(Paths.get(classesDir.path + "/")) + + // Class files + val files = KFiles.findRecursively(classesDir).map { File(relClassesDir.toFile(), it) } + val filesNotExcluded : List = files.filter { + ! KFiles.Companion.isExcluded(KFiles.joinDir(project.directory, it.path), zip.excludes) + } + val fileSpecs = arrayListOf() + filesNotExcluded.forEach { + fileSpecs.add(IFileSpec.FileSpec(it.path.toString().substring(prefixPath.toString().length + 1))) + } + result.add(IncludedFile(From(prefixPath.toString()), To(""), fileSpecs)) + + // Resources, if applicable + context.variant.resourceDirectories(project).forEach { + result.add(IncludedFile(From(it.path), To(""), listOf(IFileSpec.GlobSpec("**")))) + } + } else { + // + // The user specified an include, just use it verbatim + // + val includedFiles = findIncludedFiles(project.directory, zip.includedFiles, zip.excludes, false) + result.addAll(includedFiles) + } + + // + // If fatJar is true, add all the transitive dependencies as well: compile, runtime and dependent projects + // + if (zip.fatJar) { + val seen = hashSetOf() + @Suppress("UNCHECKED_CAST") + val allDependencies = project.compileDependencies + project.compileRuntimeDependencies + + context.variant.buildType.compileDependencies + + context.variant.buildType.compileRuntimeDependencies + + context.variant.productFlavor.compileDependencies + + context.variant.productFlavor.compileRuntimeDependencies + val transitiveDependencies = dependencyManager.calculateDependencies(project, context, + scopes = listOf(Scope.COMPILE), passedDependencies = allDependencies) + transitiveDependencies.map { + it.jarFile.get() + }.forEach { file : File -> + if (! seen.contains(file.path)) { + seen.add(file.path) + if (! KFiles.Companion.isExcluded(file, zip.excludes)) { + result.add(IncludedFile(specs = arrayListOf(IFileSpec.FileSpec(file.absolutePath)), + expandJarFiles = true)) + } + } + } + } + + return result + } + + override fun generateArchive(project: Project, context: KobaltContext, zip: Zip, + includedFiles: List) : File { + // + // Generate the manifest + // If manifest attributes were specified in the build file, use those to generateAndSave the manifest. Otherwise, + // try to find a META-INF/MANIFEST.MF and use that one if we find any. Otherwise, use the default manifest. + // + val manifest = + if (zip.attributes.size > 1) { + context.logger.log(project.name, 2, "Creating MANIFEST.MF from " + zip.attributes.size + " attributes") + Manifest().apply { + zip.attributes.forEach { attribute -> + mainAttributes.putValue(attribute.first, attribute.second) + } + } + } else { + fun findManifestFile(project: Project, includedFiles: List): File? { + val allFiles = includedFiles.flatMap { file -> + file.allFromFiles(project.directory).map { file.from(it.path) } + } + val manifestFiles = allFiles.filter { it.path.contains(MetaArchive.MANIFEST_MF) } + return if (manifestFiles.any()) manifestFiles[0] else null + } + + val manifestFile = findManifestFile(project, includedFiles) + if (manifestFile != null) { + context.logger.log(project.name, 2, "Including MANIFEST.MF file $manifestFile") + Manifest(FileInputStream(manifestFile)) + } else { + null + } + } + + return Archives.generateArchive(project, context, zip.name, ".jar", includedFiles, + true /* expandJarFiles */, manifest) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/beust/kobalt/JavaInfo.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/JavaInfo.kt similarity index 66% rename from src/main/kotlin/com/beust/kobalt/JavaInfo.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/JavaInfo.kt index 53c4e010..3c957454 100644 --- a/src/main/kotlin/com/beust/kobalt/JavaInfo.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/JavaInfo.kt @@ -2,18 +2,18 @@ package com.beust.kobalt import java.io.File -abstract public class JavaInfo { - public var javaExecutable: File? = null +abstract class JavaInfo { + val javaExecutable: File? get() = findExecutable("java") - public var javacExecutable: File? = null + val javacExecutable: File? get() = findExecutable("javac") - public var javadocExecutable: File? = null + val javadocExecutable: File? get() = findExecutable("javadoc") - abstract public var javaHome: File? - abstract public var runtimeJar: File? - abstract public var toolsJar: File? + abstract var javaHome: File? + abstract var runtimeJar: File? + abstract var toolsJar: File? - abstract public fun findExecutable(command: String) : File + abstract fun findExecutable(command: String) : File companion object { fun create(javaBase: File?): Jvm { diff --git a/src/main/kotlin/com/beust/kobalt/Jvm.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Jvm.kt similarity index 88% rename from src/main/kotlin/com/beust/kobalt/Jvm.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Jvm.kt index a1459112..14c55efd 100644 --- a/src/main/kotlin/com/beust/kobalt/Jvm.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Jvm.kt @@ -1,18 +1,18 @@ package com.beust.kobalt -import com.beust.kobalt.misc.log +import com.beust.kobalt.misc.kobaltLog import com.beust.kobalt.misc.warn import java.io.File import java.io.IOException -public open class Jvm constructor( +open class Jvm constructor( val os: OperatingSystem, var javaBase: File? = null) : JavaInfo() { private var _javaHome: File? = null - override public var javaHome: File? = null + override var javaHome: File? = null get() = _javaHome!! - override public var runtimeJar: File? = null + override var runtimeJar: File? = null private fun findRuntimeJar() : File? { var runtimeJar = File(javaBase, "lib/rt.jar") if (runtimeJar.exists()) { @@ -21,7 +21,7 @@ public open class Jvm constructor( runtimeJar = File(javaBase, "jre/lib/rt.jar") return if (runtimeJar.exists()) runtimeJar else null } - override public var toolsJar: File? = null + override var toolsJar: File? = null private var userSupplied: Boolean? = false private var javaVersion: String? = null @@ -67,7 +67,7 @@ public open class Jvm constructor( return toolsJar } if (javaHome!!.name.equals("jre", true)) { - javaHome = javaHome!!.parentFile + _javaHome = javaHome!!.parentFile toolsJar = File(javaHome, "lib/tools.jar") if (toolsJar.exists()) { return toolsJar @@ -78,7 +78,7 @@ public open class Jvm constructor( val version = SystemProperties.Companion.javaVersion if (javaHome!!.name.toRegex().matches("jre\\d+") || javaHome!!.name == "jre$version") { - javaHome = File(javaHome!!.parentFile, "jdk$version") + _javaHome = File(javaHome!!.parentFile, "jdk$version") toolsJar = File(javaHome, "lib/tools.jar") if (toolsJar.exists()) { return toolsJar @@ -89,15 +89,15 @@ public open class Jvm constructor( return null } -// open public fun isIbmJvm(): Boolean { +// open fun isIbmJvm(): Boolean { // return false // } - override public fun findExecutable(command: String): File { + override fun findExecutable(command: String): File { if (javaHome != null) { val jdkHome = if (javaHome!!.endsWith("jre")) javaHome!!.parentFile else javaHome val exec = File(jdkHome, "bin/" + command) - var executable = File(os.getExecutableName(exec.absolutePath)) + val executable = File(os.getExecutableName(exec.absolutePath)) if (executable.isFile) { return executable } @@ -110,8 +110,8 @@ public open class Jvm constructor( val pathExecutable = os.findInPath(command) if (pathExecutable != null) { - log(1, "Unable to find the $command executable using home: " + - "$javaHome. We found it on the PATH: $pathExecutable.") + kobaltLog(2, "Unable to find the $command executable using home: " + + "$javaHome but found it on the PATH: $pathExecutable.") return pathExecutable } diff --git a/src/main/kotlin/com/beust/kobalt/KobaltException.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/KobaltException.kt similarity index 100% rename from src/main/kotlin/com/beust/kobalt/KobaltException.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/KobaltException.kt diff --git a/src/main/kotlin/com/beust/kobalt/OperatingSystem.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/OperatingSystem.kt similarity index 100% rename from src/main/kotlin/com/beust/kobalt/OperatingSystem.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/OperatingSystem.kt diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Plugins.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Plugins.kt new file mode 100644 index 00000000..0102dd8b --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Plugins.kt @@ -0,0 +1,212 @@ +package com.beust.kobalt + +import com.beust.kobalt.api.* +import com.beust.kobalt.api.annotation.IncrementalTask +import com.beust.kobalt.api.annotation.Task +import com.beust.kobalt.internal.IncrementalManager +import com.beust.kobalt.internal.KobaltSettings +import com.beust.kobalt.internal.PluginInfo +import com.beust.kobalt.internal.TaskManager +import com.beust.kobalt.maven.DependencyManager +import com.beust.kobalt.misc.JarUtils +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.KobaltExecutors +import com.beust.kobalt.misc.kobaltLog +import com.google.inject.Provider +import java.io.File +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.net.URLClassLoader +import java.util.* +import java.util.jar.JarFile +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class Plugins @Inject constructor (val taskManagerProvider : Provider, + val files: KFiles, + val depManager: DependencyManager, + val settings: KobaltSettings, + val executors: KobaltExecutors, + val incrementalManagerFactory: IncrementalManager.IFactory, + val taskManager: TaskManager) { + + companion object { + private var pluginMap = hashMapOf() + + fun addPluginInstance(plugin: IPlugin) { + pluginMap.put(plugin.name, plugin) + } + + val plugins : List + get() = ArrayList(pluginMap.values) + + /** + * The list of plugins found in the build file. + */ + val dynamicPlugins : ArrayList = arrayListOf() + fun addDynamicPlugin(plugin: IClasspathDependency) = dynamicPlugins.add(plugin) + + fun findPlugin(name: String) : IPlugin? = pluginMap[name] + } + + fun shutdownPlugins() = plugins.forEach { it.shutdown() } + + fun applyPlugins(context: KobaltContext, projects: List) { + plugins.forEach { plugin -> + addPluginInstance(plugin) + // We could inject this in the plug-in but since these will be written by external users, + // I want to keep the verbosity of plugins to a minimum, so instead, we do the injection + // manually here + if (plugin is BasePlugin) { + plugin.taskManager = taskManagerProvider.get() + plugin.plugins = this + } + + // Call apply() on each plug-in that accepts a project + kobaltLog(2, "Applying plug-in \"${plugin.name}\"") + projects.filter { plugin.accept(it) }.forEach { project -> + plugin.apply(project, context) + } + + findStaticTasks(plugin, Task::class.java, { method -> isValidTaskMethod(method)}).forEach { + taskManager.addAnnotationTask(plugin, it.first, it.second) + } + findStaticTasks(plugin, IncrementalTask::class.java, + { method -> isValidIncrementalTaskMethod(method)}).forEach { + taskManager.addIncrementalTask(plugin, it.first, it.second) + } + } + + // Collect all the tasks from the task contributors + context.pluginInfo.taskContributors.forEach { + projects.forEach { project -> + taskManager.dynamicTasks.addAll(it.tasksFor(project, context)) + } + } + + // ... and from the incremental task contributors + val incrementalManager = incrementalManagerFactory.create() + context.pluginInfo.incrementalTaskContributors.forEach { + projects.forEach { project -> + it.incrementalTasksFor(project, context).forEach { + // Convert the closure (Project) -> IncrementalTaskInfo to (Project) -> TaskResult + // and create a DynamicTask out of it + val closure = + incrementalManager.toIncrementalTaskClosure(it.name, it.incrementalClosure, Variant()) + val task = DynamicTask(it.plugin, it.name, it.doc, it.group, it.project, it.dependsOn, + it.reverseDependsOn, it.runBefore, it.runAfter, it.alwaysRunAfter, + closure) + taskManager.dynamicTasks.add(task) + } + } + } + + // Now that we have collected all static and dynamic tasks, turn them all into plug-in tasks + taskManager.computePluginTasks(projects) + } + + private fun findStaticTasks(plugin: IPlugin, klass: Class, validate: (Method) -> Boolean) + : List> { + val result = arrayListOf>() + + var currentClass : Class? = plugin.javaClass + + // Tasks can come from two different places: plugin classes and build files. + // When a task is read from a build file, ScriptCompiler adds it right away to plugin.methodTasks. + // The following loop introspects the current plugin, finds all the tasks using the @Task annotation + // and adds them to plugin.methodTasks + while (currentClass != null && ! (klass.equals(currentClass))) { + currentClass.declaredMethods.map { + Pair(it, it.getAnnotation(klass)) + }.filter { + it.second != null + }.filter { + validate(it.first) + }.forEach { + if (Modifier.isPrivate(it.first.modifiers)) { + throw KobaltException("A task method cannot be private: ${it.first}") + } + result.add(it) + } + + currentClass = currentClass.superclass + } + return result + } + + /** + * Make sure this task method has the right signature. + */ + private fun isValidIncrementalTaskMethod(method: Method): Boolean { + val t = "Task ${method.declaringClass.simpleName}.${method.name}: " + + if (method.returnType != IncrementalTaskInfo::class.java) { + throw IllegalArgumentException("${t}should return a IncrementalTaskInfo") + } + return true + } + + /** + * Make sure this task method has the right signature. + */ + private fun isValidTaskMethod(method: Method): Boolean { + val t = "Task ${method.declaringClass.simpleName}.${method.name}: " + + if (method.returnType != TaskResult::class.java) { + throw IllegalArgumentException("${t}should return a TaskResult") + } + if (method.parameterTypes.size != 1) { + throw IllegalArgumentException("${t}should take exactly one parameter of type a Project") + } + with(method.parameterTypes) { + if (! Project::class.java.isAssignableFrom(get(0))) { + throw IllegalArgumentException("${t}first parameter should be of type Project," + + "not ${get(0)}") + } + } + return true + } + + val dependencies = arrayListOf() + +// @Inject +// lateinit var pluginInfo: PluginInfo + + fun installPlugins(dependencies: List, scriptClassLoader: ClassLoader) { + val executor = executors.newExecutor("Plugins", 5) + dependencies.forEach { + // + // Load all the jar files synchronously (can't compile the build script until + // they are installed locally). + depManager.create(it.id) + + // + // Open the jar, parse its kobalt-plugin.xml and add the resulting PluginInfo to pluginInfo + // + val file = it.jarFile.get() + val pluginXml = if (file.isDirectory) { + // The plug-in can point to a directory (e.g. plugin("classes")), in which case we just + // read kobalt-plugin.xml directly + File(file, PluginInfo.PLUGIN_XML).readText() + } else { + // The plug-in is pointing to a jar file, read kobalt-plugin.xml from it + JarUtils.extractTextFile(JarFile(file), PluginInfo.PLUGIN_XML) + } + + val pluginInfo = Kobalt.INJECTOR.getInstance(PluginInfo::class.java) + if (pluginXml != null) { + val pluginClassLoader = URLClassLoader(arrayOf(file.toURI().toURL())) + val thisPluginInfo = PluginInfo.readPluginXml(pluginXml, pluginClassLoader, scriptClassLoader) + pluginInfo.addPluginInfo(thisPluginInfo) + thisPluginInfo.plugins.forEach { + Plugins.addPluginInstance(it) + } + } else { + throw KobaltException("Plugin $it doesn't contain a ${PluginInfo.PLUGIN_XML} file") + } + } + executor.shutdown() + } + +} \ No newline at end of file diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/ResolveDependency.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/ResolveDependency.kt new file mode 100644 index 00000000..0d1223e9 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/ResolveDependency.kt @@ -0,0 +1,117 @@ +package com.beust.kobalt + +import com.beust.kobalt.api.IClasspathDependency +import com.beust.kobalt.maven.LocalRepo +import com.beust.kobalt.maven.MavenId +import com.beust.kobalt.maven.aether.* +import com.beust.kobalt.misc.* +import com.google.inject.Inject +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.graph.DependencyNode +import java.util.* + +/** + * Display information about a Maven id. + */ +class ResolveDependency @Inject constructor( + val localRepo: LocalRepo, + val aether: KobaltMavenResolver, + val executors: KobaltExecutors) { + val increment = 8 + val leftFirst = "\u2558" + val leftMiddle = "\u255f" + val leftLast = "\u2559" + val vertical = "\u2551" + + class Dep(val dep: IClasspathDependency, val level: Int) + + fun run(id: String) = displayDependenciesFor(id) + + private fun latestMavenArtifact(group: String, artifactId: String, extension: String = "jar"): DependencyNode { + val artifact = DefaultArtifact(group, artifactId, extension, "(0,]") + val resolved = aether.resolveRange(artifact) + if (resolved != null) { + val newArtifact = DefaultArtifact(artifact.groupId, artifact.artifactId, artifact.extension, + resolved.highestVersion.toString()) + val artifactResult = aether.resolve(KobaltMavenResolver.artifactToId(newArtifact), null) + return artifactResult.root + } else { + throw KobaltException("Couldn't find latest artifact for $group:$artifactId") + } + } + + class PairResult(val dependency: IClasspathDependency, val repoUrl: String) + + fun latestArtifact(group: String, artifactId: String, extension: String = "jar"): PairResult + = latestMavenArtifact(group, artifactId, extension).let { + PairResult(AetherDependency(it.artifact), "(TBD repo)") + } + + private fun displayDependenciesFor(id: String) { + val mavenId = MavenId.create(id) + val resolved : PairResult = + if (mavenId.hasVersion) { + val node = aether.resolve(id, filter = Filters.EXCLUDE_OPTIONAL_FILTER) + PairResult(AetherDependency(node.root.artifact), node.artifactResults[0].repository.toString()) + } else { + latestArtifact(mavenId.groupId, mavenId.artifactId) + } + + displayDependencies(resolved.dependency, resolved.repoUrl) + } + + private fun displayDependencies(dep: IClasspathDependency, url: String) { + val indent = -1 + val root = Node(Dep(dep, indent)) + val seen = hashSetOf(dep.id) + root.addChildren(findChildren(root, seen)) + + kobaltLog(1, AsciiArt.logBox(listOf(dep.id, url, dep.jarFile.get()).map { " $it" })) + + display(root.children) + kobaltLog(1, "") + } + + private fun display(nodes: List>) { + nodes.withIndex().forEach { indexNode -> + val node = indexNode.value + with(node.value) { + val left = + if (indexNode.index == nodes.size - 1) leftLast + else leftMiddle + val indent = level * increment + for(i in 0..indent - 2) { + if (!KobaltLogger.isQuiet) { + if (i == 0 || ((i + 1) % increment == 0)) print(vertical) + else print(" ") + } + } + kobaltLog(1, left + " " + dep.id + (if (dep.optional) " (optional)" else "")) + display(node.children) + } + } + + } + + private fun findChildren(root: Node, seen: HashSet): List> { + val result = arrayListOf>() + root.value.dep.directDependencies().forEach { + if (! seen.contains(it.id)) { + val dep = Dep(it, root.value.level + 1) + val node = Node(dep) + kobaltLog(2, "Found dependency ${dep.dep.id} level: ${dep.level}") + result.add(node) + seen.add(it.id) + try { + node.addChildren(findChildren(node, seen)) + } catch(ex: Exception) { + if (! it.optional) warn("Couldn't resolve " + node) + // else don't warn about missing optional dependencies + } + } + } + kobaltLog(2, "Children for ${root.value.dep.id}: ${result.size}") + return result + } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/SystemProperties.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/SystemProperties.kt new file mode 100644 index 00000000..d5507497 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/SystemProperties.kt @@ -0,0 +1,22 @@ +package com.beust.kobalt + +class SystemProperties { + companion object { + val javaBase : String + get() { + val jh = System.getenv("JAVA_HOME") + ?: System.getProperty("java.home") + ?: throw IllegalArgumentException("JAVA_HOME not defined") + val result = + if (jh.toLowerCase().endsWith("jre")) jh.substring(0, jh.length - 4) + else jh + return result + } + val javaVersion = System.getProperty("java.version") + val homeDir = System.getProperty("user.home") + val tmpDir = System.getProperty("java.io.tmpdir") + val currentDir = System.getProperty("user.dir") + val username = System.getProperty("user.name") + } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/TaskResult.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/TaskResult.kt new file mode 100644 index 00000000..241bc045 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/TaskResult.kt @@ -0,0 +1,8 @@ +package com.beust.kobalt + +class TestResult(val success: Boolean, val shortMessage: String? = null, val longMessage: String? = null) + +open class TaskResult(val success: Boolean = true, + val testResult: TestResult? = null, + val errorMessage: String? = null +) diff --git a/src/main/kotlin/com/beust/kobalt/Template.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Template.kt similarity index 100% rename from src/main/kotlin/com/beust/kobalt/Template.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Template.kt diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/TestConfig.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/TestConfig.kt new file mode 100644 index 00000000..f84b094d --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/TestConfig.kt @@ -0,0 +1,40 @@ +package com.beust.kobalt + +import com.beust.kobalt.api.Project +import com.beust.kobalt.api.annotation.Directive + +class TestConfig(val project: Project, val isDefault : Boolean = false) { + val testArgs = arrayListOf() + val jvmArgs = arrayListOf() + val testIncludes = arrayListOf("**/*Test.class") + val testExcludes = arrayListOf() + + @Directive + var name: String = "" + + @Directive + fun args(vararg arg: String) { + testArgs.addAll(arg) + } + + @Directive + fun jvmArgs(vararg arg: String) { + jvmArgs.addAll(arg) + } + + @Directive + fun include(vararg arg: String) { + testIncludes.apply { + clear() + addAll(arg) + } + } + + @Directive + fun exclude(vararg arg: String) { + testExcludes.apply { + clear() + addAll(arg) + } + } +} \ No newline at end of file diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/TestDirective.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/TestDirective.kt new file mode 100644 index 00000000..ad026380 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/TestDirective.kt @@ -0,0 +1,18 @@ +package com.beust.kobalt + +import com.beust.kobalt.api.Project +import com.beust.kobalt.api.annotation.Directive + +@Directive +fun Project.test(init: TestConfig.() -> Unit): TestConfig = let { project -> + with(testConfigs) { + val tf = TestConfig(project).apply { init() } + if (! map { it.name }.contains(tf.name)) { + add(tf) + tf + } else { + throw KobaltException("Test configuration \"${tf.name}\" already exists, give it a different " + + "name with test { name = ... }") + } + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Variant.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Variant.kt new file mode 100644 index 00000000..13120fa0 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Variant.kt @@ -0,0 +1,236 @@ +package com.beust.kobalt + +import com.beust.kobalt.api.* +import com.beust.kobalt.internal.ActorUtils +import com.beust.kobalt.internal.ParallelLogger +import com.beust.kobalt.internal.SourceSet +import com.beust.kobalt.misc.KFiles +import java.io.File +import java.util.* + +/** + * Capture the product flavor and the build type of a build. + */ +class Variant(val initialProductFlavor: ProductFlavorConfig? = null, + val initialBuildType: BuildTypeConfig? = null) { + + val productFlavor: ProductFlavorConfig by lazy { + initialProductFlavor ?: Variant.DEFAULT_PRODUCT_FLAVOR + } + val buildType: BuildTypeConfig by lazy { + initialBuildType ?: Variant.DEFAULT_BUILD_TYPE + } + + val isDefault : Boolean + get() = productFlavor == DEFAULT_PRODUCT_FLAVOR && buildType == DEFAULT_BUILD_TYPE + + fun toTask(taskName: String) = taskName + productFlavor.name.capitalize() + buildType.name.capitalize() + + /** + * for {internal, release}, return [internalRelease, internal, release] + */ + fun allDirectories(): List { + return arrayListOf().apply { + add(toCamelcaseDir()) + add(productFlavor.name) + add(buildType.name) + } + } + + fun sourceDirectories(project: Project, context: KobaltContext, sourceSet: SourceSet) : List { + val result = arrayListOf() + val compilerContributors = ActorUtils.selectAffinityActors(project, context, + context.pluginInfo.compilerContributors) + compilerContributors.forEach { + it.compilersFor(project, context).forEach { compiler -> + result.addAll(sourceDirectories(project, compiler.sourceDirectory, variantFirst = true, + sourceSet = sourceSet)) + } + + } + return result.filter { ! KFiles.isResource(it.path) }.toList() + } + + /** + * Might be used by plug-ins. + */ + fun resourceDirectories(project: Project, sourceSet: SourceSet = SourceSet.MAIN) + = sourceDirectories(project, "resources", variantFirst = false, sourceSet = sourceSet) + .filter { KFiles.isResource(it.path) } + + /** + * suffix is either "java" (to find source files) or "resources" (to find resources). + * The priority directory is always returned first. For example, if a "pro" product flavor + * is requested, "src/pro/kotlin" will appear in the result before "src/main/kotlin". Later, + * files that have already been seen get skipped, which is how compilation and resources + * receive the correct priority in the final jar. + */ + private fun sourceDirectories(project: Project, suffix: String, variantFirst: Boolean, sourceSet: SourceSet) + : List { + val result = arrayListOf() + val sourceDirectories = sourceSet.correctSourceSet(project) + .filter { File(project.directory, it).exists() } + .map(::File) + + if (isDefault) { + result.addAll(sourceDirectories) + } else { +// // The ordering of files is: 1) build type 2) product flavor 3) default + val kobaltLog = Kobalt.INJECTOR.getInstance(ParallelLogger::class.java) + buildType.let { + val dir = File(KFiles.joinDir("src", it.name, suffix)) + kobaltLog.log(project.name, 3, "Adding source for build type ${it.name}: ${dir.path}") + result.add(dir) + } + productFlavor.let { + val dir = File(KFiles.joinDir("src", it.name, suffix)) + kobaltLog.log(project.name, 3, "Adding source for product flavor ${it.name}: ${dir.path}") + result.add(dir) + } + + result.addAll(allDirectories() + .map { File(KFiles.joinDir("src", it, suffix)) } + .filter(File::exists)) + + // Now that all the variant source directories have been added, add the project's default ones + result.addAll(sourceDirectories) + } + + // Generated directory, if applicable + generatedSourceDirectory?.let { + result.add(it) + } + + val filteredResult = result.filter { File(project.directory, it.path).exists() } + val sortedResult = if (variantFirst) filteredResult + else filteredResult.reversed().toList() + val deduplicatedResult = LinkedHashSet(sortedResult).toList() + return deduplicatedResult + } + + fun archiveName(project: Project, archiveName: String?, suffix: String) : String { + val result = + if (isDefault) { + archiveName ?: project.name + "-" + project.version + suffix + } else { + val base = archiveName?.substring(0, archiveName.length - suffix.length) + ?: project.name + "-" + project.version + val flavor = if (productFlavor.name.isEmpty()) "" else "-" + productFlavor.name + val type = if (buildType.name.isEmpty()) "" else "-" + buildType.name + val result: String = base + flavor + type + suffix + + result + } + return result + } + + var generatedSourceDirectory: File? = null + + private fun findBuildTypeBuildConfig(project: Project, variant: Variant?) : BuildConfig? { + val buildTypeName = variant?.buildType?.name + return project.buildTypes[buildTypeName]?.buildConfig + } + + private fun findProductFlavorBuildConfig(project: Project, variant: Variant?) : BuildConfig? { + val buildTypeName = variant?.productFlavor?.name + return project.productFlavors[buildTypeName]?.buildConfig + } + + /** + * Return a list of the BuildConfigs found on the productFlavor{}, buildType{} and project{} (in that order). + */ + private fun findBuildConfigs(project: Project, variant: Variant?) : List { + val result = listOf( + findBuildTypeBuildConfig(project, variant), + findProductFlavorBuildConfig(project, variant), + project.buildConfig) + .filterNotNull() + + return result + } + + /** + * Generate BuildConfig.java if requested. Also look up if any BuildConfig is defined on the current build type, + * product flavor or main project, and use them to generateAndSave any additional field (in that order to + * respect the priorities). Return the generated file if it was generated, null otherwise. + */ + fun maybeGenerateBuildConfig(project: Project, context: KobaltContext) : File? { + val buildConfigs = findBuildConfigs(project, this) + + if (buildConfigs.size > 0) { + val pkg = project.packageName ?: project.group + ?: throw KobaltException( + "packageName needs to be defined on the project in order to generateAndSave BuildConfig") + + val contributor = ActorUtils.selectAffinityActor(project, context, + context.pluginInfo.buildConfigContributors) + if (contributor != null) { + val code = contributor.generateBuildConfig(project, context, pkg, this, buildConfigs) + val result = KFiles.makeDir(KFiles.generatedSourceDir(project, this, "buildConfig")) + // Make sure the generatedSourceDirectory doesn't contain the project.directory since + // that directory will be added when trying to find recursively all the sources in it + generatedSourceDirectory = result.relativeTo(File(project.directory)) + val outputGeneratedSourceDirectory = File(result, pkg.replace('.', File.separatorChar)) + val outputDir = File(outputGeneratedSourceDirectory, "BuildConfig." + contributor.buildConfigSuffix) + KFiles.saveFile(outputDir, code) + context.logger.log(project.name, 2, "Generated ${outputDir.path}") + return result + } else { + throw KobaltException("Couldn't find a contributor to generateAndSave BuildConfig") + } + } else { + return null + } + } + + override fun toString() = toTask("") + + + companion object { + val DEFAULT_PRODUCT_FLAVOR = ProductFlavorConfig("") + val DEFAULT_BUILD_TYPE = BuildTypeConfig("") + + fun allVariants(project: Project): List { + val result = arrayListOf() + + if (project.buildTypes.size > 0) { + project.buildTypes.keys.forEach { + val bt = project.buildTypes[it] + if (project.productFlavors.size > 0) { + project.productFlavors.keys.forEach { + result.add(Variant(project.productFlavors[it], bt)) + } + } else { + result.add(Variant(null, bt)) + } + } + } else if (project.productFlavors.size > 0) { + project.productFlavors.keys.forEach { + val pf = project.productFlavors[it] + if (project.buildTypes.size > 0) { + project.buildTypes.keys.forEach { + result.add(Variant(pf, project.buildTypes[it])) + } + } else { + result.add(Variant(pf, null)) + } + } + } + return result + } + } + + fun toCamelcaseDir() : String { + fun lci(s : String) = if (s.isEmpty() || s.length == 1) s else s[0].toLowerCase() + s.substring(1) + + return lci(productFlavor.name) + buildType.name.capitalize() + } + + fun toIntermediateDir() : String { + if (isDefault) { + return "" + } else { + return KFiles.joinDir(productFlavor.name, buildType.name) + } + } +} \ No newline at end of file diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/BasePlugin.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/BasePlugin.kt new file mode 100644 index 00000000..ff6d1e95 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/BasePlugin.kt @@ -0,0 +1,19 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.Plugins +import com.beust.kobalt.internal.TaskManager + +abstract class BasePlugin : IPlugin { + lateinit var context: KobaltContext + + override fun accept(project: Project) = true + + override fun apply(project: Project, context: KobaltContext) { + this.context = context + } + + override fun shutdown() {} + + override lateinit var taskManager: TaskManager + lateinit var plugins: Plugins +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/BuildTypeConfig.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/BuildTypeConfig.kt new file mode 100644 index 00000000..2db3d939 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/BuildTypeConfig.kt @@ -0,0 +1,11 @@ +package com.beust.kobalt.api + +class BuildTypeConfig(val name: String) : IBuildConfig, IDependencyHolder by DependencyHolder() { + + var minifyEnabled = false + var applicationIdSuffix: String? = null + var proguardFile: String? = null + + override var buildConfig : BuildConfig? = BuildConfig() +} + diff --git a/src/main/kotlin/com/beust/kobalt/api/CompilerActionInfo.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/CompilerActionInfo.kt similarity index 57% rename from src/main/kotlin/com/beust/kobalt/api/CompilerActionInfo.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/CompilerActionInfo.kt index 9a1d374d..e323e474 100644 --- a/src/main/kotlin/com/beust/kobalt/api/CompilerActionInfo.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/CompilerActionInfo.kt @@ -8,7 +8,9 @@ import java.io.File data class CompilerActionInfo(val directory: String?, val dependencies: List, val sourceFiles: List, + val suffixesBeingCompiled: List, val outputDir: File, - val compilerArgs: List) - - + val compilerArgs: List, + val friendPaths: List, + val forceRecompile: Boolean, + val compilerSeparateProcess: Boolean = false) diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ConfigActor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ConfigActor.kt new file mode 100644 index 00000000..cd4ae745 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ConfigActor.kt @@ -0,0 +1,19 @@ +package com.beust.kobalt.api + +import java.util.* + +/** + * Actors that have one config object per project can implement `IConfigActor` by delegating to + * `ConfigActor`. Then they can easily add and look up configurations per project. + */ +interface IConfigActor { + val configurations : HashMap + + fun configurationFor(project: Project?) = if (project != null) configurations[project.name] else null + + fun addConfiguration(project: Project, configuration: T) = configurations.put(project.name, configuration) +} + +open class ConfigActor: IConfigActor { + override val configurations : HashMap = hashMapOf() +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ConfigsActor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ConfigsActor.kt new file mode 100644 index 00000000..5ec4ce53 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ConfigsActor.kt @@ -0,0 +1,19 @@ +package com.beust.kobalt.api + +import com.google.common.collect.ArrayListMultimap +import com.google.common.collect.ListMultimap + +/** + * Actors that have more than config object per project can use this helper class. + */ +interface IConfigsActor { + val configurations : ListMultimap + + fun configurationFor(project: Project?) = if (project != null) configurations[project.name] else null + + fun addConfiguration(project: Project, configuration: T) = configurations.put(project.name, configuration) +} + +open class ConfigsActor: IConfigsActor { + override val configurations = ArrayListMultimap.create() +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/DependencyHolder.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/DependencyHolder.kt new file mode 100644 index 00000000..e1195ca3 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/DependencyHolder.kt @@ -0,0 +1,47 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.api.annotation.Directive +import java.util.* + +/** + * Various elements in a build file let you specify dependencies: projects, buildType and productFlavor. + * They all implement this interface and delegate to an instance of the `DependencyHolder` concrete class. + */ +interface IDependencyHolder { + var project: Project + + val compileDependencies : ArrayList + val optionalDependencies : ArrayList + val compileProvidedDependencies : ArrayList + val compileOnlyDependencies : ArrayList + val compileRuntimeDependencies : ArrayList + val excludedDependencies : ArrayList + val nativeDependencies : ArrayList + + @Directive + var dependencies: Dependencies? + + @Directive + fun dependencies(init: Dependencies.() -> Unit) : Dependencies +} + +open class DependencyHolder : IDependencyHolder { + override lateinit var project: Project + override val compileDependencies : ArrayList = arrayListOf() + override val optionalDependencies : ArrayList = arrayListOf() + override val compileProvidedDependencies : ArrayList = arrayListOf() + override val compileOnlyDependencies : ArrayList = arrayListOf() + override val compileRuntimeDependencies : ArrayList = arrayListOf() + override val excludedDependencies : ArrayList = arrayListOf() + override val nativeDependencies : ArrayList = arrayListOf() + + override var dependencies : Dependencies? = null + + override fun dependencies(init: Dependencies.() -> Unit) : Dependencies { + dependencies = Dependencies(project, compileDependencies, optionalDependencies, compileProvidedDependencies, + compileOnlyDependencies, compileRuntimeDependencies, excludedDependencies, nativeDependencies) + dependencies!!.init() + return dependencies!! + } +} + diff --git a/src/main/kotlin/com/beust/kobalt/api/IAffinity.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IAffinity.kt similarity index 50% rename from src/main/kotlin/com/beust/kobalt/api/IAffinity.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IAffinity.kt index 5a2e8e60..e02d54bb 100644 --- a/src/main/kotlin/com/beust/kobalt/api/IAffinity.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IAffinity.kt @@ -1,5 +1,8 @@ package com.beust.kobalt.api +/** + * Base interface for affinity interfaces. + */ interface IAffinity { companion object { /** @@ -8,11 +11,5 @@ interface IAffinity { */ const val DEFAULT_POSITIVE_AFFINITY = 100 } - - /** - * @return an integer indicating the affinity of your actor for the given project. The actor that returns - * the highest affinity gets selectedyour affinity for running the current project. The runner with - * the highest affinity gets selected. - */ - fun affinity(project: Project, context: KobaltContext) : Int } + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IAssemblyContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IAssemblyContributor.kt new file mode 100644 index 00000000..6c35fc1d --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IAssemblyContributor.kt @@ -0,0 +1,10 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.TaskResult + +/** + * Plug-ins that will be invoked during the "assemble" task. + */ +interface IAssemblyContributor : IContributor { + fun assemble(project: Project, context: KobaltContext) : TaskResult +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildConfigContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildConfigContributor.kt new file mode 100644 index 00000000..ef9d3b4d --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildConfigContributor.kt @@ -0,0 +1,16 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.Variant + +/** + * Plug-ins that can generate a BuildConfig file. + */ +interface IBuildConfigContributor : IProjectAffinity { + fun generateBuildConfig(project: Project, context: KobaltContext, packageName: String, variant: Variant, + buildConfigs: List) : String + + /** + * The suffix of the generated BuildConfig, e.g. ".java". + */ + val buildConfigSuffix: String +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildConfigFieldContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildConfigFieldContributor.kt new file mode 100644 index 00000000..0d26032f --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildConfigFieldContributor.kt @@ -0,0 +1,10 @@ +package com.beust.kobalt.api + +class BuildConfigField(val type: String, val name: String, val value: Any) + +/** + * Plug-ins that want to add fields to BuildConfig need to implement this interface. + */ +interface IBuildConfigFieldContributor : IContributor { + fun fieldsFor(project: Project, context: KobaltContext) : List +} diff --git a/src/main/kotlin/com/beust/kobalt/api/IBuildDirectoryInterceptor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildDirectoryInterceptor.kt similarity index 79% rename from src/main/kotlin/com/beust/kobalt/api/IBuildDirectoryInterceptor.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildDirectoryInterceptor.kt index 4d722f12..afb0f1ef 100644 --- a/src/main/kotlin/com/beust/kobalt/api/IBuildDirectoryInterceptor.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildDirectoryInterceptor.kt @@ -3,7 +3,7 @@ package com.beust.kobalt.api /** * Plug-ins can alter the build directory by implementing this interface. */ -interface IBuildDirectoryIncerceptor : IPluginActor { +interface IBuildDirectoryInterceptor : IInterceptor { fun intercept(project: Project, context: KobaltContext, buildDirectory: String) : String } diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildListener.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildListener.kt new file mode 100644 index 00000000..2b0fdadb --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildListener.kt @@ -0,0 +1,20 @@ +package com.beust.kobalt.api + +/** + * Plug-ins that listen to build events. + */ +interface IBuildListener : IListener { + + class TaskEndInfo(val success: Boolean, val shortMessage: String? = null, + val longMessage: String? = null) + + fun taskStart(project: Project, context: KobaltContext, taskName: String) {} + fun taskEnd(project: Project, context: KobaltContext, taskName: String, info: TaskEndInfo) {} + + fun projectStart(project: Project, context: KobaltContext) {} + fun projectEnd(project: Project, context: KobaltContext, status: ProjectBuildStatus) {} +} + +enum class ProjectBuildStatus { + SUCCESS, FAILED, SKIPPED +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildReportContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildReportContributor.kt new file mode 100644 index 00000000..130c50d2 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildReportContributor.kt @@ -0,0 +1,8 @@ +package com.beust.kobalt.api + +/** + * Plug-ins that produce build reports. + */ +interface IBuildReportContributor : IContributor { + fun generateReport(context: KobaltContext) +} diff --git a/src/main/kotlin/com/beust/kobalt/api/IClasspathContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathContributor.kt similarity index 57% rename from src/main/kotlin/com/beust/kobalt/api/IClasspathContributor.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathContributor.kt index 1474d342..ce9afc39 100644 --- a/src/main/kotlin/com/beust/kobalt/api/IClasspathContributor.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathContributor.kt @@ -1,12 +1,10 @@ package com.beust.kobalt.api -import com.beust.kobalt.api.IClasspathDependency - /** * Plugins that export classpath entries need to implement this interface. */ interface IClasspathContributor : IContributor { - fun entriesFor(project: Project?) : Collection + fun classpathEntriesFor(project: Project?, context: KobaltContext) : Collection } diff --git a/src/main/kotlin/com/beust/kobalt/api/IClasspathDependency.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathDependency.kt similarity index 61% rename from src/main/kotlin/com/beust/kobalt/api/IClasspathDependency.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathDependency.kt index 9ee3f4dc..527e6f13 100644 --- a/src/main/kotlin/com/beust/kobalt/api/IClasspathDependency.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathDependency.kt @@ -8,20 +8,34 @@ import java.util.concurrent.Future * Encapsulate a dependency that can be put on the classpath. This interface * has two subclasses: FileDependency, a physical file, and MavenDependency, * which represents a dependency living in a Maven repo. + * + * You can instantiate either of these concrete classes with DependencyManager#createMaven + * and DependencyManager#createFile. */ interface IClasspathDependency { /** Identifier for this dependency */ val id: String + /** Version for this identifier */ + val version: String + + /** @return true if this dependency represents a Maven coordinate */ + val isMaven: Boolean + + /** @return true if this dependency is optional */ + val optional: Boolean + /** Absolute path to the jar file on the local file system */ val jarFile: Future /** Convert to a Maven model tag */ - fun toMavenDependencies() : Dependency + fun toMavenDependencies(scope: String? = null) : Dependency - /** The list of dependencies for this element (not the transitive closure */ + /** The list of dependencies for this element (not the transitive closure) */ fun directDependencies(): List /** Used to only keep the most recent version for an artifact if no version was specified */ val shortId: String + + val excluded: ArrayList } \ No newline at end of file diff --git a/src/main/kotlin/com/beust/kobalt/api/IClasspathInterceptor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathInterceptor.kt similarity index 52% rename from src/main/kotlin/com/beust/kobalt/api/IClasspathInterceptor.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathInterceptor.kt index b105928b..906dee86 100644 --- a/src/main/kotlin/com/beust/kobalt/api/IClasspathInterceptor.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathInterceptor.kt @@ -1,10 +1,8 @@ package com.beust.kobalt.api -import com.beust.kobalt.api.IClasspathDependency - /** * Modify a list of dependencies before Kobalt starts using them. */ interface IClasspathInterceptor : IInterceptor { - fun intercept(dependencies: List) : List + fun intercept(project: Project, dependencies: List) : List } diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ICompilerContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ICompilerContributor.kt new file mode 100644 index 00000000..b6ce8fd2 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ICompilerContributor.kt @@ -0,0 +1,68 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.TaskResult + +interface ICompilerDescription : Comparable { + /** + * The name of the language compiled by this compiler. + */ + val name: String + + /** + * The suffixes handled by this compiler (without the dot, e.g. "java" or "kt"). + */ + val sourceSuffixes: List + + /** + * The trailing end of the source directory (e.g. "kotlin" in "src/main/kotlin") + */ + val sourceDirectory: String + + /** + * Run the compilation based on the info. + */ + fun compile(project: Project, context: KobaltContext, info: CompilerActionInfo) : TaskResult + + companion object { + val DEFAULT_PRIORITY: Int = 10 + } + + /** + * The priority of this compiler. Lower priority compilers are run first. + */ + val priority: Int get() = DEFAULT_PRIORITY + + override fun compareTo(other: ICompilerDescription) = priority.compareTo(other.priority) + + /** + * Can this compiler be passed directories or does it need individual source files? + */ + val canCompileDirectories: Boolean get() = false +} + +interface ICompilerContributor : IProjectAffinity, IContributor { + fun compilersFor(project: Project, context: KobaltContext): List +} + +interface ICompiler { + fun compile(project: Project, context: KobaltContext, info: CompilerActionInfo): TaskResult +} + +class CompilerDescription(override val name: String, override val sourceDirectory: String, + override val sourceSuffixes: List, val compiler: ICompiler, + override val priority: Int = ICompilerDescription.DEFAULT_PRIORITY, + override val canCompileDirectories: Boolean = false) : ICompilerDescription { + override fun compile(project: Project, context: KobaltContext, info: CompilerActionInfo): TaskResult { + val result = + if (info.sourceFiles.isNotEmpty()) { + compiler.compile(project, context, info) + } else { + context.logger.log(project.name, 2, "$name couldn't find any source files to compile") + TaskResult() + } + return result + } + + override fun toString() = name + " compiler" +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ICompilerFlagContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ICompilerFlagContributor.kt new file mode 100644 index 00000000..2d8febe4 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ICompilerFlagContributor.kt @@ -0,0 +1,29 @@ +package com.beust.kobalt.api + +/** + * Plugins that add compiler flags. + */ +class FlagContributor(val flagPriority: Int = DEFAULT_FLAG_PRIORITY, + val closure: (project: Project, context: KobaltContext, currentFlags: List, + suffixesBeingCompiled: List) -> List) : IContributor { + companion object { + val DEFAULT_FLAG_PRIORITY = 20 + } + + fun flagsFor(project: Project, context: KobaltContext, currentFlags: List, + suffixesBeingCompiled: List) = closure(project, context, currentFlags, suffixesBeingCompiled) +} + +interface IFlagBase { + val flagPriority: Int get() = FlagContributor.DEFAULT_FLAG_PRIORITY +} + +interface ICompilerFlagContributor : IContributor, IFlagBase { + fun compilerFlagsFor(project: Project, context: KobaltContext, currentFlags: List, + suffixesBeingCompiled: List): List +} + +interface IDocFlagContributor : IContributor, IFlagBase { + fun docFlagsFor(project: Project, context: KobaltContext, currentFlags: List, + suffixesBeingCompiled: List): List +} \ No newline at end of file diff --git a/src/main/kotlin/com/beust/kobalt/api/ICompilerInterceptor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ICompilerInterceptor.kt similarity index 100% rename from src/main/kotlin/com/beust/kobalt/api/ICompilerInterceptor.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ICompilerInterceptor.kt diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IDependencyManager.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IDependencyManager.kt new file mode 100644 index 00000000..3a66f980 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IDependencyManager.kt @@ -0,0 +1,88 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.maven.aether.Filters.EXCLUDE_OPTIONAL_FILTER +import com.beust.kobalt.maven.aether.KobaltMavenResolver +import com.beust.kobalt.maven.aether.Scope +import com.beust.kobalt.misc.kobaltLog +import org.eclipse.aether.graph.DependencyFilter +import org.eclipse.aether.graph.DependencyNode + +/** + * Manage the creation of dependencies and also provide dependencies for projects. + */ +interface IDependencyManager { + /** + * Parse the id and return the correct IClasspathDependency + */ + fun create(id: String, optional: Boolean = false, projectDirectory: String? = null): IClasspathDependency + + /** + * Create an IClasspathDependency from a Maven id. + */ + fun createMaven(id: String, optional: Boolean = false): IClasspathDependency + + /** + * Create an IClasspathDependency from a path. + */ + fun createFile(path: String): IClasspathDependency + + /** + * @return the source dependencies for this project, including the contributors. + */ + fun dependencies(project: Project, context: KobaltContext, scopes: List): List + + /** + * @return the test dependencies for this project, including the contributors. + */ + fun testDependencies(project: Project, context: KobaltContext): List + + /** + * @return the classpath for this project, including the IClasspathContributors. + * allDependencies is typically either compileDependencies or testDependencies + */ + fun calculateDependencies(project: Project?, context: KobaltContext, + dependencyFilter: DependencyFilter = + createDependencyFilter(project, project?.compileDependencies ?: emptyList()), + scopes: List = listOf(Scope.COMPILE), + vararg passedDependencies: List): List + + /** + * Create an Aether dependency filter that uses the dependency configuration included in each + * IClasspathDependency. + */ + fun createDependencyFilter(project: Project?, dependencies: List) : DependencyFilter { + return DependencyFilter { p0, p1 -> + fun isNodeExcluded(node: DependencyNode, passedDep: IClasspathDependency) : Boolean { + val dep = create(KobaltMavenResolver.artifactToId(node.artifact)) + return passedDep.excluded.any { ex -> ex.isExcluded(dep)} + } + fun isDepExcluded(node: DependencyNode, excluded: List?) : Boolean { + val dep = create(KobaltMavenResolver.artifactToId(node.artifact)) + return excluded?.map { it.id }?.contains(dep.id) ?: false + } + + val accept = dependencies.isEmpty() || dependencies.any { + // Is this dependency excluded? + val isExcluded = isNodeExcluded(p0, it) || isDepExcluded(p0, project?.excludedDependencies) + + // Is the parent dependency excluded? + val isParentExcluded = + if (p1.any()) { + isNodeExcluded(p1[0], it) || isDepExcluded(p1[0], project?.excludedDependencies) + } else { + false + } + + // Only accept if no exclusions were found + ! isExcluded && ! isParentExcluded + } + + if (! accept) { + kobaltLog(2, "Excluding $p0") + } + + if (accept) EXCLUDE_OPTIONAL_FILTER.accept(p0, p1) + else accept + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/beust/kobalt/api/IDocContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IDocContributor.kt similarity index 73% rename from src/main/kotlin/com/beust/kobalt/api/IDocContributor.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IDocContributor.kt index df28f9c5..48797530 100644 --- a/src/main/kotlin/com/beust/kobalt/api/IDocContributor.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IDocContributor.kt @@ -2,7 +2,7 @@ package com.beust.kobalt.api import com.beust.kobalt.TaskResult -interface IDocContributor : IAffinity { +interface IDocContributor : IProjectAffinity, IContributor { fun generateDoc(project: Project, context: KobaltContext, info: CompilerActionInfo) : TaskResult } diff --git a/src/main/kotlin/com/beust/kobalt/api/IFactory.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IFactory.kt similarity index 100% rename from src/main/kotlin/com/beust/kobalt/api/IFactory.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IFactory.kt diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IIncrementalAssemblyContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IIncrementalAssemblyContributor.kt new file mode 100644 index 00000000..cd730171 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IIncrementalAssemblyContributor.kt @@ -0,0 +1,12 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.IncrementalTaskInfo + +/** + * Plug-ins that will be invoked during the "assemble" task and wish to return an incremental task instead + * of a regular one. + */ +interface IIncrementalAssemblyContributor : IContributor { + fun assemble(project: Project, context: KobaltContext) : IncrementalTaskInfo +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IIncrementalTaskContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IIncrementalTaskContributor.kt new file mode 100644 index 00000000..1decac22 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IIncrementalTaskContributor.kt @@ -0,0 +1,28 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.IncrementalTaskInfo + +/** + * Plug-ins that need to add incremental dynamic tasks (tasks that are not methods annotated with @Task) need + * to implement this interface. + */ +interface IIncrementalTaskContributor : IContributor { + fun incrementalTasksFor(project: Project, context: KobaltContext) : List +} + +class IncrementalDynamicTask(val context: KobaltContext, + val plugin: IPlugin, + val name: String, + val doc: String, + val group: String, + val project: Project, + val dependsOn: List = listOf(), + val reverseDependsOn: List = listOf(), + val runBefore: List = listOf(), + val runAfter: List = listOf(), + val alwaysRunAfter: List = listOf(), + val incrementalClosure: (Project) -> IncrementalTaskInfo) { + override fun toString() = "[IncrementalDynamicTask $name dependsOn=$dependsOn reverseDependsOn=$reverseDependsOn]" +} + + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IJvmFlagContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IJvmFlagContributor.kt new file mode 100644 index 00000000..f912ae1d --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IJvmFlagContributor.kt @@ -0,0 +1,13 @@ +package com.beust.kobalt.api + +/** + * Plug-ins that add flags to the JVM used to run apps should implement this interface. + */ +interface IJvmFlagContributor : IContributor { + /** + * The list of JVM flags that will be added to the JVM when the app gets run. @param[currentFlags] is only here + * for convenience, in case you need to look at the current JVM flags before adding your own flags. + */ + fun jvmFlagsFor(project: Project, context: KobaltContext, currentFlags: List) : List +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ILocalMavenRepoPathInterceptor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ILocalMavenRepoPathInterceptor.kt new file mode 100644 index 00000000..1993f130 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ILocalMavenRepoPathInterceptor.kt @@ -0,0 +1,8 @@ +package com.beust.kobalt.api + +/** + * Plug-ins that want to override the local maven repo path. + */ +interface ILocalMavenRepoPathInterceptor : IInterceptor { + fun repoPath(currentPath: String) : String +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IMavenIdInterceptor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IMavenIdInterceptor.kt new file mode 100644 index 00000000..fd9fc115 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IMavenIdInterceptor.kt @@ -0,0 +1,10 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.maven.MavenId + +/** + * Plug-ins can rewrite Maven id's before Kobalt sees them with this interface. + */ +interface IMavenIdInterceptor: IInterceptor { + fun intercept(mavenId: MavenId) : MavenId +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IPlugin.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IPlugin.kt new file mode 100644 index 00000000..e9d7acb6 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IPlugin.kt @@ -0,0 +1,30 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.internal.TaskManager + +interface IPlugin : IPluginActor { + /** + * The name of this plug-in. + */ + val name: String + + /** + * @return true if this plug-in decided it should be enabled for this project. + */ + fun accept(project: Project) : Boolean + + /** + * Invoked on all plug-ins before the Kobalt execution stops. + */ + fun shutdown() + + /** + * Main entry point for a plug-in to initialize itself based on a project and a context. + */ + fun apply(project: Project, context: KobaltContext) {} + + /** + * Injected by Kobalt to manage tasks. + */ + var taskManager : TaskManager +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IPluginActor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IPluginActor.kt new file mode 100644 index 00000000..96d54218 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IPluginActor.kt @@ -0,0 +1,15 @@ +package com.beust.kobalt.api + +interface IPluginActor { + /** + * Clean up any state that your actor might have saved so it can be run again. + */ + fun cleanUpActors() {} +} + +interface IContributor : IPluginActor + +interface IInterceptor : IPluginActor + +interface IListener : IPluginActor + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IProjectAffinity.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IProjectAffinity.kt new file mode 100644 index 00000000..4e667b6e --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IProjectAffinity.kt @@ -0,0 +1,12 @@ +package com.beust.kobalt.api + +/** + * An affinity interface that uses a project and a context to calculate its affinity. + */ +interface IProjectAffinity : IAffinity { + /** + * @return an integer indicating the affinity of your actor for the given project. The actor that returns + * the highest affinity gets selected. + */ + fun affinity(project: Project, context: KobaltContext) : Int +} diff --git a/src/main/kotlin/com/beust/kobalt/api/IProjectContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IProjectContributor.kt similarity index 100% rename from src/main/kotlin/com/beust/kobalt/api/IProjectContributor.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IProjectContributor.kt diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IRepoContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IRepoContributor.kt new file mode 100644 index 00000000..0df2baee --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IRepoContributor.kt @@ -0,0 +1,16 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.HostConfig + +/** + * Plugins that add their own repos. + */ +interface IRepoContributor : IContributor { + /** + * Note that the project passed might be null because this contributor is called twice: + * before the build file gets parsed (so we don't have any projects yet) and after the + * build file has been parsed (then it gets called once for each project discovered). + */ + fun reposFor(project: Project?) : List +} + diff --git a/src/main/kotlin/com/beust/kobalt/api/IRunnerContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IRunnerContributor.kt similarity index 74% rename from src/main/kotlin/com/beust/kobalt/api/IRunnerContributor.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IRunnerContributor.kt index 5f094758..f8c28b52 100644 --- a/src/main/kotlin/com/beust/kobalt/api/IRunnerContributor.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IRunnerContributor.kt @@ -1,12 +1,13 @@ package com.beust.kobalt.api import com.beust.kobalt.TaskResult -import com.beust.kobalt.api.IClasspathDependency /** * Plugins that can run a project (task "run" or "test") should implement this interface. + * + * Currently not used. */ -interface IRunnerContributor : IContributor, IAffinity { +interface IRunnerContributor : IContributor, IProjectAffinity { /** * Run the project. */ diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ISimpleAffinity.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ISimpleAffinity.kt new file mode 100644 index 00000000..b9c7cdfe --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ISimpleAffinity.kt @@ -0,0 +1,12 @@ +package com.beust.kobalt.api + +/** + * An affinity interface that gets run without a project nor a context. + */ +interface ISimpleAffinity : IAffinity { + /** + * @return an integer indicating the affinity of your actor. The actor that returns + * the highest affinity gets selected. + */ + fun affinity(project: T) : Int +} \ No newline at end of file diff --git a/src/main/kotlin/com/beust/kobalt/api/ISourceDirectoriesInterceptor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ISourceDirectoriesInterceptor.kt similarity index 81% rename from src/main/kotlin/com/beust/kobalt/api/ISourceDirectoriesInterceptor.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ISourceDirectoriesInterceptor.kt index 6e8af5eb..33ec6e7c 100644 --- a/src/main/kotlin/com/beust/kobalt/api/ISourceDirectoriesInterceptor.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ISourceDirectoriesInterceptor.kt @@ -5,7 +5,7 @@ import java.io.File /** * Plug-ins can alter the source directories by implementing this interface. */ -interface ISourceDirectoriesIncerceptor : IPluginActor { +interface ISourceDirectoryInterceptor : IInterceptor { fun intercept(project: Project, context: KobaltContext, sourceDirectories: List) : List } diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ISourceDirectoryContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ISourceDirectoryContributor.kt new file mode 100644 index 00000000..fced0771 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ISourceDirectoryContributor.kt @@ -0,0 +1,20 @@ +package com.beust.kobalt.api + +import java.io.File + +/** + * Plug-ins that add source directories to be compiled need to implement this interface. + */ +interface ISourceDirectoryContributor : IContributor { + fun sourceDirectoriesFor(project: Project, context: KobaltContext): List +} + +/** + * @return the source directories for this project including source contributors. + */ +fun KobaltContext.sourceDirectories(project: Project) : Set { + val result = pluginInfo.sourceDirContributors.flatMap { + it.sourceDirectoriesFor(project, this) + } + return result.toSet() +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITaskContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITaskContributor.kt new file mode 100644 index 00000000..c606d54f --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITaskContributor.kt @@ -0,0 +1,32 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.TaskResult +import com.beust.kobalt.internal.TaskResult2 + +/** + * Plug-ins that need to add dynamic tasks (tasks that are not methods annotated with @Task) need + * to implement this interface. + */ +interface ITaskContributor : IContributor { + fun tasksFor(project: Project, context: KobaltContext) : List +} + +class DynamicTask(override val plugin: IPlugin, override val name: String, override val doc: String, + override val group: String, + override val project: Project, + val dependsOn: List = listOf(), + val reverseDependsOn: List = listOf(), + val runBefore: List = listOf(), + val runAfter: List = listOf(), + val alwaysRunAfter: List = listOf(), + val closure: (Project) -> TaskResult) : ITask { + + override fun call(): TaskResult2 { + val taskResult = closure.invoke(project) + return TaskResult2(taskResult.success, errorMessage = taskResult.errorMessage, value = this) + } + + override fun toString() = + "[DynamicTask ${project.name}:$name dependsOn=$dependsOn reverseDependsOn=$reverseDependsOn]" +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITemplateContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITemplateContributor.kt new file mode 100644 index 00000000..9af4bb7d --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITemplateContributor.kt @@ -0,0 +1,45 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.Args + +/** + * Plugins that want to participate in the --init process (they can generate files to initialize + * a new project). + */ +interface ITemplateContributor : IContributor { + companion object { + val DIRECTORY_NAME = "templates" + } + + val templates: List +} + +interface ITemplate { + /** + * The name of this template. This is the name that will be looked up when passed to the --init + * argument. + */ + val templateName: String + + /** + * Description of this template. + */ + val templateDescription: String + + /** + * The plug-in this template belongs to. + */ + val pluginName: String + + /** + * Instructions to display to the user after a template has been generated. + */ + val instructions : String get() = "Build this project with `./kobaltw assemble`" + + /** + * Generate the files for this template. The parameter is the arguments that were passed to the kobaltw + * command. + */ + fun generateTemplate(args: Args, classLoader: ClassLoader) +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITestJvmFlagContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITestJvmFlagContributor.kt new file mode 100644 index 00000000..6f1c0bfb --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITestJvmFlagContributor.kt @@ -0,0 +1,12 @@ +package com.beust.kobalt.api + +/** + * Plug-ins that add flags to the JVM used to run tests should implement this interface. + */ +interface ITestJvmFlagContributor : IContributor { + /** + * The list of JVM flags that will be added to the JVM when the tests get run. @param[currentFlags] is only here + * for convenience, in case you need to look at the current JVM flags before adding your own flags. + */ + fun testJvmFlagsFor(project: Project, context: KobaltContext, currentFlags: List) : List +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITestJvmFlagInterceptor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITestJvmFlagInterceptor.kt new file mode 100644 index 00000000..389f4704 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITestJvmFlagInterceptor.kt @@ -0,0 +1,14 @@ +package com.beust.kobalt.api + +/** + * Plug-ins that add flags to the JVM used to run tests should implement this interface. + */ +interface ITestJvmFlagInterceptor : IInterceptor { + /** + * @return the list of all flags that should be used. If you only want to add flags to the current list, + * just return the concatenation of @param[currentFlags] and your own list (or use ITestJvmFlagContributor). + * If you actually alter the list of flags, make sure you don't remove anything critical from @param[currentFlags]. + */ + fun testJvmFlagsFor(project: Project, context: KobaltContext, currentFlags: List) : List +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITestRunnerContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITestRunnerContributor.kt new file mode 100644 index 00000000..b8b860f6 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITestRunnerContributor.kt @@ -0,0 +1,15 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.TaskResult + +/** + * Plugins that can run a project (task "run" or "test") should implement this interface. + */ +interface ITestRunnerContributor : IContributor, IProjectAffinity { + /** + * Run the tests. If [[configName]] is not empty, a specific test configuration is requested. + */ + fun run(project: Project, context: KobaltContext, configName: String, + classpath: List) : TaskResult +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITestSourceDirectoryContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITestSourceDirectoryContributor.kt new file mode 100644 index 00000000..646d9dd2 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITestSourceDirectoryContributor.kt @@ -0,0 +1,18 @@ +package com.beust.kobalt.api + +import java.io.File + +/** + * Plug-ins that add test source directories to be compiled need to implement this interface. + */ +interface ITestSourceDirectoryContributor : IContributor { + fun testSourceDirectoriesFor(project: Project, context: KobaltContext): List +} + +fun KobaltContext.testSourceDirectories(project: Project) : List { + val result = pluginInfo.testSourceDirContributors.flatMap { + it.testSourceDirectoriesFor(project, this) + } + return result +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/InputStreamJarTemplate.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/InputStreamJarTemplate.kt new file mode 100644 index 00000000..222a2829 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/InputStreamJarTemplate.kt @@ -0,0 +1,55 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.Args +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.kobaltLog +import java.io.* +import java.net.URL +import java.util.jar.JarInputStream + +/** + * Base class for templates that decompress a jar file. + */ +interface InputStreamJarTemplate : ITemplate { + val inputStream: InputStream + + override fun generateTemplate(args: Args, classLoader: ClassLoader) { + val destDir = File(".") + JarInputStream(inputStream).use { ins -> + var entry = ins.nextEntry + while (entry != null) { + val f = File(destDir.path + File.separator + entry.name) + if (entry.isDirectory) { + f.mkdir() + entry = ins.nextEntry + continue + } + + kobaltLog(2, " Extracting: $entry to ${f.absolutePath}") + FileOutputStream(f).use { fos -> + KFiles.copy(ins, fos) + } + entry = ins.nextEntry + } + } + } +} + +abstract class ResourceJarTemplate(jarName: String, val classLoader: ClassLoader) : InputStreamJarTemplate { + override val inputStream : InputStream = classLoader.getResource(jarName).openConnection().inputStream +} + +abstract class FileJarTemplate(val fileName: String) : InputStreamJarTemplate { + override val inputStream = FileInputStream(File(fileName)) +} + +abstract class HttpJarTemplate(val url: String) : InputStreamJarTemplate { + override val inputStream : InputStream + get() { + try { + return URL(url).openConnection().inputStream + } catch(ex: IOException) { + throw IllegalArgumentException("Couldn't connect to $url") + } + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/JarFinder.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/JarFinder.kt new file mode 100644 index 00000000..08f08924 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/JarFinder.kt @@ -0,0 +1,19 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.maven.DependencyManager +import java.io.File +import java.util.concurrent.Future + +class JarFinder { + companion object { + /** + * @return a Future for the jar file corresponding to this id. + */ + fun byIdFuture(id: String) : Future = DependencyManager.create(id).jarFile + + /** + * @return the jar file corresponding to this id. This might cause a network call. + */ + fun byId(id: String) = byIdFuture(id).get() + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Kobalt.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Kobalt.kt new file mode 100644 index 00000000..7d37a0b8 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Kobalt.kt @@ -0,0 +1,141 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.Constants +import com.beust.kobalt.HostConfig +import com.beust.kobalt.Plugins +import com.beust.kobalt.internal.PluginInfo +import com.beust.kobalt.maven.DependencyManager +import com.beust.kobalt.maven.aether.KobaltMavenResolver +import com.google.inject.Guice +import com.google.inject.Injector +import com.google.inject.Module +import java.io.InputStream +import java.time.Duration +import java.util.* + +class Kobalt { + companion object { + lateinit var INJECTOR : Injector + + fun init(module: Module) { + Kobalt.INJECTOR = Guice.createInjector(module) + + // + // Add all the plugins read in kobalt-plugin.xml to the Plugins singleton, so that code + // in the build file that calls Plugins.findPlugin() can find them (code in the + // build file do not have access to the KobaltContext). + // + val pluginInfo = Kobalt.INJECTOR.getInstance(PluginInfo::class.java) + pluginInfo.plugins.forEach { Plugins.addPluginInstance(it) } + } + + var context: KobaltContext? = null + + /** + * @return the repos calculated from various places where repos can be specified. + */ + val repos : Set + get() { + val settingsRepos = Kobalt.context?.settings?.defaultRepos?.map { HostConfig(it) } ?: emptyList() + // Repos from in the settings + val result = ArrayList( + (if (settingsRepos.isEmpty()) Constants.DEFAULT_REPOS + else settingsRepos) + ) + + // Repo from in the settings + Kobalt.context?.settings?.kobaltCompilerRepo?.let { + result.add(HostConfig(it)) + } + + // Repos from the repo contributors + Kobalt.context?.pluginInfo?.repoContributors?.forEach { + result.addAll(it.reposFor(null)) + } + + // Repos from the build file + result.addAll(reposFromBuildFiles) + + result.forEach { + KobaltMavenResolver.initAuthentication(it) + } + return result.toHashSet() + } + + val reposFromBuildFiles = hashSetOf() + + fun addRepo(repo: HostConfig) = reposFromBuildFiles.add( + if (repo.url.endsWith("/")) repo + else repo.copy(url = (repo.url + "/"))) + + val buildFileClasspath = arrayListOf() + + fun addBuildFileClasspath(dep: String) { + val dependencyManager = Kobalt.INJECTOR.getInstance(DependencyManager::class.java) + buildFileClasspath.add(dependencyManager.create(dep)) + } + + private val KOBALT_PROPERTIES = "kobalt.properties" + private val PROPERTY_KOBALT_VERSION = "kobalt.version" + private val PROPERTY_KOBALT_VERSION_CHECK_TIMEOUT = "kobalt.version.checkTimeout" // ISO-8601 + + /** kobalt.properties */ + private val kobaltProperties: Properties by lazy { readProperties() } + + /** + * Read the content of kobalt.properties + */ + private fun readProperties() : Properties { + val result = Properties() + + // kobalt.properties is internal to Kobalt + val url = Kobalt::class.java.classLoader.getResource(KOBALT_PROPERTIES) + if (url != null) { + readProperties(result, url.openConnection().inputStream) + } else { + throw AssertionError("Couldn't find $KOBALT_PROPERTIES") + } + + // local.properties can be used by external users +// Paths.get(LOCAL_PROPERTIES).let { path -> +// if (Files.exists(path)) { +// Files.newInputStream(path).use { +// readProperties(result, it) +// } +// } +// } + + return result + } + + private fun readProperties(properties: Properties, ins: InputStream) { + properties.load(ins) + ins.close() + properties.forEach { es -> System.setProperty(es.key.toString(), es.value.toString()) } + } + + val version: String + get() = kobaltProperties.getProperty(PROPERTY_KOBALT_VERSION) + + // Note: Duration is Java 8 only, might need an alternative if we want to support Java < 8 + val versionCheckTimeout: Duration + get() = Duration.parse( kobaltProperties.getProperty(PROPERTY_KOBALT_VERSION_CHECK_TIMEOUT) ?: "P1D") + + fun findPlugin(name: String) = Plugins.findPlugin(name) + + val optionsFromBuild = arrayListOf() + fun addKobaltOptions(options: Array) { + optionsFromBuild.addAll(options) + } + + val buildSourceDirs = arrayListOf() + fun addBuildSourceDirs(dirs: Array) { + buildSourceDirs.addAll(dirs) + } + + fun cleanUp() { + buildSourceDirs.clear() + buildFileClasspath.clear() + } + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/KobaltContext.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/KobaltContext.kt new file mode 100644 index 00000000..b7e5ace8 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/KobaltContext.kt @@ -0,0 +1,116 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.Args +import com.beust.kobalt.KobaltException +import com.beust.kobalt.Plugins +import com.beust.kobalt.Variant +import com.beust.kobalt.internal.ILogger +import com.beust.kobalt.internal.IncrementalManager +import com.beust.kobalt.internal.KobaltSettings +import com.beust.kobalt.internal.PluginInfo +import com.beust.kobalt.maven.DependencyManager +import com.beust.kobalt.maven.MavenId +import com.beust.kobalt.maven.PomGenerator +import com.beust.kobalt.maven.SimpleDep +import com.beust.kobalt.maven.aether.KobaltMavenResolver +import com.beust.kobalt.misc.KobaltExecutors +import java.io.File + +class KobaltContext(val args: Args) { + lateinit var variant: Variant + val profiles = arrayListOf() + + init { + args.profiles?.split(',')?.filterNotNull()?.forEach { + profiles.add(it) + } + } + + fun findPlugin(name: String) = Plugins.findPlugin(name) + + /** + * Files that can be resolved in the local cache. + */ + enum class FileType { JAR, POM, SOURCES, JAVADOC, OTHER } + + /** + * @param{id} is the Maven coordinate (e.g. "org.testng:testng:6.9.11"). If you are looking for a file + * that is not described by the enum (e.g. "aar"), use OTHER and make sure your @param{id} contains + * the fully qualified id (e.g. "com.example:example::aar:1.0"). + */ + fun fileFor(id: String, fileType: FileType) : File { + val dep = SimpleDep(MavenId.create(id)) + fun toQualifier(dep: SimpleDep, ext: String, qualifier: String?) = + dep.groupId + ":" + dep.artifactId + + ":$ext" + + (if (qualifier != null) ":$qualifier" else "") + + ":" + dep.version + val fullId = + when (fileType) { + FileType.JAR -> toQualifier(dep, "jar", null) + FileType.POM -> toQualifier(dep, "pom", null) + FileType.SOURCES -> toQualifier(dep, "", "sources") + FileType.JAVADOC -> toQualifier(dep, "", "javadoc") + FileType.OTHER -> id + } + val resolved = resolver.resolveToArtifact(fullId) + if (resolved != null) { + return resolved.file + } else { + throw KobaltException("Couldn't resolve $id") + } + } + + /** + * @return the content of the pom.xml for the given project. + */ + fun generatePom(project: Project) = pomGeneratorFactory.create(project).generate() + + /** All the projects that are being built during this run */ + val allProjects = arrayListOf() + + /** For internal use only */ + val internalContext = InternalContext() + + // + // Injected + // + lateinit var pluginInfo: PluginInfo + lateinit var pluginProperties: PluginProperties + lateinit var dependencyManager: DependencyManager + lateinit var executors: KobaltExecutors + lateinit var settings: KobaltSettings + lateinit var incrementalManager: IncrementalManager + lateinit var resolver: KobaltMavenResolver + lateinit var pomGeneratorFactory: PomGenerator.IFactory + lateinit var logger: ILogger +} + +class InternalContext { + /** + * When an incremental task decides it's up to date, it sets this boolean to true so that subsequent + * tasks in that project can be skipped as well. This is an internal field that should only be set by Kobalt. + */ + private val incrementalSuccesses = hashSetOf() + fun previousTaskWasIncrementalSuccess(projectName: String) = incrementalSuccesses.contains(projectName) ?: false + fun setIncrementalSuccess(projectName: String) = incrementalSuccesses.add(projectName) + + /** + * Keep track of whether the build file was modified. If this boolean is true, incremental compilation + * will be disabled. + */ + var buildFileOutOfDate: Boolean = false + + /** + * The absolute directory of the current project. + */ + var absoluteDir: File? = null + + /** + * If true, will force a recompile of the files even if using the incremental compile + */ + var forceRecompile: Boolean = false + + var noIncrementalKotlin: Boolean = false + +} \ No newline at end of file diff --git a/src/main/kotlin/com/beust/kobalt/api/PluginProperties.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/PluginProperties.kt similarity index 90% rename from src/main/kotlin/com/beust/kobalt/api/PluginProperties.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/PluginProperties.kt index 797a9b35..3838b215 100644 --- a/src/main/kotlin/com/beust/kobalt/api/PluginProperties.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/PluginProperties.kt @@ -5,7 +5,7 @@ import java.util.* /** * Plugins can publish to and read from this object in order to exchange information. Keys stored in - * these maps should be annotated with @ExportedProperty. + * these maps should be annotated with @ExportedPluginProperty. */ class PluginProperties @Inject constructor() { private val pluginProperties = hashMapOf>() diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/PluginTask.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/PluginTask.kt new file mode 100644 index 00000000..b416f96b --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/PluginTask.kt @@ -0,0 +1,20 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.internal.TaskResult2 +import java.util.concurrent.Callable + +interface ITask : Callable> { + val plugin: IPlugin + val project: Project + val name: String + val doc: String + val group: String +} + +abstract class PluginTask : ITask { + override val name: String = "" + override open val doc: String = "" + override open val group: String = "other" + + override fun toString() = project.name + ":" + name +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ProductFlavorConfig.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ProductFlavorConfig.kt new file mode 100644 index 00000000..86c69301 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ProductFlavorConfig.kt @@ -0,0 +1,9 @@ +package com.beust.kobalt.api + +class ProductFlavorConfig(val name: String) : IBuildConfig, + IDependencyHolder by DependencyHolder() { + var applicationId: String? = null + override var buildConfig : BuildConfig? = BuildConfig() +} + + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Project.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Project.kt new file mode 100644 index 00000000..e54e30ec --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Project.kt @@ -0,0 +1,314 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.TestConfig +import com.beust.kobalt.api.annotation.Directive +import com.beust.kobalt.maven.DependencyManager +import com.beust.kobalt.maven.aether.AetherDependency +import com.beust.kobalt.maven.aether.KobaltMavenResolver +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.kobaltLog +import org.apache.maven.model.Model +import java.io.File +import java.util.* +import java.util.concurrent.Future +import java.util.concurrent.FutureTask +import java.util.regex.Pattern + +open class Project( + @Directive open var name: String = "", + @Directive open var version: String? = null, + @Directive open var directory: String = ".", + @Directive open var buildDirectory: String = KFiles.KOBALT_BUILD_DIR, + @Directive open var group: String? = null, + @Directive open var artifactId: String? = null, + @Directive open var packaging: String? = null, + @Directive open var description : String = "", + @Directive open var url: String? = null, + @Directive open var pom: Model? = null, + @Directive open var dependsOn: ArrayList = arrayListOf(), + @Directive open var testsDependOn: ArrayList = arrayListOf(), + @Directive open var packageName: String? = group) + : IBuildConfig, IDependencyHolder by DependencyHolder() { + + init { + this.project = this + } + + fun allProjectDependedOn() = project.dependsOn + project.testsDependOn + + class ProjectExtra(project: Project) { + var isDirty = false + + /** + * @return true if any of the projects we depend on is dirty. + */ + fun dependsOnDirtyProjects(project: Project) = project.allProjectDependedOn().any { it.projectExtra.isDirty } + } + + /** + * This field caches a bunch of things we don't want to recalculate all the time, such as the list of suffixes + * found in this project. + */ + val projectExtra = ProjectExtra(this) + + val testConfigs = arrayListOf() + + // If one is specified by default, we only generateAndSave a BuildConfig, find a way to fix that + override var buildConfig : BuildConfig? = null // BuildConfig() + + val projectProperties = ProjectProperties() + + override fun equals(other: Any?) = name == (other as Project).name + override fun hashCode() = name.hashCode() + + companion object { + val DEFAULT_SOURCE_DIRECTORIES = setOf("src/main/java", "src/main/kotlin", "src/main/resources") + val DEFAULT_SOURCE_DIRECTORIES_TEST = setOf("src/test/java", "src/test/kotlin", "src/test/resources") + } + + // + // Directories + // + + @Directive + fun sourceDirectories(init: Sources.() -> Unit) : Sources { + return Sources(this, sourceDirectories).apply { init() } + } + + var sourceDirectories = hashSetOf().apply { addAll(DEFAULT_SOURCE_DIRECTORIES)} + + @Directive + fun sourceDirectoriesTest(init: Sources.() -> Unit) : Sources { + return Sources(this, sourceDirectoriesTest).apply { init() } + } + + var sourceDirectoriesTest = hashSetOf().apply { addAll(DEFAULT_SOURCE_DIRECTORIES_TEST)} + + // + // Dependencies + // + + @Directive + fun dependenciesTest(init: Dependencies.() -> Unit) : Dependencies { + dependencies = Dependencies(this, testDependencies, arrayListOf(), + testProvidedDependencies, compileOnlyDependencies, compileRuntimeDependencies, + excludedDependencies, nativeDependencies) + dependencies!!.init() + return dependencies!! + } + + val testDependencies : ArrayList = arrayListOf() + val testProvidedDependencies : ArrayList = arrayListOf() + + fun testsDependOn(vararg projects: Project) = testsDependOn.addAll(projects) + fun dependsOn(vararg projects: Project) = dependsOn.addAll(projects) + + /** Used to disambiguate various name properties */ + @Directive + val projectName: String get() = name + + val productFlavors = hashMapOf() + + fun addProductFlavor(name: String, pf: ProductFlavorConfig) { + productFlavors.put(name, pf) + } + + var defaultConfig : BuildConfig? = null + + val buildTypes = hashMapOf() + + fun addBuildType(name: String, bt: BuildTypeConfig) { + buildTypes.put(name, bt) + } + + fun classesDir(context: KobaltContext): String { + val initial = KFiles.joinDir(buildDirectory, "classes") + val result = context.pluginInfo.buildDirectoryInterceptors.fold(initial, { dir, intercept -> + intercept.intercept(this, context, dir) + }) + return result + } + + class Dep(val file: File, val id: String) + + /** + * @return a list of the transitive dependencies (absolute paths to jar files) for the given dependencies. + * Can be used for example as `collect(compileDependencies)`. + */ + @Directive + fun collect(dependencies: List) : List { + return (Kobalt.context?.dependencyManager?.transitiveClosure(dependencies) ?: emptyList()) + .map { Dep(it.jarFile.get(), it.id) } + } + + override fun toString() = "[Project $name]" +} + +class Sources(val project: Project, val sources: HashSet) { + @Directive + fun path(vararg paths: String) { + sources.addAll(paths) + } +} + +class Dependencies(val project: Project, + val dependencies: ArrayList, + val optionalDependencies: ArrayList, + val providedDependencies: ArrayList, + val compileOnlyDependencies: ArrayList, + val runtimeDependencies: ArrayList, + val excludedDependencies: ArrayList, + val nativeDependencies: ArrayList) { + + /** + * Add the dependencies to the given ArrayList and return a list of future jar files corresponding to + * these dependencies. Futures are necessary here since this code is invoked from the build file and + * we might not have set up the extra IRepositoryContributors just yet. By the time these + * future tasks receive a get(), the repos will be correct. + */ + private fun addToDependencies(project: Project, dependencies: ArrayList, + dep: Array, optional: Boolean = false, excludeConfig: ExcludeConfig? = null): List> + = with(dep.map { + val resolved = + if (KobaltMavenResolver.isRangeVersion(it)) { + // Range id + val node = Kobalt.INJECTOR.getInstance(KobaltMavenResolver::class.java).resolveToArtifact(it) + val result = KobaltMavenResolver.artifactToId(node) + kobaltLog(2, "Resolved range id $it to $result") + result + } else { + it + } + DependencyManager.create(resolved, optional, project.directory) + }) { + dependencies.addAll(this) + if (excludeConfig != null) { + this.forEach { it.excluded.add(excludeConfig) } + } + + this.map { FutureTask { it.jarFile.get() } } + } + + @Directive + fun compile(vararg dep: String) = addToDependencies(project, dependencies, dep) + + class ExcludeConfig { + val ids = arrayListOf() + + @Directive + fun exclude(vararg passedIds: String) = ids.addAll(passedIds) + + class ArtifactConfig( + var groupId: String? = null, + var artifactId: String? = null, + var version: String? = null + ) + + val artifacts = arrayListOf() + + @Directive + fun exclude(groupId: String? = null, artifactId: String? = null, version: String? = null) + = artifacts.add(ArtifactConfig(groupId, artifactId, version)) + + fun match(pattern: String?, id: String) : Boolean { + return pattern == null || Pattern.compile(pattern).matcher(id).matches() + } + + /** + * @return true if the dependency is excluded with any of the exclude() directives. The matches + * are performed by a regular expression match against the dependency. + */ + fun isExcluded(dep: IClasspathDependency) : Boolean { + // Straight id match + var result = ids.any { match(it, dep.id) } + + // Match on any combination of (groupId, artifactId, version) + if (! result && dep.isMaven) { + val mavenDep = dep as AetherDependency + val artifact = mavenDep.artifact + result = artifacts.any { + val match1 = it.groupId == null || match(it.groupId, artifact.groupId) + val match2 = it.artifactId == null || match(it.artifactId, artifact.artifactId) + val match3 = it.version == null || match(it.version, artifact.version) + match1 && match2 && match3 + } + } + + return result + } + } + + @Directive + fun compile(dep: String, init: ExcludeConfig.() -> Unit) { + val excludeConfig = ExcludeConfig().apply { + init() + } + addToDependencies(project, dependencies, arrayOf(dep), excludeConfig = excludeConfig) + } + + @Directive + fun compileOnly(vararg dep: String) = addToDependencies(project, compileOnlyDependencies, dep) + + @Directive + fun compileOptional(vararg dep: String) { + addToDependencies(project, optionalDependencies, dep, optional = true) + addToDependencies(project, dependencies, dep, optional = true) + } + + @Directive + fun provided(vararg dep: String) { + addToDependencies(project, providedDependencies, dep) + } + + @Directive + fun runtime(vararg dep: String) = addToDependencies(project, runtimeDependencies, dep) + + @Directive + fun exclude(vararg dep: String) = addToDependencies(project, excludedDependencies, dep) + + @Directive + fun native(vararg dep: String) = addToDependencies(project, nativeDependencies, dep) +} + +class BuildConfig { + class Field(val name: String, val type: String, val value: Any) { + override fun hashCode() = name.hashCode() + override fun equals(other: Any?) = (other as Field).name == name + } + + val fields = arrayListOf() + + fun field(type: String, name: String, value: Any) { + fields.add(Field(name, type, value)) + } +} + +interface IBuildConfig { + var buildConfig: BuildConfig? + + fun buildConfig(init: BuildConfig.() -> Unit) { + buildConfig = BuildConfig().apply { + init() + } + } +} + +fun Project.defaultConfig(init: BuildConfig.() -> Unit) = let { project -> + BuildConfig().apply { + init() + project.defaultConfig = this + } +} + +@Directive +fun Project.buildType(name: String, init: BuildTypeConfig.() -> Unit) = BuildTypeConfig(name).apply { + init() + addBuildType(name, this) +} + + +@Directive +fun Project.productFlavor(name: String, init: ProductFlavorConfig.() -> Unit) = ProductFlavorConfig(name).apply { + init() + addProductFlavor(name, this) +} diff --git a/src/main/kotlin/com/beust/kobalt/api/ProjectProperties.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ProjectProperties.kt similarity index 100% rename from src/main/kotlin/com/beust/kobalt/api/ProjectProperties.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ProjectProperties.kt diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Task.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Task.kt new file mode 100644 index 00000000..c45a820d --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Task.kt @@ -0,0 +1,7 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.misc.toString + +data class Task(val pluginName: String, val taskName: String) { + override fun toString() = toString("Task", pluginName, taskName) +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/TaskContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/TaskContributor.kt new file mode 100644 index 00000000..8c68be94 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/TaskContributor.kt @@ -0,0 +1,86 @@ +package com.beust.kobalt.api + +import com.beust.kobalt.IncrementalTaskInfo +import com.beust.kobalt.TaskResult +import com.beust.kobalt.Variant +import com.beust.kobalt.api.annotation.AnnotationDefault +import com.beust.kobalt.internal.IncrementalManager +import com.google.inject.Inject + +/** + * Plug-ins that are ITaskContributor can use this class to manage their collection of tasks and + * implement the interface by delegating to an instance of this class (if injection permits). + */ +class TaskContributor @Inject constructor(val incrementalManagerFactory: IncrementalManager.IFactory) + : ITaskContributor { + val dynamicTasks = arrayListOf() + + /** + * Register dynamic tasks corresponding to the variants found in the project,e.g. assembleDevDebug, + * assembleDevRelease, etc... + * + * TODO: this should be done automatically so that users don't have to invoke it themselves. + * Certain tasks could have a boolean flag "hasVariants" and any task that depends on it automatically + * depends on variants of that task. + */ + fun addVariantTasks(plugin: IPlugin, project: Project, context: KobaltContext, taskName: String, + group: String = AnnotationDefault.GROUP, + dependsOn: List = emptyList(), + reverseDependsOn : List = emptyList(), + runBefore : List = emptyList(), + runAfter : List = emptyList(), + runTask: (Project) -> TaskResult) { + Variant.allVariants(project).forEach { variant -> + val variantTaskName = variant.toTask(taskName) + dynamicTasks.add(DynamicTask(plugin, variantTaskName, variantTaskName, group, project, + dependsOn = dependsOn.map { variant.toTask(it) }, + reverseDependsOn = reverseDependsOn.map { variant.toTask(it) }, + runBefore = runBefore.map { variant.toTask(it) }, + runAfter = runAfter.map { variant.toTask(it) }, + closure = { p: Project -> + context.variant = variant + runTask(project) + })) + } + } + + fun addTask(plugin: IPlugin, project: Project, taskName: String, description: String, + group: String = AnnotationDefault.GROUP, + dependsOn: List = emptyList(), + reverseDependsOn : List = emptyList(), + runBefore : List = emptyList(), + runAfter : List = emptyList(), + alwaysRunAfter: List = emptyList(), + runTask: (Project) -> TaskResult) { + dynamicTasks.add(DynamicTask(plugin, taskName, description, group, project, + dependsOn = dependsOn, + reverseDependsOn = reverseDependsOn, + runBefore = runBefore, + runAfter = runAfter, + alwaysRunAfter = alwaysRunAfter, + closure = { p: Project -> + runTask(project) + })) + } + + fun addIncrementalVariantTasks(plugin: IPlugin, project: Project, context: KobaltContext, taskName: String, + group: String = AnnotationDefault.GROUP, + dependsOn: List = emptyList(), + reverseDependsOn : List = emptyList(), + runBefore : List = emptyList(), + runAfter : List = emptyList(), + runTask: (Project) -> IncrementalTaskInfo) { + Variant.allVariants(project).forEach { variant -> + val variantTaskName = variant.toTask(taskName) + context.variant = variant + dynamicTasks.add(DynamicTask(plugin, variantTaskName, variantTaskName, group, project, + dependsOn = dependsOn.map { variant.toTask(it) }, + reverseDependsOn = reverseDependsOn.map { variant.toTask(it) }, + runBefore = runBefore.map { variant.toTask(it) }, + runAfter = runAfter.map { variant.toTask(it) }, + closure = incrementalManagerFactory.create().toIncrementalTaskClosure(taskName, runTask, variant))) + } + } + + override fun tasksFor(project: Project, context: KobaltContext) : List = dynamicTasks +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/annotation/Annotations.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/annotation/Annotations.kt new file mode 100644 index 00000000..f4269d47 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/annotation/Annotations.kt @@ -0,0 +1,90 @@ +package com.beust.kobalt.api.annotation + +/** + * Plugins that export directives should annotated those with this annotation so they can be documented and also + * receive special treatment for auto completion in the plug-in. + */ +annotation class Directive + +object AnnotationDefault { + const val GROUP = "other" +} + +@Retention(AnnotationRetention.RUNTIME) +annotation class Task( + /* This task's name */ + val name: String, + + /* The documentation for this task */ + val description: String = "", + + /** Used to show the task in the correct group in the IDE */ + val group: String = AnnotationDefault.GROUP, + + /** Dependency: tasks this task depends on */ + val dependsOn: Array = arrayOf(), + + /** Dependency: tasks this task will be made dependend upon */ + val reverseDependsOn: Array = arrayOf(), + + /** Ordering: tasks that need to be run before this one */ + val runBefore: Array = arrayOf(), + + /** Ordering: tasks this task runs after */ + val runAfter: Array = arrayOf(), + + /** Wrapper tasks */ + val alwaysRunAfter: Array = arrayOf() +) + +@Retention(AnnotationRetention.RUNTIME) +annotation class IncrementalTask( + /* This task's name */ + val name: String, + + /* The documentation for this task */ + val description: String = "", + + /** Used to show the task in the correct group in the IDE */ + val group: String = AnnotationDefault.GROUP, + + /** Dependency: tasks this task depends on */ + val dependsOn: Array = arrayOf(), + + /** Dependency: tasks this task will be made dependend upon */ + val reverseDependsOn: Array = arrayOf(), + + /** Tasks that this task depends on */ + val runBefore: Array = arrayOf(), + + /** Ordering: tasks this task runs after */ + val runAfter: Array = arrayOf(), + + /** Wrapper tasks */ + val alwaysRunAfter: Array = arrayOf() +) + +/** + * Plugins that export properties should annotate those with this annotation so they can be documented. + */ +@Retention(AnnotationRetention.RUNTIME) +annotation class ExportedPluginProperty( + /** Documentation for this property */ + val doc: String = "", + + /** The type of this property */ + val type: String = "" +) + +/** + * Plugins that export properties on the Project instance should annotate those with this annotation so + * they can be documented. + */ +@Retention(AnnotationRetention.RUNTIME) +annotation class ExportedProjectProperty( + /** Documentation for this property */ + val doc: String = "", + + /** The type of this property */ + val type: String = "" +) diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/Archives.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/Archives.kt new file mode 100644 index 00000000..5334e09f --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/Archives.kt @@ -0,0 +1,83 @@ +package com.beust.kobalt.archive + +import com.beust.kobalt.* +import com.beust.kobalt.api.KobaltContext +import com.beust.kobalt.api.Project +import com.beust.kobalt.api.annotation.ExportedProjectProperty +import com.beust.kobalt.misc.JarUtils +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.kobaltLog +import java.io.File +import java.util.* + +class Archives { + companion object { + @ExportedProjectProperty(doc = "The name of the jar file", type = "String") + const val JAR_NAME = "jarName" + @ExportedProjectProperty(doc = "The name of the a jar file with a main() method", type = "String") + const val JAR_NAME_WITH_MAIN_CLASS = "jarNameWithMainClass" + + fun defaultArchiveName(project: Project) = project.name + + if (project.version.isNullOrBlank()) "" else "-${project.version}" + + fun generateArchive(project: Project, + context: KobaltContext, + archiveName: String?, + suffix: String, + includedFiles: List, + expandJarFiles : Boolean = false, + manifest: java.util.jar.Manifest? = null) : File { + val fullArchiveName = context.variant.archiveName(project, archiveName, suffix) + val archiveDir = File(KFiles.libsDir(project)) + val result = File(archiveDir.path, fullArchiveName) + context.logger.log(project.name, 3, "Creating $result") + if (! Features.USE_TIMESTAMPS || isOutdated(project.directory, includedFiles, result)) { + try { + MetaArchive(result, manifest).use { metaArchive -> + JarUtils.addFiles(project.directory, includedFiles, metaArchive, expandJarFiles) + context.logger.log(project.name, 2, "Added ${includedFiles.size} files to $result") + context.logger.log(project.name, 1, " Created $result") + } + } catch (e: Throwable) { + // make sure that incomplete archive is deleted + // otherwise incremental build does not work on next run + result.delete() + throw e + } + + } else { + context.logger.log(project.name, 3, " $result is up to date") + } + + return result + } + + private fun isOutdated(directory: String, includedFiles: List, output: File): Boolean { + if (! output.exists()) return true + + val lastModified = output.lastModified() + includedFiles.forEach { root -> + val allFiles = root.allFromFiles(directory) + allFiles.forEach { relFile -> + val file = if (relFile.isAbsolute) + relFile // e.g. jar file or classes folder (of another project) when building a fat jar + else + File(KFiles.joinDir(directory, root.from, relFile.path)) + if (file.isFile) { + if (file.lastModified() > lastModified) { + kobaltLog(3, " TS - Outdated $file and $output " + + Date(file.lastModified()) + " " + Date(output.lastModified())) + return true + } + } else if (file.isDirectory) { + // e.g. classes folder (of another project) when building a fat jar + val includedFile = IncludedFile(From(""), To(""), listOf(IFileSpec.GlobSpec("**"))) + if (isOutdated(file.absolutePath, listOf(includedFile), output)) + return true + } + } + } + return false + } + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/AttributeHolder.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/AttributeHolder.kt new file mode 100644 index 00000000..4abffb21 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/AttributeHolder.kt @@ -0,0 +1,6 @@ +package com.beust.kobalt.archive + +interface AttributeHolder { + fun addAttribute(k: String, v: String) +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/Jar.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/Jar.kt new file mode 100644 index 00000000..d5086cbd --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/Jar.kt @@ -0,0 +1,28 @@ +package com.beust.kobalt.archive + +import com.beust.kobalt.api.Project +import com.beust.kobalt.api.annotation.Directive + +/** + * A jar is exactly like a zip with the addition of a manifest and an optional fatJar boolean. + */ +open class Jar(override val project: Project, + override var name : String = Archives.defaultArchiveName(project) + ".jar", + override var fatJar: Boolean = false) : Zip(project, name, fatJar), AttributeHolder { + @Directive + fun manifest(init: Manifest.(p: Manifest) -> Unit) : Manifest { + val m = Manifest(this) + m.init(m) + return m + } + + // Need to specify the version or attributes will just be dropped + @Directive + override val attributes = arrayListOf(Pair("Manifest-Version", "1.0")) + + override fun addAttribute(k: String, v: String) { + attributes.add(Pair(k, v)) + } +} + + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/Manifest.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/Manifest.kt new file mode 100644 index 00000000..1771dbe8 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/Manifest.kt @@ -0,0 +1,11 @@ +package com.beust.kobalt.archive + +import com.beust.kobalt.api.annotation.Directive + +class Manifest(val jar: AttributeHolder) { + @Directive + fun attributes(k: String, v: String) { + jar.addAttribute(k, v) + } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/MetaArchive.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/MetaArchive.kt new file mode 100644 index 00000000..c217c83e --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/MetaArchive.kt @@ -0,0 +1,125 @@ +package com.beust.kobalt.archive + +import com.beust.kobalt.Glob +import com.beust.kobalt.misc.KFiles +import org.apache.commons.compress.archivers.ArchiveEntry +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream +import java.io.Closeable +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.jar.Manifest +import org.apache.commons.compress.archivers.zip.ZipFile as ApacheZipFile + +/** + * Abstraction of a zip/jar/war archive that automatically manages the addition of expanded jar files. + * Uses ZipArchiveOutputStream for fast inclusion of expanded jar files. + */ +class MetaArchive(outputFile: File, val manifest: Manifest?) : Closeable { + companion object { + const val MANIFEST_MF = "META-INF/MANIFEST.MF" + } + + private val zos= ZipArchiveOutputStream(outputFile).apply { + encoding = "UTF-8" + } + + init { + // If no manifest was passed, create an empty one so it's the first one in the archive + val m = manifest ?: Manifest() + val manifestFile = File.createTempFile("kobalt", "tmpManifest") + addEntry(ZipArchiveEntry("META-INF/"), null) + if (manifest != null) { + FileOutputStream(manifestFile).use { fos -> + m.write(fos) + } + } + val entry = zos.createArchiveEntry(manifestFile, MetaArchive.MANIFEST_MF) + addEntry(entry, FileInputStream(manifestFile)) + } + + + fun addFile(f: File, entryFile: File, path: String?) { + maybeCreateParentDirectories(f) + addFile2(f, entryFile, path) + } + + private fun addFile2(f: File, entryFile: File, path: String?) { + val file = f.normalize() + FileInputStream(file).use { inputStream -> + val actualPath = KFiles.fixSlashes(if (path != null) path + entryFile.path else entryFile.path) + ZipArchiveEntry(actualPath).let { entry -> + maybeCreateParentDirectories(File(actualPath)) + maybeAddEntry(entry) { + addEntry(entry, inputStream) + } + } + } + } + + private val createdDirs = hashSetOf() + + /** + * For an entry a/b/c/File, an entry needs to be created for each individual directory: + * a/ + * a/b/ + * a/b/c + * a/b/c/File + */ + private fun maybeCreateParentDirectories(file: File) { + val toCreate = arrayListOf() + var current = file.parentFile + while (current != null && current.path != ".") { + if (!createdDirs.contains(current.path)) { + toCreate.add(0, KFiles.fixSlashes(current) + "/") + createdDirs.add(current.path) + } + current = current.parentFile + } + toCreate.forEach { dir -> + addEntry(ZipArchiveEntry(dir), null) + } + } + + fun addArchive(jarFile: File) { + ApacheZipFile(jarFile).use { jar -> + val jarEntries = jar.entries + for (entry in jarEntries) { + maybeAddEntry(entry) { + zos.addRawArchiveEntry(entry, jar.getRawInputStream(entry)) + } + } + } + } + + + + private fun okToAdd(name: String) : Boolean { + val result = !KFiles.isExcluded(name, + Glob("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", MANIFEST_MF)) +// if (name.startsWith("META-INF")) println((if (result) "ADDING" else "NOT ADDING") + " $name") + return result + } + + override fun close() = zos.close() + + private fun addEntry(entry: ArchiveEntry, inputStream: FileInputStream?) { + zos.putArchiveEntry(entry) + inputStream?.use { ins -> + ins.copyTo(zos, 50 * 1024) + } + zos.closeArchiveEntry() + } + + private val seen = hashSetOf() + + private fun maybeAddEntry(entry: ArchiveEntry, action:() -> Unit) { + entry.name.let { name -> + if (!seen.contains(name) && okToAdd(name)) { + action() + } + seen.add(name) + } + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/War.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/War.kt new file mode 100644 index 00000000..978f21bf --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/War.kt @@ -0,0 +1,13 @@ +package com.beust.kobalt.archive + +import com.beust.kobalt.api.Project +import com.beust.kobalt.glob + +class War(override val project: Project, override var name: String = Archives.defaultArchiveName(project) + ".war") + : Jar(project, name), AttributeHolder { + init { + include(from("src/main/webapp"), to(""), glob("**")) + include(from("kobaltBuild/classes"), to("WEB-INF/classes"), glob("**")) + } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/Zip.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/Zip.kt new file mode 100644 index 00000000..41957218 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/archive/Zip.kt @@ -0,0 +1,27 @@ +package com.beust.kobalt.archive + +import com.beust.kobalt.* +import com.beust.kobalt.api.Project +import com.beust.kobalt.api.annotation.Directive + +open class Zip(open val project: Project, open var name: String = Archives.defaultArchiveName(project) + ".zip", + open var fatJar: Boolean = false): AttributeHolder, IncludeFromTo() { + val excludes = arrayListOf() + + @Directive + fun exclude(vararg files: String) { + files.forEach { excludes.add(Glob(it)) } + } + + @Directive + fun exclude(vararg specs: Glob) { + specs.forEach { excludes.add(it) } + } + + @Directive + open val attributes = arrayListOf(Pair("Manifest-Version", "1.0")) + + override fun addAttribute(k: String, v: String) { + attributes.add(Pair(k, v)) + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/ActorUtils.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/ActorUtils.kt new file mode 100644 index 00000000..e9b315a5 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/ActorUtils.kt @@ -0,0 +1,22 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.api.IProjectAffinity +import com.beust.kobalt.api.KobaltContext +import com.beust.kobalt.api.Project + +class ActorUtils { + companion object { + /** + * Return the plug-in actor with the highest affinity. + */ + fun selectAffinityActor(project: Project, context: KobaltContext, actors: List) + = actors.maxBy { it.affinity(project, context) } + + /** + * Return all the plug-in actors with a non zero affinity sorted from the highest to the lowest. + */ + fun selectAffinityActors(project: Project, context: KobaltContext, actors: List) + = actors.filter { it.affinity(project, context) > 0 } + .sortedByDescending { it.affinity(project, context) } + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/BaseJvmPlugin.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/BaseJvmPlugin.kt new file mode 100644 index 00000000..e89fc9aa --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/BaseJvmPlugin.kt @@ -0,0 +1,37 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.api.* +import com.beust.kobalt.misc.KFiles + +/** + * Base class for JVM language plug-ins. + */ +abstract class BaseJvmPlugin(open val configActor: ConfigActor) : + BasePlugin(), + IConfigActor by configActor, + ICompilerFlagContributor { + companion object { + // Run before other flag contributors + val FLAG_CONTRIBUTOR_PRIORITY = FlagContributor.DEFAULT_FLAG_PRIORITY - 10 + } + + protected fun maybeCompilerArgs(sourceSuffixes: List, suffixesBeingCompiled: List, + args: List) + = if (sourceSuffixes.any { suffixesBeingCompiled.contains(it) }) args else emptyList() + + override val flagPriority = FLAG_CONTRIBUTOR_PRIORITY + + override fun accept(project: Project) = sourceFileCount(project) > 0 + + // IBuildConfigContributor + protected fun sourceFileCount(project: Project) + = KFiles.findSourceFiles(project.directory, project.sourceDirectories, sourceSuffixes()).size + + KFiles.findSourceFiles(project.directory, project.sourceDirectoriesTest, sourceSuffixes()).size + + fun affinity(project: Project) = sourceFileCount(project) + + // IDocContributor + open fun affinity(project: Project, context: KobaltContext) = sourceFileCount(project) + + abstract fun sourceSuffixes() : List +} \ No newline at end of file diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/BaseProjectRunner.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/BaseProjectRunner.kt new file mode 100644 index 00000000..963255bd --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/BaseProjectRunner.kt @@ -0,0 +1,186 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.TestResult +import com.beust.kobalt.api.IBuildListener +import com.beust.kobalt.api.KobaltContext +import com.beust.kobalt.api.Project +import com.beust.kobalt.api.ProjectBuildStatus +import com.beust.kobalt.misc.kobaltLog +import com.google.common.annotations.VisibleForTesting +import com.google.common.collect.ArrayListMultimap +import com.google.common.collect.Multimap +import java.util.* + +abstract class BaseProjectRunner { + + abstract fun runProjects(taskInfos: List, projects: List) + : TaskManager.RunTargetResult + + companion object { + val TAG = "graph" + + fun runBuildListenersForProject(project: Project, context: KobaltContext, start: Boolean, + status: ProjectBuildStatus = ProjectBuildStatus.SUCCESS) { + context.pluginInfo.buildListeners.forEach { + if (start) it.projectStart(project, context) else it.projectEnd(project, context, status) + } + } + + fun runBuildListenersForTask(project: Project, context: KobaltContext, taskName: String, start: Boolean, + success: Boolean = false, testResult: TestResult? = null) { + context.pluginInfo.buildListeners.forEach { + if (start) { + it.taskStart(project, context, taskName) + } else { + val info = IBuildListener.TaskEndInfo(success, testResult?.shortMessage, testResult?.longMessage) + it.taskEnd(project, context, taskName, info) + } + } + } + + /** + * Create a graph representing the tasks and their dependencies. That graph will then be run + * in topological order. + * + * @taskNames is the list of tasks requested by the user. @nodeMap maps these tasks to the nodes + * we'll be adding to the graph while @toName extracts the name of a node. + */ + @VisibleForTesting + fun createTaskGraph(projectName: String, passedTasks: List, + nodeMap: Multimap, + dependsOn: Multimap, + reverseDependsOn: Multimap, + runBefore: Multimap, + runAfter: Multimap, + alwaysRunAfter: Multimap, + toName: (T) -> String, + accept: (T) -> Boolean): + DynamicGraph { + + /** + * Add an edge from @param from to all its tasks. + */ + fun addEdge(result: DynamicGraph, from: String, to: String, newToProcess: ArrayList, text: String) { + val froms = nodeMap[from] + froms.forEach { f: T -> + nodeMap[to].forEach { t: T -> + kobaltLog(TAG, " Adding edge ($text) $f -> $t") + result.addEdge(f, t) + newToProcess.add(t) + } + } + } + + val result = DynamicGraph() + val newToProcess = arrayListOf() + val seen = hashSetOf() + + // + // Reverse the always map so that tasks can be looked up. + // + val always = ArrayListMultimap.create().apply { + alwaysRunAfter.keySet().forEach { k -> + alwaysRunAfter[k].forEach { v -> + put(v, k) + } + } + } + + // + // Keep only the tasks we need to run. + // + val taskInfos = passedTasks.filter { + it.matches(projectName) + } + + // The nodes we need to process, initialized with the set of tasks requested by the user. + // As we run the graph and discover dependencies, new nodes get added to @param[newToProcess]. At + // the end of the loop, @param[toProcess] is cleared and all the new nodes get added to it. Then we loop. + val toProcess = ArrayList(taskInfos) + + while (toProcess.size > 0) { + + /** + * Whenever a task is added to the graph, we also add its alwaysRunAfter tasks. + */ + fun processAlways(always: Multimap, node: T) { + kobaltLog(TAG, " Processing always for $node") + always[toName(node)]?.let { to: Collection -> + to.forEach { t -> + nodeMap[t].forEach { from -> + kobaltLog(TAG, " Adding always edge $from -> $node") + result.addEdge(from, node) + } + } + kobaltLog(TAG, " ... done processing always for $node") + } + } + + kobaltLog(TAG, " Current batch to process: $toProcess") + + // + // Move dependsOn + reverseDependsOn in one multimap called allDepends + // + val allDependsOn = ArrayListMultimap.create() + dependsOn.keySet().forEach { key -> + dependsOn[key].forEach { value -> + allDependsOn.put(key, value) + } + } + reverseDependsOn.keySet().forEach { key -> + reverseDependsOn[key].forEach { value -> + allDependsOn.put(value, key) + } + } + + // + // Process each node one by one + // + toProcess.forEach { taskInfo -> + val taskName = taskInfo.taskName + kobaltLog(TAG, " ***** Current node: $taskName") + nodeMap[taskName].forEach { + result.addNode(it) + processAlways(always, it) + } + + // + // dependsOn and reverseDependsOn are considered for all tasks, explicit and implicit + // + allDependsOn[taskName].forEach { to -> + addEdge(result, taskName, to, newToProcess, "dependsOn") + } + + seen.add(taskName) + } + + newToProcess.forEach { processAlways(always, it) } + + toProcess.clear() + toProcess.addAll( + newToProcess + .filter { !seen.contains(toName(it)) } + .map { TaskManager.TaskInfo(toName(it)) }) + newToProcess.clear() + } + + // + // Now that we have all the tasks tnat need to run, process runBefore/runAfter, which + // are not allowed to add new tasks. Therefore, we only add edges to the graph if both + // the from and the to are already present. + // + val finalTaskNames = result.nodes.map { TaskManager.TaskInfo(it.toString()).taskName } + finalTaskNames.forEach { taskName -> + runBefore[taskName].filter { finalTaskNames.contains(it) }.forEach { from -> + addEdge(result, from, taskName, newToProcess, "runBefore") + } + runAfter[taskName].filter { finalTaskNames.contains(it) }.forEach { to -> + addEdge(result, to, taskName, newToProcess, "runAfter") + } + } + + + return result + } + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/BuildListeners.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/BuildListeners.kt new file mode 100644 index 00000000..58d8eed8 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/BuildListeners.kt @@ -0,0 +1,128 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.Args +import com.beust.kobalt.AsciiArt +import com.beust.kobalt.api.* +import com.beust.kobalt.misc.kobaltLog +import java.util.concurrent.ConcurrentHashMap + +/** + * Record timings and statuses for tasks and projects and display them at the end of the build. + */ +class BuildListeners : IBuildListener, IBuildReportContributor { + class ProfilerInfo(val taskName: String, val durationMillis: Long) + class ProjectInfo(val projectName: String, var durationMillis: Long = 0, + var shortMessage: String? = null, var longMessage: String? = null) + + private val startTimes = ConcurrentHashMap() + private val timings = arrayListOf() + private val projectInfos = hashMapOf() + private var hasFailures = false + private val args: Args get() = Kobalt.INJECTOR.getInstance(Args::class.java) + private var buildStartTime: Long? = null + + // IBuildListener + override fun taskStart(project: Project, context: KobaltContext, taskName: String) { + startTimes.put(taskName, System.currentTimeMillis()) + if (! projectInfos.containsKey(project.name)) { + projectInfos.put(project.name, ProjectInfo(project.name)) + } + } + + // IBuildListener + override fun taskEnd(project: Project, context: KobaltContext, taskName: String, info: IBuildListener.TaskEndInfo) { + val success = info.success + if (! success) hasFailures = true + startTimes[taskName]?.let { + val taskTime = System.currentTimeMillis() - it + timings.add(ProfilerInfo(taskName, taskTime)) + projectInfos[project.name]?.let { + it.durationMillis += taskTime + if (info.shortMessage != null && it.shortMessage == null) it.shortMessage = info.shortMessage + if (info.longMessage != null && it.longMessage == null) it.longMessage = info.longMessage + } + } + } + + private val projectStatuses = arrayListOf>() + + // IBuildListener + override fun projectStart(project: Project, context: KobaltContext) { + if (buildStartTime == null) buildStartTime = System.currentTimeMillis() + } + + // IBuildListener + override fun projectEnd(project: Project, context: KobaltContext, status: ProjectBuildStatus) { + val shortMessage = projectInfos[project.name]?.shortMessage + val statusText = status.toString() + (if (shortMessage != null) " ($shortMessage)" else "") + projectStatuses.add(Pair(project, statusText)) + } + + // IBuildReportContributor + override fun generateReport(context: KobaltContext) { + fun formatMillis(millis: Long, format: String) = String.format(format, millis.toDouble() / 1000) + fun formatMillisRight(millis: Long, length: Int) = formatMillis(millis, "%1\$$length.2f") + fun formatMillisLeft(millis: Long, length: Int) = formatMillis(millis, "%1\$-$length.2f") + + fun millisToSeconds(millis: Long) = (millis.toDouble() / 1000).toInt() + + val profiling = args.profiling + if (profiling) { + kobaltLog(1, "\n" + AsciiArt.horizontalSingleLine + " Timings (in seconds)") + timings.sortedByDescending { it.durationMillis }.forEach { + kobaltLog(1, formatMillisRight(it.durationMillis, 10) + " " + it.taskName) + } + kobaltLog(1, "\n") + + } + + // Calculate the longest short message so we can create a column long enough to contain it + val width = 12 + (projectInfos.values.map { it.shortMessage?.length ?: 0 }.maxBy { it } ?: 0) + + fun col1(s: String) = String.format(" %1\$-30s", s) + fun col2(s: String) = String.format(" %1\$-${width}s", s) + fun col3(s: String) = String.format(" %1\$-8s", s) + + + + // Only print the build report if there is more than one project and at least one of them failed + if (timings.any()) { +// if (timings.size > 1 && hasFailures) { + val line = listOf(col1("Project"), col2("Build status"), col3("Time")) + .joinToString(AsciiArt.verticalBar) + val table = StringBuffer() + table.append(AsciiArt.logBox(listOf(line), AsciiArt.bottomLeft2, AsciiArt.bottomRight2, indent = 10) + "\n") + projectStatuses.forEach { pair -> + val projectName = pair.first.name + val cl = listOf(col1(projectName), col2(pair.second), + col3(formatMillisLeft(projectInfos[projectName]!!.durationMillis, 8))) + .joinToString(AsciiArt.verticalBar) + table.append(" " + AsciiArt.verticalBar + " " + cl + " " + AsciiArt.verticalBar + "\n") + } + table.append(" " + AsciiArt.lowerBox(line.length)) + kobaltLog(1, table.toString()) +// } + } + + val buildTime = + if (buildStartTime != null) + millisToSeconds(System.currentTimeMillis() - buildStartTime!!) + else + 0 + // BUILD SUCCESSFUL / FAILED message + val message = + if (hasFailures) { + String.format("BUILD FAILED", buildTime) + } else if (! args.sequential) { + val sequentialBuildTime = ((projectInfos.values.sumByDouble { it.durationMillis.toDouble() }) / 1000) + .toInt() + String.format("PARALLEL BUILD SUCCESSFUL (%d SECONDS), sequential build would have taken %d seconds", + buildTime, sequentialBuildTime) + } else { + String.format("BUILD SUCCESSFUL (%d SECONDS)", buildTime) + } + kobaltLog(1, message) + + } + +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/CollectionUtils.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/CollectionUtils.kt new file mode 100644 index 00000000..62258d62 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/CollectionUtils.kt @@ -0,0 +1,14 @@ +package com.beust.kobalt.internal + +fun Collection.doWhile(condition: (T) -> Boolean, action: (T) -> Unit) { + var i = 0 + var done = false + while (i < size && ! done) { + elementAt(i).let { element -> + if (! condition(element)) done = true + else action(element) + } + i++ + } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/CompilerUtils.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/CompilerUtils.kt new file mode 100644 index 00000000..758a10e9 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/CompilerUtils.kt @@ -0,0 +1,232 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.TaskResult +import com.beust.kobalt.api.* +import com.beust.kobalt.maven.DependencyManager +import com.beust.kobalt.maven.aether.Scope +import com.beust.kobalt.maven.dependency.FileDependency +import com.beust.kobalt.misc.KFiles +import com.google.inject.Inject +import java.io.File +import java.nio.file.Paths +import java.util.* + +/** + * Central place to compile files, used by plug-ins and non plug-ins. + */ +class CompilerUtils @Inject constructor(val files: KFiles, val dependencyManager: DependencyManager) { + + class CompilerResult(val successResults: List, val failedResult: TaskResult?) + + fun invokeCompiler(project: Project, context: KobaltContext, compiler: ICompilerDescription, + sourceDirectories: List, isTest: Boolean, buildDirectory: File): CompilerResult { + val results = arrayListOf() + var failedResult: TaskResult? = null + val contributedSourceDirs = + if (isTest) { + context.testSourceDirectories(project) + } else { + context.sourceDirectories(project) + } + val sourceFiles = KFiles.findSourceFiles(project.directory, + contributedSourceDirs.map { it.path }, compiler.sourceSuffixes) + if (sourceFiles.isNotEmpty()) { + // TODO: createCompilerActionInfo recalculates the source files, only compute them + // once and pass them + val info = createCompilerActionInfo(project, context, compiler, isTest, + sourceDirectories, sourceSuffixes = compiler.sourceSuffixes, buildDirectory = buildDirectory) + val thisResult = invokeCompiler(project, context, compiler, info) + results.addAll(thisResult.successResults) + if (failedResult == null) { + failedResult = thisResult.failedResult + } + } else { + context.logger.log(project.name, 2, + "${compiler.name} compiler not running on ${project.name} since no source files were found") + } + + return CompilerResult(results, failedResult) + } + + fun invokeCompiler(project: Project, context: KobaltContext, compiler: ICompilerDescription, info: CompilerActionInfo) + : CompilerResult { + val results = arrayListOf() + var failedResult: TaskResult? = null + val thisResult = compiler.compile(project, context, info) + results.add(thisResult) + if (!thisResult.success && failedResult == null) { + failedResult = thisResult + } + return CompilerResult(results, failedResult) + } + + /** + * Create a CompilerActionInfo (all the information that a compiler needs to know) for the given parameters. + * Runs all the contributors and interceptors relevant to that task. + */ + fun createCompilerActionInfo(project: Project, context: KobaltContext, compiler: ICompilerDescription, + isTest: Boolean, sourceDirectories: List, sourceSuffixes: List, buildDirectory: File) + : CompilerActionInfo { + copyResources(project, context, SourceSet.of(isTest)) + + val fullClasspath = dependencyManager.calculateDependencies(project, context, + scopes = if (isTest) { + listOf(Scope.COMPILE, Scope.COMPILEONLY, Scope.TEST) + } else { + listOf(Scope.COMPILE, Scope.COMPILEONLY) + }) + + + File(project.directory, buildDirectory.path).mkdirs() + + // Remove all the excluded dependencies from the classpath + var classpath = fullClasspath + + // The classpath needs to contain $buildDirectory/classes as well so that projects that contain + // multiple languages can use classes compiled by the compiler run before them. + fun containsClassFiles(dir: File) = + KFiles.containsCertainFile(dir) { + it.isFile && it.name.endsWith("class") + } + +// if (buildDirectory.exists()) { + if (containsClassFiles(buildDirectory)) { + classpath += FileDependency(buildDirectory.path) + } + + val initialSourceDirectories = ArrayList(sourceDirectories) + // Source directories from the contributors + val contributedSourceDirs = + if (isTest) { + context.pluginInfo.testSourceDirContributors.flatMap { it.testSourceDirectoriesFor(project, context) } + } else { + context.pluginInfo.sourceDirContributors.flatMap { it.sourceDirectoriesFor(project, context) } + } + + initialSourceDirectories.addAll(contributedSourceDirs) + + // Transform them with the interceptors, if any + val allSourceDirectories = + if (isTest) { + initialSourceDirectories + } else { + context.pluginInfo.sourceDirectoriesInterceptors.fold(initialSourceDirectories.toList(), + { sd, interceptor -> interceptor.intercept(project, context, sd) }) + }.filter { + File(project.directory, it.path).exists() + }.filter { + ! KFiles.isResource(it.path) + }.distinctBy { + Paths.get(it.path) + } + + // Now that we have all the source directories, find all the source files in them. Note that + // depending on the compiler's ability, sourceFiles can actually contain a list of directories + // instead of individual source files. + val projectDirectory = File(project.directory) + val sourceFiles = + if (compiler.canCompileDirectories) { + allSourceDirectories.map { File(projectDirectory, it.path).path } + } else { + files.findRecursively(projectDirectory, allSourceDirectories, + { file -> sourceSuffixes.any { file.endsWith(it) } }) + .map { File(projectDirectory, it).path } + } + + // Special treatment if we are compiling Kotlin files and the project also has a java source + // directory. In this case, also pass that java source directory to the Kotlin compiler as is + // so that it can parse its symbols + // Note: this should actually be queried on the compiler object so that this method, which + // is compiler agnostic, doesn't hardcode Kotlin specific stuff + val extraSourceFiles = arrayListOf() + + fun containsJavaFiles(dir: File) = + KFiles.containsCertainFile(dir) { + it.isFile && it.name.endsWith("java") + } + + if (sourceSuffixes.any { it.contains("kt")}) { + val directories = if (isTest) project.sourceDirectoriesTest else project.sourceDirectories + directories.forEach { + val javaDir = File(KFiles.joinDir(project.directory, it)) + if (javaDir.exists() && containsJavaFiles(javaDir) && ! KFiles.isResource(javaDir.path)) { + extraSourceFiles.add(javaDir.path) + // Add all the source directories contributed as potential Java directories too + // (except our own) + context.pluginInfo.sourceDirContributors.forEach { + val sd = it.sourceDirectoriesFor(project, context).map { it.path } + .filter { ! it.contains("kotlin") } + if (! sd.contains("kotlin")) { + extraSourceFiles.addAll(sd) + } + } + } + } + } + + val distinctSources = (sourceFiles + extraSourceFiles).distinctBy { File(it).toURI().normalize().path } + val allSources = distinctSources + .map { File(it).path } + .distinct() + .filter { File(it).exists() } + + // Finally, alter the info with the compiler interceptors before returning it + val initialActionInfo = CompilerActionInfo(projectDirectory.path, classpath, allSources, + sourceSuffixes, buildDirectory, emptyList() /* the flags will be provided by flag contributors */, + emptyList(), context.internalContext.forceRecompile) + val result = context.pluginInfo.compilerInterceptors.fold(initialActionInfo, { ai, interceptor -> + interceptor.intercept(project, context, ai) + }) + + // + // friendPaths + // + val friendPaths = KFiles.joinDir(project.buildDirectory, KFiles.CLASSES_DIR) + + return result + } + + /** + * Copy the resources from a source directory to the build one + */ + private fun copyResources(project: Project, context: KobaltContext, sourceSet: SourceSet) { + val outputDir = sourceSet.outputDir + + val variantSourceDirs = context.variant.resourceDirectories(project, sourceSet) + if (variantSourceDirs.isNotEmpty()) { + context.logger.log(project.name, 2, "Copying $sourceSet resources") + val absOutputDir = File(KFiles.joinDir(project.directory, project.buildDirectory, outputDir)) + variantSourceDirs + .map { File(project.directory, it.path) } + .filter(File::exists) + .forEach { + context.logger.log(project.name, 2, "Copying from $it to $absOutputDir") + KFiles.copyRecursively(it, absOutputDir, replaceExisting = true) + } + } else { + context.logger.log(project.name, 2, "No resources to copy for $sourceSet") + } + } + + fun sourceCompilerFlags(project: Project?, context: KobaltContext, info: CompilerActionInfo) : List { + val adapters = context.pluginInfo.compilerFlagContributors.map { + val closure = { project: Project, context: KobaltContext, currentFlags: List, + suffixesBeingCompiled: List + -> it.compilerFlagsFor(project, context, currentFlags, suffixesBeingCompiled) } + FlagContributor(it.flagPriority, closure) + } + return compilerFlags(project, context, info, adapters) + } + + fun compilerFlags(project: Project?, context: KobaltContext, info: CompilerActionInfo, + adapters: List) : List { + val result = arrayListOf() + if (project != null) { + adapters.sortedBy { it.flagPriority } + adapters.forEach { + result.addAll(it.flagsFor(project, context, result, info.suffixesBeingCompiled)) + } + } + return result + } +} diff --git a/src/main/kotlin/com/beust/kobalt/internal/DocUrl.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/DocUrl.kt similarity index 77% rename from src/main/kotlin/com/beust/kobalt/internal/DocUrl.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/DocUrl.kt index 8acab0ab..93010294 100644 --- a/src/main/kotlin/com/beust/kobalt/internal/DocUrl.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/DocUrl.kt @@ -2,7 +2,7 @@ package com.beust.kobalt.internal class DocUrl { companion object { - private const val HOST = "http://beust.com/kobalt/" + private const val HOST = "https://beust.com/kobalt/" private fun url(path: String) = HOST + path val PUBLISH_PLUGIN_URL = url("plug-ins/index.html#publishing") diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/DynamicGraph.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/DynamicGraph.kt new file mode 100644 index 00000000..a3e26afd --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/DynamicGraph.kt @@ -0,0 +1,408 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.* +import com.beust.kobalt.misc.* +import com.google.common.collect.HashMultimap +import java.lang.reflect.InvocationTargetException +import java.util.* +import java.util.concurrent.* + +open class TaskResult2(success: Boolean, testResult: TestResult? = null, + errorMessage: String? = null, val value: T) : TaskResult(success, testResult, errorMessage) { + override fun toString() = com.beust.kobalt.misc.toString("TaskResult", "value", value, "success", success) +} + +class DynamicGraph { + val VERBOSE = 3 + val values : Collection get() = nodes.map { it.value } + val nodes = hashSetOf>() + private val dependedUpon = HashMultimap.create, PrivateNode>() + private val dependingOn = HashMultimap.create, PrivateNode>() + + class PrivateNode(val value: T) { + override fun hashCode() = value!!.hashCode() + override fun equals(other: Any?) : Boolean { + val result = if (other is PrivateNode<*>) other.value == value else false + return result + } + override fun toString() = value.toString() + } + + companion object { + fun transitiveClosure(root: T, childrenFor: (T) -> List) : List { + val result = arrayListOf() + val seen = hashSetOf() + val toProcess = arrayListOf().apply { + add(root) + } + while (toProcess.any()) { + val newToProcess = arrayListOf() + toProcess.forEach { + if (! seen.contains(it)) { + result.add(it) + newToProcess.addAll(childrenFor(it)) + seen.add(it) + } + } + toProcess.clear() + toProcess.addAll(newToProcess) + } + return result + } + + class Node(val value: T, val children: List>) { + fun dump(root : Node = this, indent: String = "") : String { + return StringBuffer().apply { + append(indent).append(root.value).append("\n") + root.children.forEach { + append(dump(it, indent + " ")) + } + }.toString() + } + } + + fun transitiveClosureGraph(roots: List, childrenFor: (T) -> List, + filter: (T) -> Boolean): List> + = roots.map { transitiveClosureGraph(it, childrenFor, filter) } + + fun transitiveClosureGraph(root: T, childrenFor: (T) -> List, + filter: (T) -> Boolean = { t: T -> true }, + seen: HashSet = hashSetOf()) : Node { + val children = arrayListOf>() + childrenFor(root).filter(filter).forEach { child -> + if (! seen.contains(child)) { + seen.add(child) + val c = transitiveClosureGraph(child, childrenFor, filter, seen) + children.add(c) + } + } + return Node(root, children) + } + } + + fun childrenOf(v: T) : Collection = dependedUpon[PrivateNode(v)].map { it.value } + + fun transitiveClosure(root: T) + = transitiveClosure(root) { element -> dependedUpon[PrivateNode(element)].map { it.value } } + + fun addNode(t: T) = synchronized(nodes) { + nodes.add(PrivateNode(t)) + } + + fun removeNode(t: T) = synchronized(nodes) { + kobaltLog(VERBOSE, " Removing node $t") + PrivateNode(t).let { node -> + nodes.remove(node) + dependingOn.removeAll(node) + val set = dependedUpon.keySet() + val toReplace = arrayListOf, Collection>>>() + set.forEach { du -> + val l = ArrayList(dependedUpon[du]) + l.remove(node) + toReplace.add(Pair(du, l)) + } + toReplace.forEach { + dependedUpon.replaceValues(it.first, it.second) + } + } + } + + /** + * Make "from" depend on "to" ("from" is no longer free). + */ + fun addEdge(from: T, to: T) { + val fromNode = PrivateNode(from) + nodes.add(fromNode) + val toNode = PrivateNode(to) + nodes.add(PrivateNode(to)) + dependingOn.put(toNode, fromNode) + dependedUpon.put(fromNode, toNode) + } + + val freeNodes: Set + get() { + val nonFree = hashSetOf() + synchronized(nodes) { + nodes.forEach { + val du = dependedUpon[it] + if (du != null && du.size > 0) { + nonFree.add(it.value) + } + } + val result = nodes.map { it.value }.filter { !nonFree.contains(it) }.toHashSet() + kobaltLog(VERBOSE, " Free nodes: $result") + return result + } + } + + fun dump() : String { + val result = StringBuffer() + result.append("************ Graph dump ***************\n") + val free = arrayListOf>() + nodes.forEach { node -> + val d = dependedUpon.get(node) + if (d == null || d.isEmpty()) { + free.add(node) + } + } + + result.append("All nodes: $values\n").append("Free nodes: $free").append("\nDependent nodes:\n") + nodes.forEach { + val deps = dependedUpon.get(it) + if (! deps.isEmpty()) { + result.append(" $it -> $deps\n") + } + } + return result.toString() + } +} + +interface IWorker : Callable> { + /** + * @return list of tasks this worker is working on. + */ + // val tasks : List + + val name: String + + /** + * @return the priority of this task. + */ + val priority : Int +} + +interface IThreadWorkerFactory { + + /** + * Creates {@code IWorker} for specified set of tasks. It is not necessary that + * number of workers returned be same as number of tasks entered. + * + * @param nodes tasks that need to be executed + * @return list of workers + */ + fun createWorkers(nodes: Collection) : List> +} + +class DynamicGraphExecutor(val graph : DynamicGraph, val factory: IThreadWorkerFactory, + val threadCount: Int = 1) { + val executor : ExecutorService + = Executors.newFixedThreadPool(threadCount, NamedThreadFactory("DynamicGraphExecutor")) + val completion = ExecutorCompletionService>(executor) + + data class HistoryLog(val name: String, val timestamp: Long, val threadId: Long, val start: Boolean) + + val historyLog = arrayListOf() + val threadIds = ConcurrentHashMap() + + fun run() : TaskResult { + try { + return run2() + } finally { + executor.shutdown() + } + } + + private fun run2() : TaskResult { + var running = 0 + val nodesRun = hashSetOf() + var failedResult: TaskResult? = null + val newFreeNodes = HashSet(graph.freeNodes) + while (failedResult == null && (running > 0 || newFreeNodes.size > 0)) { + nodesRun.addAll(newFreeNodes) + val callables : List> = factory.createWorkers(newFreeNodes).map { + it -> object: IWorker { + override val priority: Int + get() = it.priority + + override val name: String get() = it.name + override fun call(): TaskResult2 { + val threadId = Thread.currentThread().id + historyLog.add(HistoryLog(it.name, System.currentTimeMillis(), threadId, + start = true)) + threadIds.put(threadId, threadId) + val result = it.call() + historyLog.add(HistoryLog(it.name, System.currentTimeMillis(), Thread.currentThread().id, + start = false)) + return result + } + } + } + callables.forEach { completion.submit(it) } + running += callables.size + + try { + val future = completion.take() + val taskResult = future.get(2, TimeUnit.SECONDS) + running-- + if (taskResult.success) { + nodesRun.add(taskResult.value) + kobaltLog(3, "Task succeeded: $taskResult") + graph.removeNode(taskResult.value) + newFreeNodes.clear() + newFreeNodes.addAll(graph.freeNodes.minus(nodesRun)) + } else { + kobaltLog(3, "Task failed: $taskResult") + newFreeNodes.clear() + if (failedResult == null) { + failedResult = taskResult + } + } + } catch(ex: TimeoutException) { + kobaltLog(3, "Time out") + } catch(ex: Exception) { + val ite = ex.cause + if (ite is InvocationTargetException) { + if (ite.targetException is KobaltException) { + throw (ex.cause as InvocationTargetException).targetException + } else { + error("Error: ${ite.cause?.message}", ite.cause) + failedResult = TaskResult(success = false, errorMessage = ite.cause?.message) + } + } else { + error("Error: ${ex.message}", ex) + failedResult = TaskResult(success = false, errorMessage = ex.message) + } + } + } + return if (failedResult != null) failedResult else TaskResult() + } + + fun dumpHistory() { + kobaltLog(1, "Thread report") + + val table = AsciiTable.Builder() + .columnWidth(11) + threadIds.keys.forEach { + table.columnWidth(24) + } + table.header("Time (sec)") + threadIds.keys.forEach { + table.header("Thread " + it.toString()) + } + + fun toSeconds(millis: Long) = (millis / 1000).toInt().toString() + + fun displayCompressedLog(table: AsciiTable.Builder) : AsciiTable.Builder { + data class CompressedLog(val timestamp: Long, val threadMap: HashMap) + + fun compressLog(historyLog: List): ArrayList { + val compressed = arrayListOf() + + var currentLog: CompressedLog? = null + + val projectStart = hashMapOf() + fun toName(hl: HistoryLog) : String { + var duration = "" + if (! hl.start) { + val start = projectStart[hl.name] + if (start != null) { + duration = " (" + ((hl.timestamp - start) / 1000) + .toInt().toString() + ")" + } else { + kobaltLog(1, "DONOTCOMMIT") + } + } + return hl.name + duration + } + + historyLog.forEach { hl -> + kobaltLog(1, "CURRENT LOG: " + currentLog + " HISTORY LINE: " + hl) + if (hl.start) { + projectStart[hl.name] = hl.timestamp + } + if (currentLog == null) { + currentLog = CompressedLog(hl.timestamp, hashMapOf(hl.threadId to hl.name)) + } else currentLog?.let { cl -> + if (! hl.start || hl.timestamp - cl.timestamp < 1000) { + kobaltLog(1, " CURRENT LOG IS WITHING ONE SECOND: $hl") + cl.threadMap[hl.threadId] = toName(hl) + } else { + kobaltLog(1, " ADDING COMPRESSED LINE $cl") + compressed.add(cl) + currentLog = CompressedLog(hl.timestamp, hashMapOf(hl.threadId to toName(hl))) + } + } + } + return compressed + } + + compressLog(historyLog).forEach { + val row = arrayListOf() + row.add(toSeconds(it.timestamp)) + it.threadMap.values.forEach { + row.add(it) + } + table.addRow(row) + } + + return table + } + + fun displayRegularLog(table: AsciiTable.Builder) : AsciiTable.Builder { + if (historyLog.any()) { + if (historyLog[0] != null) { + val start = historyLog[0].timestamp + val projectStart = ConcurrentHashMap() + historyLog.forEach { line -> + val row = arrayListOf() + row.add(toSeconds(line.timestamp - start)) + threadIds.keys.forEach { + if (line.threadId == it) { + var duration = "" + if (line.start) { + projectStart[line.name] = line.timestamp + } else { + val projectStart = projectStart[line.name] + if (projectStart != null) { + duration = " (" + ((line.timestamp - projectStart) / 1000) + .toInt().toString() + ")" + } else { + warn("Couldn't determine project start: " + line.name) + } + } + row.add((line.name + duration)) + } else { + row.add("") + } + } + table.addRow(row) + } + } else { + warn("Couldn't find historyLog") + } + } + return table + } + + kobaltLog(1, displayRegularLog(table).build()) + } +} + +fun main(argv: Array) { + val dg = DynamicGraph().apply { + // a -> b + // b -> c, d + // e + addEdge("a", "b") + addEdge("b", "c") + addEdge("b", "d") + addNode("e") + } + val factory = object : IThreadWorkerFactory { + override fun createWorkers(nodes: Collection): List> { + return nodes.map { + object: IWorker { + override fun call(): TaskResult2? { + kobaltLog(1, " Running worker $it") + return TaskResult2(true, value = it) + } + + override val priority: Int get() = 0 + override val name: String = "workerName" + } + } + } + } + + DynamicGraphExecutor(dg, factory).run() +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/GenericRunner.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/GenericRunner.kt new file mode 100644 index 00000000..995dba53 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/GenericRunner.kt @@ -0,0 +1,198 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.* +import com.beust.kobalt.api.* +import com.beust.kobalt.misc.KFiles +import com.google.common.annotations.VisibleForTesting +import com.google.inject.Inject +import java.io.File +import java.util.* + +/** + * Base class for testing frameworks that are invoked from a main class with arguments. Test runners can + * subclass this class and override mainClass, args and the name of the dependency that should trigger this runner. + */ +abstract class GenericTestRunner: ITestRunnerContributor { + abstract val dependencyName : String + abstract val mainClass: String + abstract val annotationPackage: String + abstract val runnerName: String + open var shortMessage: String? = null + open var longMessage: String? = null + + @Inject + private lateinit var jvm: Jvm + + abstract fun args(project: Project, context: KobaltContext, classpath: List, + testConfig: TestConfig) : List + + open fun onFinish(project: Project) {} + + open val extraClasspath: List = emptyList() + + open fun filterTestClasses(project: Project, context: KobaltContext, classes: List) : List = classes + + override fun run(project: Project, context: KobaltContext, configName: String, + classpath: List) : TaskResult { + val tr = runTests(project, context, classpath, configName) + return TaskResult(tr.success, testResult = tr) + } + + override fun affinity(project: Project, context: KobaltContext) : Int { + val result = + if (project.testDependencies.any { it.id.contains(dependencyName) }) IAffinity.DEFAULT_POSITIVE_AFFINITY + else 0 + return result + } + + protected fun findTestClasses(project: Project, context: KobaltContext, testConfig: TestConfig): List { + val testClassDir = KFiles.joinDir(project.buildDirectory, KFiles.TEST_CLASSES_DIR) + val path = testClassDir.apply { + File(this).mkdirs() + } + + val files = IFileSpec.GlobSpec(toClassPaths(testConfig.testIncludes)) + .toFiles(project.directory, path, testConfig.testExcludes.map { Glob(it) }) + val testClasses = files + .map { + File(KFiles.joinDir(project.directory, testClassDir, it.path)) + } + val result = testClasses.map { + val prefix = KFiles.joinDir(project.directory, testClassDir) + val className = it.toString().substring(prefix.length + 1) + .replace("/", ".").replace("\\", ".").replace(".class", "") + Pair(it, className) + } +// .filter { +// val result = acceptClass(it.first, it.second, testClasspath, File(testClassDir)) +// result +// } + + context.logger.log(project.name, 2, "Found ${result.size} test classes") + return filterTestClasses(project, context, result.map { it.second }) + } + + /** + * Accept the given class if it contains an annotation of the current test runner's package. Annotations + * are looked up on both the classes and methods. + */ +// private fun acceptClass(cf: File, className: String, testClasspath: List, +// testClassDir: File): Boolean { +// val cp = (testClasspath.map { it.jarFile.get() } + listOf(testClassDir)).map { it.toURI().toURL() } +// try { +// val cls = URLClassLoader(cp.toTypedArray()).loadClass(className) +// val ann = cls.annotations.filter { +// val qn = it.annotationClass.qualifiedName +// qn != null && qn.contains(annotationPackage) +// } +// if (ann.any()) { +// return true +// } else { +// val ann2 = cls.declaredMethods.flatMap { it.declaredAnnotations.toList() }.filter { it.toString() +// .contains(annotationPackage)} +// if (ann2.any()) { +// val a0 = ann2[0] +// return true +// } +// } +// } catch(ex: Throwable) { +// println("Exception: " + ex.message) +// return false +// } +// return false +// } + + private fun toClassPaths(paths: List): ArrayList = + paths.map { if (it.endsWith("class")) it else it + "class" }.toCollection(ArrayList()) + + /** + * @return true if all the tests passed + */ + open fun runTests(project: Project, context: KobaltContext, classpath: List, + configName: String) : TestResult { + var result = false + + context.logger.log(project.name, 1, "Running tests with $runnerName") + + val testConfig = project.testConfigs.firstOrNull { it.name == configName } + + var errorCode = -1 + if (testConfig != null) { + val args = args(project, context, classpath, testConfig) + if (args.size > 0) { + + val java = jvm.javaExecutable + val jvmArgs = calculateAllJvmArgs(project, context, testConfig, classpath, + Kobalt.INJECTOR.getInstance (PluginInfo::class.java)) + val allArgs = arrayListOf().apply { + add(java!!.absolutePath) + addAll(jvmArgs) + add(mainClass) + addAll(args) + } + + val pb = ProcessBuilder(allArgs) + pb.directory(File(project.directory)) + pb.inheritIO() + context.logger.log(project.name, 2, "Running tests with classpath size ${classpath.size}") + context.logger.log(project.name, 2, "Launching " + allArgs.joinToString(" ")) + val process = pb.start() + errorCode = process.waitFor() + result = result || errorCode == 0 + } else { + context.logger.log(project.name, 1, " No tests to run") + result = true + } + } else { + throw KobaltException("Couldn't find a test configuration named \"$configName\"") + } + + onFinish(project) + + if (errorCode == 0) { + context.logger.log(project.name, 1, "All tests passed") + } else { + context.logger.log(project.name, 1, longMessage!!) + } + + return TestResult(result, shortMessage, longMessage) + } + + /* + ** @return all the JVM flags from contributors and interceptors. + */ + @VisibleForTesting + fun calculateAllJvmArgs(project: Project, context: KobaltContext, + testConfig: TestConfig, classpath: List, pluginInfo: IPluginInfo) : List { + val fullClasspath = classpath.map { it.jarFile.get().absolutePath } + extraClasspath + // Default JVM args + val jvmFlags = arrayListOf().apply { + addAll(testConfig.jvmArgs) + add("-ea") + add("-classpath") + add(fullClasspath.joinToString(File.pathSeparator)) + } + + // JVM flags from the contributors + val jvmFlagsFromContributors = pluginInfo.testJvmFlagContributors.flatMap { + it.testJvmFlagsFor(project, context, jvmFlags) + } + + // JVM flags from the interceptors (these overwrite flags instead of just adding to the list) + val result = ArrayList(jvmFlags + jvmFlagsFromContributors) + pluginInfo.testJvmFlagInterceptors.forEach { + val newFlags = it.testJvmFlagsFor(project, context, result) + result.clear() + result.addAll(newFlags) + } + + if (result.any()) { + context.logger.log(project.name, 2, + "Final JVM test flags after running the contributors and interceptors: $result") + } + + return result + } + +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/GraphUtil.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/GraphUtil.kt new file mode 100644 index 00000000..6cef8ada --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/GraphUtil.kt @@ -0,0 +1,35 @@ +package com.beust.kobalt.internal + +/** + * Generic operations on graph-like structures. + */ +object GraphUtil { + /** + * Apply the operation in `closure` to all the nodes in the tree. + */ + fun map(roots: List, children: (T) -> List, closure: (T) -> Unit) { + roots.forEach { + closure(it) + map(children(it), children, closure) + } + } + + /** + * Display each node in the roots by calling the `display` function on each of them. + */ + fun displayGraph(roots: List, + children: (T) -> List, + display: (node: T, indent: String) -> Unit) { + + fun pd(node: T, indent: String) { + display(node, indent) + children(node).forEach { + pd(it, indent + " ") + } + } + roots.forEach { + pd(it, "") + } + } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/IncrementalManager.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/IncrementalManager.kt new file mode 100644 index 00000000..067cf0c1 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/IncrementalManager.kt @@ -0,0 +1,186 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.Args +import com.beust.kobalt.IncrementalTaskInfo +import com.beust.kobalt.TaskResult +import com.beust.kobalt.Variant +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.api.Project +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.kobaltLog +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.inject.Inject +import com.google.inject.assistedinject.Assisted +import java.io.File +import java.io.FileReader +import java.nio.charset.Charset +import java.nio.file.Files +import java.nio.file.Paths +import java.util.* + +/** + * Manage the file .kobalt/buildInfo.json, which keeps track of input and output checksums to manage + * incremental builds. + */ +class IncrementalManager @Inject constructor(val args: Args, @Assisted val fileName : String) { + + private data class TaskInfo(val taskName: String, var inputChecksum: String? = null, + var outputChecksum: String? = null) + + private class BuildInfo(var tasks: List) + + interface IFactory { + fun create(@Assisted fileName: String = IncrementalManager.BUILD_INFO_FILE) : IncrementalManager + } + + companion object { + val BUILD_INFO_FILE = KFiles.joinDir(KFiles.KOBALT_DOT_DIR, "buildInfo.json") + } + + private fun buildInfo() = File(fileName).let { file -> + if (file.exists()) { + Gson().fromJson(FileReader(file), BuildInfo::class.java) ?: BuildInfo(emptyList()) + } else { + BuildInfo(emptyList()) + } + } + + private fun taskInfos() = hashMapOf().apply { + buildInfo().tasks.forEach { + put(it.taskName, it) + } + } + + private fun save(map: Map) { + val bi = BuildInfo(map.values.toList()) + val json = GsonBuilder().setPrettyPrinting().create().toJson(bi) + + Files.write(Paths.get(fileName), json.toByteArray(Charset.defaultCharset())) + } + + private fun taskInfoFor(taskInfos: HashMap, taskName: String) + = taskInfos.getOrPut(taskName, { -> TaskInfo(taskName) }) + + fun saveInputChecksum(taskName: String, inputChecksum: String) { + synchronized(BUILD_INFO_FILE) { + with(taskInfos()) { + taskInfoFor(this, taskName).inputChecksum = inputChecksum + save(this) + } + } + } + + fun inputChecksumFor(taskName: String) : String? = + synchronized(BUILD_INFO_FILE) { + taskInfoFor(taskInfos(), taskName).inputChecksum + } + + fun saveOutputChecksum(taskName: String, outputChecksum: String) { + synchronized(BUILD_INFO_FILE) { + with(taskInfos()) { + taskInfoFor(this, taskName).outputChecksum = outputChecksum + save(this) + } + } + } + + fun outputChecksumFor(taskName: String) : String? = + synchronized(BUILD_INFO_FILE) { + taskInfoFor(taskInfos(), taskName).outputChecksum + } + + /** + * @param method is assumed to return an IncrementalTaskInfo. + * @return a closure that invokes that method and decide whether to run the task or not based + * on the content of that IncrementalTaskInfo + */ + fun toIncrementalTaskClosure(shortTaskName: String, method: (Project) -> IncrementalTaskInfo, + variant: Variant): (Project) -> TaskResult { + return { project: Project -> + Kobalt.context?.variant = variant + val iti = method(project) + val taskName = project.name + ":" + shortTaskName + var upToDate = false + var taskOutputChecksum : String? = null + + if (! args.forceIncremental && + (args.noIncremental || (Kobalt.context?.internalContext?.buildFileOutOfDate as Boolean))) { + // + // If the user turned off incremental builds or if the build file was modified, always run this task + // + logIncremental(LEVEL, "Incremental builds are turned off, running $taskName") + upToDate = false +// } else if (iti.context.internalContext.previousTaskWasIncrementalSuccess(project.name)) { +// // +// // If the previous task was an incremental success, no need to run this task. + // + // Disabled for now since this can only work if exactly the previous task was + // an incremental success. If it was a regular task, then this boolean should be + // set back to false, which is currently not done. +// // +// logIncremental(LEVEL, "Previous incremental task was a success, not running $shortTaskName") +// upToDate = true + } else { + // + // First, compare the input checksums + // + inputChecksumFor(taskName)?.let { inputChecksum -> + val dependsOnDirtyProjects = project.projectExtra.dependsOnDirtyProjects(project) + if (inputChecksum == iti.inputChecksum() && !dependsOnDirtyProjects) { + // + // Input checksums are equal, compare the output checksums + // + outputChecksumFor(taskName)?.let { outputChecksum -> + taskOutputChecksum = iti.outputChecksum() + if (outputChecksum == taskOutputChecksum) { + upToDate = true + } else { + logIncremental(LEVEL, "Incremental task $taskName output is out of date" + + " (different output checksums), running it") + } + } + } else { + if (dependsOnDirtyProjects) { + logIncremental(LEVEL, "Project ${project.name} depends on dirty project, running $taskName") + } else { + logIncremental(LEVEL, "Incremental task $taskName input is out of date, running it" + + " (different input checksums old: $inputChecksum new: ${iti.inputChecksum()})") + } + project.projectExtra.isDirty = true + } + } + } + + if (!upToDate) { + // + // The task is out of date, invoke the task on the IncrementalTaskInfo object + // + val result = iti.task(project) + if (result.success) { + logIncremental(LEVEL, "Incremental task $taskName done running, saving checksums") + iti.inputChecksum()?.let { + saveInputChecksum(taskName, it) + logIncremental(LEVEL, " input checksum \"$it\" saved") + } + // Important to rerun the checksum here since the output of the task might have changed it + iti.outputChecksum()?.let { + saveOutputChecksum(taskName, it) + logIncremental(LEVEL, " output checksum \"$it\" saved") + } + } + result + } else { + // + // Identical input and output checksums, don't run the task + // + logIncremental(LEVEL, "Incremental task \"$taskName\" is up to date, not running it") + iti.context.internalContext.setIncrementalSuccess(project.name) + TaskResult() + } + } + } + + val LEVEL = 2 + private fun logIncremental(level: Int, s: String) = kobaltLog(level, " INC - $s") +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JUnit5Runner.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JUnit5Runner.kt new file mode 100644 index 00000000..2e9b534c --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JUnit5Runner.kt @@ -0,0 +1,152 @@ +package com.beust.kobalt.internal + +import com.beust.jcommander.JCommander +import com.beust.jcommander.Parameter +import com.beust.kobalt.TestConfig +import com.beust.kobalt.api.IAffinity +import com.beust.kobalt.api.IClasspathDependency +import com.beust.kobalt.api.KobaltContext +import com.beust.kobalt.api.Project +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.KobaltLogger +import com.google.inject.Inject +import org.junit.platform.engine.TestExecutionResult +import org.junit.platform.engine.discovery.DiscoverySelectors +import org.junit.platform.engine.reporting.ReportEntry +import org.junit.platform.engine.support.descriptor.MethodSource +import org.junit.platform.launcher.LauncherDiscoveryRequest +import org.junit.platform.launcher.TestExecutionListener +import org.junit.platform.launcher.TestIdentifier +import org.junit.platform.launcher.TestPlan +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder +import org.junit.platform.launcher.core.LauncherFactory +import java.io.File +import java.nio.file.Paths + +/** + * Runner for JUnit 5 tests. This class also contains a main() entry point since JUnit 5 no longer supplies one. + */ +class JUnit5Runner @Inject constructor(kFiles: KFiles) : GenericTestRunner() { + + override val dependencyName = "jupiter" + override val annotationPackage = "org.junit.jupiter.api" + override val mainClass = "com.beust.kobalt.internal.JUnit5RunnerKt" + override val runnerName = "JUnit 5" + + override fun affinity(project: Project, context: KobaltContext) : Int { + val result = + if (project.testDependencies.any { it.id.contains("junit5") || it.id.contains("jupiter") }) + IAffinity.DEFAULT_POSITIVE_AFFINITY + 100 + else 0 + return result + + } + + override fun args(project: Project, context: KobaltContext, classpath: List, testConfig: TestConfig): List { + val testClassDir = KFiles.joinDir(project.buildDirectory, KFiles.TEST_CLASSES_DIR) + val classDir = KFiles.joinDir(project.buildDirectory, KFiles.CLASSES_DIR) + val args = listOf("--testClassDir", testClassDir, + "--classDir", classDir, + "--log", KobaltLogger.LOG_LEVEL.toString()) + return args + } + + override val extraClasspath = kFiles.kobaltJar +} + +private class Args { + @Parameter(names = arrayOf("--log")) + var log: Int = 1 + + @Parameter(names = arrayOf("--testClassDir")) + var testClassDir: String = "kobaltBuild/test-classes" + + @Parameter(names = arrayOf("--classDir")) + var classDir: String = "kobaltBuild/classes" +} + +fun main(argv: Array) { + val args = Args() + val jc = JCommander(args) + jc.parse(*argv) + + val testClassDir = File(args.testClassDir).absolutePath + val classDir = File(args.classDir).absolutePath + val request : LauncherDiscoveryRequest = LauncherDiscoveryRequestBuilder() + .selectors(DiscoverySelectors.selectClasspathRoots(setOf( + Paths.get(testClassDir), + Paths.get(classDir) + ))) + .selectors(DiscoverySelectors.selectDirectory(testClassDir)) + .build() + + fun testName(id: TestIdentifier) : String? { + val result = + if (id.source.isPresent) { + val source = id.source.get() + if (source is MethodSource) { + source.className + "." + source.methodName + } else { + null + } + } else { + null + } + return result + } + + var passed = 0 + var failed = 0 + var skipped = 0 + var aborted = 0 + + fun log(level: Int, s: String) { + if (level <= args.log) println(s) + } + + val listener = object: TestExecutionListener { + override fun executionFinished(testIdentifier: TestIdentifier, testExecutionResult: TestExecutionResult) { + val testName = testName(testIdentifier) + if (testName != null) { + when(testExecutionResult.status) { + TestExecutionResult.Status.FAILED -> { + log(1, "FAILED: $testName, reason: " + testExecutionResult.throwable.get().toString()) + failed++ + } + TestExecutionResult.Status.ABORTED -> { + log(1, "ABORTED: $testName, reason: " + testExecutionResult.throwable.get().toString()) + aborted++ + } + TestExecutionResult.Status.SUCCESSFUL -> { + log(2, "PASSED: $testName") + passed++ + } else -> { + + } + } + } + } + + override fun executionSkipped(testIdentifier: TestIdentifier, reason: String) { + testName(testIdentifier)?.let { + log(1, "Skipping $it because $reason") + skipped++ + } + } + + override fun executionStarted(testIdentifier: TestIdentifier) { + testName(testIdentifier)?.let { + log(2, "Starting $it") + } + } + + override fun testPlanExecutionStarted(testPlan: TestPlan?) {} + override fun dynamicTestRegistered(testIdentifier: TestIdentifier?) {} + override fun reportingEntryPublished(testIdentifier: TestIdentifier?, entry: ReportEntry?) {} + override fun testPlanExecutionFinished(testPlan: TestPlan?) {} + } + + LauncherFactory.create().execute(request, listener) + + log(1, "TEST RESULTS: $passed PASSED, $failed FAILED, $skipped SKIPPED, $aborted ABORTED") +} \ No newline at end of file diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JUnitRunner.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JUnitRunner.kt new file mode 100644 index 00000000..c5b36997 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JUnitRunner.kt @@ -0,0 +1,32 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.TestConfig +import com.beust.kobalt.api.IClasspathDependency +import com.beust.kobalt.api.KobaltContext +import com.beust.kobalt.api.Project +import com.beust.kobalt.maven.DependencyManager +import com.google.inject.Inject +import java.lang.reflect.Modifier +import java.net.URLClassLoader + +open class JUnitRunner() : GenericTestRunner() { + + override val mainClass = "org.junit.runner.JUnitCore" + override val annotationPackage = "org.junit" + override val dependencyName = "junit" + override val runnerName = "JUnit 4" + + override fun args(project: Project, context: KobaltContext, classpath: List, + testConfig: TestConfig) = findTestClasses(project, context, testConfig) + + @Inject + lateinit var dependencyManager: DependencyManager + + override fun filterTestClasses(project: Project, context: KobaltContext, classes: List) : List { + val deps = dependencyManager.testDependencies(project, context) + val cl = URLClassLoader(deps.map { it.jarFile.get().toURI().toURL() }.toTypedArray()) + return classes.filter { !Modifier.isAbstract(cl.loadClass(it).modifiers) } + } + +} + diff --git a/src/main/kotlin/com/beust/kobalt/internal/JvmCompiler.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JvmCompiler.kt similarity index 61% rename from src/main/kotlin/com/beust/kobalt/internal/JvmCompiler.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JvmCompiler.kt index 3e0bddc0..e7773737 100644 --- a/src/main/kotlin/com/beust/kobalt/internal/JvmCompiler.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JvmCompiler.kt @@ -15,42 +15,36 @@ import java.util.* * Also validates the classpath and run all the contributors. */ class JvmCompiler @Inject constructor(val dependencyManager: DependencyManager) { - /** * Take the given CompilerActionInfo and enrich it with all the applicable contributors and * then pass it to the ICompilerAction. */ - fun doCompile(project: Project?, context: KobaltContext?, action: ICompilerAction, info: CompilerActionInfo) - : TaskResult { + fun doCompile(project: Project?, context: KobaltContext?, action: ICompilerAction, info: CompilerActionInfo, + flags: List): TaskResult { // Dependencies - val allDependencies = (info.dependencies - + dependencyManager.calculateDependencies(project, context!!, allDependencies = info.dependencies)) + val allDependencies = (info.dependencies + dependencyManager.calculateDependencies(project, context!!, + passedDependencies = info.dependencies)) .distinct() // Plugins that add flags to the compiler - val addedFlags = ArrayList(info.compilerArgs) + - if (project != null) { - context.pluginInfo.compilerFlagContributors.flatMap { - it.flagsFor(project, info.compilerArgs) - } - } else { - emptyList() - } + val contributorFlags : List = if (project != null) flags else emptyList() + + val addedFlags = contributorFlags + ArrayList(info.compilerArgs) validateClasspath(allDependencies.map { it.jarFile.get().absolutePath }) - return action.compile(info.copy(dependencies = allDependencies, compilerArgs = addedFlags)) + return action.compile(project, info.copy(dependencies = allDependencies, compilerArgs = addedFlags)) } private fun validateClasspath(cp: List) { cp.forEach { if (! File(it).exists()) { - throw KobaltException("Couldn't find $it") + throw KobaltException("Invalid classpath: couldn't find $it") } } } } interface ICompilerAction { - fun compile(info: CompilerActionInfo): TaskResult + fun compile(project: Project?, info: CompilerActionInfo): TaskResult } \ No newline at end of file diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JvmCompilerPlugin.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JvmCompilerPlugin.kt new file mode 100644 index 00000000..5e2a9354 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JvmCompilerPlugin.kt @@ -0,0 +1,261 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.IncrementalTaskInfo +import com.beust.kobalt.KobaltException +import com.beust.kobalt.TaskResult +import com.beust.kobalt.TestConfig +import com.beust.kobalt.api.* +import com.beust.kobalt.api.annotation.ExportedProjectProperty +import com.beust.kobalt.api.annotation.IncrementalTask +import com.beust.kobalt.api.annotation.Task +import com.beust.kobalt.maven.DependencyManager +import com.beust.kobalt.maven.Md5 +import com.beust.kobalt.maven.aether.Scope +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.KobaltExecutors +import com.beust.kobalt.misc.error +import com.beust.kobalt.misc.warn +import java.io.File +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This plug-in takes care of compilation: it declares several common tasks ("compile", "compileTest") + * and picks up all the compiler contributors in order to run them whenever a compilation is requested. + */ +@Singleton +open class JvmCompilerPlugin @Inject constructor( + open val files: KFiles, + open val dependencyManager: DependencyManager, + open val executors: KobaltExecutors, + open val taskContributor : TaskContributor, + val compilerUtils: CompilerUtils) + : BasePlugin(), ISourceDirectoryContributor, IProjectContributor, ITaskContributor by taskContributor { + + companion object { + val PLUGIN_NAME = "JvmCompiler" + + @ExportedProjectProperty(doc = "Compiler args", type = "List") + const val COMPILER_ARGS = "compilerArgs" + + const val TASK_COMPILE = "compile" + const val TASK_COMPILE_TEST = "compileTest" + const val TASK_CLEAN = "clean" + const val TASK_TEST = "test" + + const val DOCS_DIRECTORY = "docs/javadoc" + + const val GROUP_TEST = "test" + const val GROUP_BUILD = "build" + const val GROUP_DOCUMENTATION = "documentation" + } + + override val name: String = PLUGIN_NAME + + override fun accept(project: Project) = true + + override fun apply(project: Project, context: KobaltContext) { + super.apply(project, context) +// cleanUpActors() + taskContributor.addIncrementalVariantTasks(this, project, context, "compile", GROUP_BUILD, + runTask = { taskCompile(project) }) + + // + // Add each test config as a test task. If none was specified, create a default one so that + // users don't have to specify a test{} + // + if (project.testConfigs.isEmpty()) { + project.testConfigs.add(TestConfig(project, isDefault = true)) + } + project.testConfigs.forEach { config -> + val taskName = if (config.name.isEmpty()) TASK_TEST else TASK_TEST + config.name + + taskManager.addTask(this, project, taskName, group = GROUP_TEST, + dependsOn = listOf(JvmCompilerPlugin.TASK_COMPILE, JvmCompilerPlugin.TASK_COMPILE_TEST), + task = { taskTest(project, config.name)} ) + } + + } + + private fun taskTest(project: Project, configName: String): TaskResult { + context.logger.log(project.name, 2, "Running tests: $configName") + + val testContributor = ActorUtils.selectAffinityActor(project, context, + context.pluginInfo.testRunnerContributors) + if (testContributor != null && testContributor.affinity(project, context) > 0) { +// val td1 = dependencyManager.testDependencies(project, context) + val testDependencies = dependencyManager.calculateDependencies(project, context, + dependencyFilter = dependencyManager.createDependencyFilter(project, project.testDependencies), + scopes = listOf(Scope.TEST)) + val compileDependencies = dependencyManager.calculateDependencies(project, context, + scopes = listOf(Scope.COMPILE, Scope.COMPILEONLY)) + val allDependencies = (testDependencies + compileDependencies).distinct() + return testContributor.run(project, context, configName, allDependencies.toList()) + } else { + context.logger.log(project.name, 2, + "Couldn't find a test runner for project ${project.name}, did you specify dependenciesTest{}?") + return TaskResult() + } + } + + @Task(name = TASK_CLEAN, description = "Clean the project", group = GROUP_BUILD, + runBefore = arrayOf(JvmCompilerPlugin.TASK_COMPILE)) + fun taskClean(project: Project): TaskResult { + java.io.File(project.directory, project.buildDirectory).let { dir -> + if (!dir.deleteRecursively()) { + warn("Couldn't delete $dir") + } + } + return TaskResult() + } + + @IncrementalTask(name = TASK_COMPILE_TEST, description = "Compile the tests", group = GROUP_BUILD, + dependsOn = arrayOf(TASK_COMPILE)) + fun taskCompileTest(project: Project): IncrementalTaskInfo { + return IncrementalTaskInfo( + inputChecksum = { + Md5.toMd5Directories(context.testSourceDirectories(project).map { File(project.directory, it.path)}) + }, + outputChecksum = { + Md5.toMd5Directories(listOf(KFiles.makeOutputTestDir(project))) + }, + task = { project -> doTaskCompileTest(project)}, + context = context + ) + } + + private fun sourceDirectories(project: Project, context: KobaltContext, isTest: Boolean) + = context.variant.sourceDirectories(project, context, SourceSet.of(isTest)) + + @IncrementalTask(name = JvmCompilerPlugin.TASK_COMPILE, description = "Compile the project", group = GROUP_BUILD, + runAfter = arrayOf(TASK_CLEAN)) + fun taskCompile(project: Project): IncrementalTaskInfo { + return IncrementalTaskInfo( + inputChecksum = { + Md5.toMd5Directories(context.sourceDirectories(project).map { File(project.directory, it.path) }) + }, + outputChecksum = { + Md5.toMd5Directories(listOf(File(project.directory, project.classesDir(context)))) + }, + task = { project -> doTaskCompile(project) }, + context = context + ) + } + + private fun doTaskCompile(project: Project) = doTaskCompile(project, isTest = false) + + private fun doTaskCompileTest(project: Project) = doTaskCompile(project, isTest = true) + + private fun doTaskCompile(project: Project, isTest: Boolean): TaskResult { + val results = arrayListOf() + + val compilerContributors = context.pluginInfo.compilerContributors + ActorUtils.selectAffinityActors(project, context, context.pluginInfo.compilerContributors) + + var failedResult: TaskResult? = null + if (compilerContributors.isEmpty()) { + throw KobaltException("Couldn't find any compiler for project ${project.name}") + } else { + + // Generate BuildConfig if applicable + context.variant.maybeGenerateBuildConfig(project, context) + + val allCompilers = compilerContributors.flatMap { it.compilersFor(project, context)}.sorted() + + /** + * Swap the Java and Kotlin compilers from the list. + */ + fun swapJavaAndKotlin(allCompilers: List): List { + val result = ArrayList(allCompilers) + var ik = -1 + var ij = -1 + allCompilers.withIndex().forEach { wi -> + if (wi.value.sourceSuffixes.contains("java")) ij = wi.index + if (wi.value.sourceSuffixes.contains("kt")) ik = wi.index + } + + if (ik >= 0 && ij >= 0) { + Collections.swap(result, ik, ij) + } + return result + } + + // If this project has a kapt{} directive, we want to run the Java compiler first + val hasKapt = project.projectProperties.get("kaptConfig") != null + val allCompilersSorted = if (hasKapt) swapJavaAndKotlin(allCompilers) else allCompilers + var done = false + // The directory where the classes get compiled + val buildDirectory = + if (isTest) File(KFiles.joinDir(project.buildDirectory, KFiles.TEST_CLASSES_DIR)) + else File(KFiles.joinDir(project.classesDir(context))) + + allCompilersSorted.doWhile({ ! done }) { compiler -> + val compilerResults = compilerUtils.invokeCompiler(project, context, compiler, + sourceDirectories(project, context, isTest), isTest, buildDirectory) + results.addAll(compilerResults.successResults) + if (failedResult == null) failedResult = compilerResults.failedResult + compilerResults.failedResult?.let { failedResult -> + done = true + failedResult.errorMessage?.let { errorMessage -> + error(text = errorMessage) + } + } + } + + return if (failedResult != null) failedResult!! + else if (results.size > 0) results[0] + else TaskResult(true) + } + } + + private val allProjects = arrayListOf() + + // IProjectContributor + override fun projects() = allProjects + + override fun cleanUpActors() { + allProjects.clear() + } + + fun addDependentProjects(project: Project, dependents: List) { + project.dependsOn.addAll(dependents) + with(ProjectDescription(project, dependents)) { + allProjects.add(this) + } + } + + @Task(name = "doc", description = "Generate the documentation for the project", group = GROUP_DOCUMENTATION, + runBefore = arrayOf("assemble"), runAfter = arrayOf("clean")) + fun taskJavadoc(project: Project): TaskResult { + val docGenerator = ActorUtils.selectAffinityActor(project, context, context.pluginInfo.docContributors) + if (docGenerator != null) { + val buildDirectory = File(project.buildDirectory, JvmCompilerPlugin.DOCS_DIRECTORY) + val contributors = + ActorUtils.selectAffinityActors(project, context, context.pluginInfo.compilerContributors) + var result: TaskResult? = null + contributors.forEach { + it.compilersFor(project, context).forEach { compiler -> + result = docGenerator.generateDoc(project, context, + compilerUtils.createCompilerActionInfo(project, context, compiler, + isTest = false, sourceDirectories = sourceDirectories(project, context, false), + sourceSuffixes = compiler.sourceSuffixes, buildDirectory = buildDirectory)) + } + } + return result!! + } else { + warn("Couldn't find any doc contributor for project ${project.name}") + return TaskResult() + } + } + + // ISourceDirectoryContributor + override fun sourceDirectoriesFor(project: Project, context: KobaltContext) + = if (accept(project)) { + sourceDirectories(project, context, isTest = false) + } else { + arrayListOf() + } + + open val compiler: ICompilerContributor? = null +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/KobaltPluginXml.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/KobaltPluginXml.kt new file mode 100644 index 00000000..d8ca3555 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/KobaltPluginXml.kt @@ -0,0 +1,278 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.KobaltException +import com.beust.kobalt.api.* +import com.beust.kobalt.misc.kobaltLog +import java.io.ByteArrayInputStream +import java.io.InputStream +import javax.xml.bind.JAXBContext +import javax.xml.bind.annotation.XmlElement +import javax.xml.bind.annotation.XmlRootElement + +// +// Operations related to the parsing of kobalt-plugin.xml: XML parsing, PluginInfo, etc... +// + +/** + * If a plug-in didn't specify a factory, we use our own injector to instantiate all its components. + */ +class GuiceFactory : IFactory { + override fun instanceOf(c: Class) : T = Kobalt.INJECTOR.getInstance(c) +} + +///// +// XML parsing +// +// The following classes are used by JAXB to parse the kobalt-plugin.xml file. + +/** + * The root element of kobalt-plugin.xml + */ +@XmlRootElement(name = "kobalt-plugin") +class KobaltPluginXml { + @XmlElement @JvmField + var name: String? = null + + @XmlElement(name = "plugin-actors") @JvmField + var pluginActors : ClassNameXml? = null + + @XmlElement(name = "factory-class-name") @JvmField + var factoryClassName: String? = null +} + +class ContributorXml { + @XmlElement @JvmField + val name: String? = null +} + +class ClassNameXml { + @XmlElement(name = "class-name") @JvmField + var className: List = arrayListOf() +} + +/** + * The fields in this interface have tests. + */ +interface IPluginInfo { + val testJvmFlagContributors : List + val testJvmFlagInterceptors : List +} + +open class BasePluginInfo : IPluginInfo { + override val testJvmFlagContributors = arrayListOf() + override val testJvmFlagInterceptors = arrayListOf() +} +/** + * Turn a KobaltPluginXml (the raw content of kobalt-plugin.xml mapped to POJO's) into a PluginInfo object, which + * contains all the contributors instantiated and other information that Kobalt can actually use. Kobalt code that + * needs to access plug-in info can then just inject a PluginInfo object. + */ +class PluginInfo(val xml: KobaltPluginXml, val pluginClassLoader: ClassLoader?, val classLoader: ClassLoader?) + : BasePluginInfo() { + val plugins = arrayListOf() + val projectContributors = arrayListOf() + val classpathContributors = arrayListOf() + val templateContributors = arrayListOf() + val repoContributors = arrayListOf() + val compilerFlagContributors = arrayListOf() + val compilerInterceptors = arrayListOf() + val sourceDirectoriesInterceptors = arrayListOf() + val buildDirectoryInterceptors = arrayListOf() +// val runnerContributors = arrayListOf() + val testRunnerContributors = arrayListOf() + val classpathInterceptors = arrayListOf() + val compilerContributors = arrayListOf() + val docContributors = arrayListOf() + val sourceDirContributors = arrayListOf() + val testSourceDirContributors = arrayListOf() + val buildConfigFieldContributors = arrayListOf() + val taskContributors = arrayListOf() + val assemblyContributors = arrayListOf() + val incrementalAssemblyContributors = arrayListOf() + val incrementalTaskContributors = arrayListOf() + + // Not documented yet + val buildConfigContributors = arrayListOf() + val mavenIdInterceptors = arrayListOf() + val jvmFlagContributors = arrayListOf() + val localMavenRepoPathInterceptors = arrayListOf() + val buildListeners = arrayListOf() + val buildReportContributors = arrayListOf() + val docFlagContributors = arrayListOf() + + // Note: intentionally repeating them here even though they are defined by our base class so + // that this class always contains the full list of contributors and interceptors + override val testJvmFlagContributors = arrayListOf() + override val testJvmFlagInterceptors = arrayListOf() + + companion object { + /** + * Where plug-ins define their plug-in actors. + */ + val PLUGIN_XML = "META-INF/kobalt-plugin.xml" + + /** + * Kobalt's core XML file needs to be different from kobalt-plugin.xml because classloaders + * can put a plug-in's jar file in front of Kobalt's, which means we'll read + * that one instead of the core one. + */ + val PLUGIN_CORE_XML = "META-INF/kobalt-core-plugin.xml" + + /** + * Read Kobalt's own kobalt-plugin.xml. + */ + fun readKobaltPluginXml(): PluginInfo { + // Note: use forward slash here since we're looking up this file in a .jar file + val url = Kobalt::class.java.classLoader.getResource(PLUGIN_CORE_XML) + kobaltLog(2, "URL for core kobalt-plugin.xml: $url") + if (url != null) { + return readPluginXml(url.openConnection().inputStream) + } else { + throw AssertionError("Couldn't find $PLUGIN_XML") + } + } + + /** + * Read a general kobalt-plugin.xml. + */ + fun readPluginXml(ins: InputStream, pluginClassLoader: ClassLoader? = null, + classLoader: ClassLoader? = null): PluginInfo { + val jaxbContext = JAXBContext.newInstance(KobaltPluginXml::class.java) + val kobaltPlugin: KobaltPluginXml = jaxbContext.createUnmarshaller().unmarshal(ins) + as KobaltPluginXml + kobaltLog(2, "Parsed plugin XML file, found: " + kobaltPlugin.name) + val result = + try { + PluginInfo(kobaltPlugin, pluginClassLoader, classLoader) + } catch(ex: Exception) { + throw KobaltException("Couldn't create PluginInfo: " + ex.message, ex) + } + return result + } + + fun readPluginXml(s: String, pluginClassLoader: ClassLoader?, scriptClassLoader: ClassLoader? = null) + = readPluginXml(ByteArrayInputStream(s.toByteArray(Charsets.UTF_8)), pluginClassLoader, + scriptClassLoader) + } + + init { + val factory = if (xml.factoryClassName != null) { + Class.forName(xml.factoryClassName).newInstance() as IFactory + } else { + GuiceFactory() + } + + fun forName(className: String) : Class<*> { + fun loadClass(className: String, classLoader: ClassLoader?) : Class<*>? { + try { + return classLoader?.loadClass(className) + } catch(ex: ClassNotFoundException) { + return null + } + } + + val result = loadClass(className, classLoader) + ?: loadClass(className, pluginClassLoader) + ?: Class.forName(className) + + return result + } + + // + // Populate pluginInfo with what was found in Kobalt's own kobalt-plugin.xml + // + @Suppress("UNCHECKED_CAST") + xml.pluginActors?.className?.forEach { + with(factory.instanceOf(forName(it))) { + // Note: can't use "when" here since the same instance can implement multiple interfaces + if (this is IBuildConfigFieldContributor) buildConfigFieldContributors.add(this) + if (this is IBuildDirectoryInterceptor) buildDirectoryInterceptors.add(this) + if (this is IClasspathContributor) classpathContributors.add(this) + if (this is IClasspathInterceptor) classpathInterceptors.add(this) + if (this is ICompilerContributor) compilerContributors.add(this) + if (this is ICompilerFlagContributor) compilerFlagContributors.add(this) + if (this is ICompilerInterceptor) compilerInterceptors.add(this) + if (this is IDocContributor) docContributors.add(this) + if (this is ITemplateContributor) templateContributors.add(this) + if (this is IPlugin) plugins.add(this) + if (this is IProjectContributor) projectContributors.add(this) + if (this is IRepoContributor) repoContributors.add(this) +// if (this is IRunnerContributor) runnerContributors.add(this) + if (this is ISourceDirectoryContributor) sourceDirContributors.add(this) + if (this is ISourceDirectoryInterceptor) sourceDirectoriesInterceptors.add(this) + if (this is ITaskContributor) taskContributors.add(this) + if (this is ITestRunnerContributor) testRunnerContributors.add(this) + if (this is IMavenIdInterceptor) mavenIdInterceptors.add(this) + if (this is ITestSourceDirectoryContributor) testSourceDirContributors.add(this) + if (this is IBuildConfigContributor) buildConfigContributors.add(this) + if (this is IAssemblyContributor) assemblyContributors.add(this) + if (this is IIncrementalAssemblyContributor) incrementalAssemblyContributors.add(this) + if (this is IIncrementalTaskContributor) incrementalTaskContributors.add(this) + + // Not documented yet + if (this is ITestJvmFlagContributor) testJvmFlagContributors.add(this) + if (this is ITestJvmFlagInterceptor) testJvmFlagInterceptors.add(this) + if (this is IJvmFlagContributor) jvmFlagContributors.add(this) + if (this is ILocalMavenRepoPathInterceptor) localMavenRepoPathInterceptors.add(this) + if (this is IBuildListener) buildListeners.add(this) + if (this is IBuildReportContributor) buildReportContributors.add(this) + if (this is IDocFlagContributor) docFlagContributors.add(this) + } + } + } + + fun cleanUp() { + listOf(projectContributors, classpathContributors, templateContributors, + repoContributors, compilerFlagContributors, compilerInterceptors, + sourceDirectoriesInterceptors, buildDirectoryInterceptors, + /* runnerContributors, */ testRunnerContributors, classpathInterceptors, + compilerContributors, docContributors, sourceDirContributors, + testSourceDirContributors, buildConfigFieldContributors, + taskContributors, incrementalTaskContributors, assemblyContributors, + incrementalAssemblyContributors, testJvmFlagInterceptors, + jvmFlagContributors, localMavenRepoPathInterceptors, buildListeners, + buildReportContributors, docFlagContributors + ).forEach { + it.forEach(IPluginActor::cleanUpActors) + } + } + + /** + * Add the content of @param[pluginInfo] to this pluginInfo. + */ + fun addPluginInfo(pluginInfo: PluginInfo) { + kobaltLog(2, "Found new plug-in, adding it to pluginInfo: $pluginInfo") + + plugins.addAll(pluginInfo.plugins) + classpathContributors.addAll(pluginInfo.classpathContributors) + projectContributors.addAll(pluginInfo.projectContributors) + templateContributors.addAll(pluginInfo.templateContributors) + repoContributors.addAll(pluginInfo.repoContributors) + compilerFlagContributors.addAll(pluginInfo.compilerFlagContributors) + compilerInterceptors.addAll(pluginInfo.compilerInterceptors) + sourceDirectoriesInterceptors.addAll(pluginInfo.sourceDirectoriesInterceptors) + buildDirectoryInterceptors.addAll(pluginInfo.buildDirectoryInterceptors) +// runnerContributors.addAll(pluginInfo.runnerContributors) + testRunnerContributors.addAll(pluginInfo.testRunnerContributors) + classpathInterceptors.addAll(pluginInfo.classpathInterceptors) + compilerContributors.addAll(pluginInfo.compilerContributors) + docContributors.addAll(pluginInfo.docContributors) + sourceDirContributors.addAll(pluginInfo.sourceDirContributors) + buildConfigFieldContributors.addAll(pluginInfo.buildConfigFieldContributors) + taskContributors.addAll(pluginInfo.taskContributors) + incrementalTaskContributors.addAll(pluginInfo.incrementalTaskContributors) + testSourceDirContributors.addAll(pluginInfo.testSourceDirContributors) + mavenIdInterceptors.addAll(pluginInfo.mavenIdInterceptors) + buildConfigContributors.addAll(pluginInfo.buildConfigContributors) + assemblyContributors.addAll(pluginInfo.assemblyContributors) + incrementalAssemblyContributors.addAll(pluginInfo.incrementalAssemblyContributors) + testJvmFlagContributors.addAll(pluginInfo.testJvmFlagContributors) + testJvmFlagInterceptors.addAll(pluginInfo.testJvmFlagInterceptors) + jvmFlagContributors.addAll(pluginInfo.jvmFlagContributors) + localMavenRepoPathInterceptors.addAll(pluginInfo.localMavenRepoPathInterceptors) + buildListeners.addAll(pluginInfo.buildListeners) + buildReportContributors.addAll(pluginInfo.buildReportContributors) + docFlagContributors.addAll(pluginInfo.docFlagContributors) + } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/KobaltSettingsXml.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/KobaltSettingsXml.kt new file mode 100644 index 00000000..2eb5374c --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/KobaltSettingsXml.kt @@ -0,0 +1,167 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.BUILD_SCRIPT_CONFIG +import com.beust.kobalt.Constants +import com.beust.kobalt.ProxyConfig +import com.beust.kobalt.homeDir +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.kobaltLog +import com.google.inject.Inject +import com.google.inject.Singleton +import java.io.File +import java.io.FileInputStream +import javax.xml.bind.JAXBContext +import javax.xml.bind.annotation.XmlElement +import javax.xml.bind.annotation.XmlRootElement + +/** + * The root element of kobalt-settings.xml + */ +@XmlRootElement(name = "kobaltSettings") +class KobaltSettingsXml { + @XmlElement(name = "localCache") @JvmField + var localCache: String = homeDir(KFiles.KOBALT_DOT_DIR, "cache") + + @XmlElement(name = "localMavenRepo") @JvmField + var localMavenRepo: String = homeDir(KFiles.KOBALT_DOT_DIR, "localMavenRepo") + + @XmlElement(name = "defaultRepos") @JvmField + var defaultRepos: DefaultReposXml? = null + + @XmlElement(name = "proxies") @JvmField + var proxies: ProxiesXml? = null + + @XmlElement(name = "kobaltCompilerVersion") @JvmField + var kobaltCompilerVersion: String = Constants.KOTLIN_COMPILER_VERSION + + @XmlElement(name = "kobaltCompilerRepo") @JvmField + var kobaltCompilerRepo: String? = null + + @XmlElement(name = "kobaltCompilerFlags") @JvmField + var kobaltCompilerFlags: String? = null + + @XmlElement(name = "kobaltCompilerSeparateProcess") @JvmField + var kobaltCompilerSeparateProcess: Boolean = false + + @XmlElement(name = "autoUpdate") @JvmField + var autoUpdate: Boolean = false +} + +class ProxiesXml { + @XmlElement @JvmField + var proxy: List = arrayListOf() +} + +class ProxyXml { + @XmlElement @JvmField + var host: String = "" + + @XmlElement @JvmField + var port: String = "" + + @XmlElement @JvmField + var type: String = "" + + @XmlElement @JvmField + var nonProxyHosts: String = "" +} + +class DefaultReposXml { + @XmlElement @JvmField + var repo: List = arrayListOf() +} + +fun List.getProxy(protocol:String) = find { it.type==protocol } + +/** + * The object Kobalt refers to for settings. + */ +@Singleton +class KobaltSettings @Inject constructor(val xmlFile: KobaltSettingsXml) { + /** + * Location of the cache repository. + */ + var localCache = KFiles.makeDir(xmlFile.localCache) // var for testing + + /** + * Location of the local Maven repo for the task "publishToLocalMaven". + */ + val localMavenRepo = KFiles.makeDir(xmlFile.localMavenRepo) + + /** + * If true, Kobalt will automatically update itself if a new version is found. + */ + val autoUpdate = xmlFile.autoUpdate + + /** + * If true, the Kotlin compiler will always be launched in a separate JVM, even if the requested + * version is the same as the internal version. + */ + val kobaltCompilerSeparateProcess = xmlFile.kobaltCompilerSeparateProcess + + val defaultRepos = xmlFile.defaultRepos?.repo + + val proxyConfigs = with(xmlFile.proxies?.proxy) { + fun toIntOr(s: String, defaultValue: Int) = try { //TODO can be extracted to some global Utils + s.toInt() + } catch(e: NumberFormatException) { + defaultValue + } + + if (this != null) { + map { proxyXml-> + ProxyConfig(proxyXml.host, toIntOr(proxyXml.port, 0), proxyXml.type, proxyXml.nonProxyHosts) + } + } else { + null + } + } + + val kobaltCompilerVersion : String? + get() { + return if (BUILD_SCRIPT_CONFIG != null && BUILD_SCRIPT_CONFIG?.kobaltCompilerVersion != null) { + BUILD_SCRIPT_CONFIG?.kobaltCompilerVersion + } else { + xmlFile.kobaltCompilerVersion + } + } + + val kobaltCompilerRepo : String? + get() { + return if (BUILD_SCRIPT_CONFIG != null && BUILD_SCRIPT_CONFIG?.kobaltCompilerRepo != null) { + BUILD_SCRIPT_CONFIG?.kobaltCompilerRepo + } else { + xmlFile.kobaltCompilerRepo + } + } + + val kobaltCompilerFlags : String? + get() { + return if (BUILD_SCRIPT_CONFIG != null && BUILD_SCRIPT_CONFIG?.kobaltCompilerFlags != null) { + BUILD_SCRIPT_CONFIG?.kobaltCompilerFlags + } else { + xmlFile.kobaltCompilerFlags + } + } + + companion object { + val SETTINGS_FILE_PATH = KFiles.joinDir(KFiles.HOME_KOBALT_DIR.absolutePath, "settings.xml") + + fun readSettingsXml() : KobaltSettings { + val file = File(KobaltSettings.SETTINGS_FILE_PATH) + if (file.exists()) { + FileInputStream(file).use { + val jaxbContext = JAXBContext.newInstance(KobaltSettingsXml::class.java) + val xmlFile: KobaltSettingsXml = jaxbContext.createUnmarshaller().unmarshal(it) + as KobaltSettingsXml + val result = KobaltSettings(xmlFile) + return result + } + } else { + kobaltLog(2, "Couldn't find ${KobaltSettings.SETTINGS_FILE_PATH}, using default settings") + return KobaltSettings(KobaltSettingsXml()) + } + } + } + +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/KotlinJarFiles.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/KotlinJarFiles.kt new file mode 100644 index 00000000..123e8b76 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/KotlinJarFiles.kt @@ -0,0 +1,20 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.maven.DependencyManager +import com.google.inject.Inject +import java.io.File + +/** + * The jar files that Kotlin needs to run. + */ +class KotlinJarFiles @Inject constructor(val dependencyManager: DependencyManager, + val settings: KobaltSettings){ + private fun getKotlinCompilerJar(name: String): File { + val id = "org.jetbrains.kotlin:kotlin-$name:${settings.kobaltCompilerVersion}" + val dep = dependencyManager.create(id) + return dep.jarFile.get().absoluteFile + } + + val stdlib: File get() = getKotlinCompilerJar("stdlib") + val compiler: File get() = getKotlinCompilerJar("compiler-embeddable") +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/KotlinTestRunner.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/KotlinTestRunner.kt new file mode 100644 index 00000000..24e643d5 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/KotlinTestRunner.kt @@ -0,0 +1,21 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.api.KobaltContext +import com.beust.kobalt.api.Project + +/** + * KotlinTestRunner triggers if it finds a dependency on io.kotlintest but other than that, it just + * uses the regular JUnitRunner. + */ +class KotlinTestRunner : JUnitRunner() { + override val dependencyName = "io.kotlintest" + override val runnerName = "Kotlin Test" + + /** + * KotlinTestRunner runs tests in the init{} initializer, so ignore all the extra + * classes generated by the Kotlin compiler. + */ + override fun filterTestClasses(projet: Project, context: KobaltContext, classes: List) + = classes.filter { !it.contains("$") } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/ParallelLogger.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/ParallelLogger.kt new file mode 100644 index 00000000..519a94c9 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/ParallelLogger.kt @@ -0,0 +1,130 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.Args +import com.beust.kobalt.KobaltException +import com.beust.kobalt.misc.* +import com.google.inject.Inject +import com.google.inject.Singleton +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue + +interface ILogger { + fun log(tag: CharSequence, level: Int, message: CharSequence, newLine: Boolean = true) +} + +/** + * This class manages logs for parallel builds. These logs come from multiple projects interwoven as + * they are being scheduled on different threads. This class maintains a "current" project which has + * its logs always displayed instantaneously while logs from other projects are being stored for later display. + * Once the current project is done, this class will catch up all the finished project logs and then + * pick the next current project to be displayed live. + * + * Yes, this code was pretty painful to write and I'm pretty sure it can be made less ugly. + */ +@Singleton +class ParallelLogger @Inject constructor(val args: Args) : ILogger { + enum class Type { LOG, WARN, ERROR } + + class LogLine(val name: CharSequence? = null, val level: Int, val message: CharSequence, val type: Type, + val newLine: Boolean) + private val logLines = ConcurrentHashMap>() + + private val runningProjects = ConcurrentLinkedQueue() + var startTime: Long? = null + + fun onProjectStarted(name: String) { + if (startTime == null) { + startTime = System.currentTimeMillis() + } + runningProjects.add(name) + logLines[name] = arrayListOf() + if (currentName == null) { + currentName = name + } + } + + val stoppedProjects = ConcurrentHashMap() + + fun onProjectStopped(name: String) { + debug("onProjectStopped($name)") + stoppedProjects[name] = name + + if (name == currentName && runningProjects.any()) { + emptyProjectLog(name) + var nextProject = runningProjects.peek() + while (nextProject != null && stoppedProjects.containsKey(nextProject)) { + val sp = runningProjects.remove() + emptyProjectLog(sp) + nextProject = runningProjects.peek() + } + currentName = nextProject + } else { + debug("Non current project $name stopping, not doing anything") + } + } + + private fun debug(s: CharSequence) { + if (args.log >= 3) { + val time = System.currentTimeMillis() - startTime!! + kobaltLog(1, " ### [$time] $s") + } + } + + val LOCK = Any() + var currentName: String? = null + set(newName) { + field = newName + } + + private fun displayLine(ll: LogLine) { + val time = System.currentTimeMillis() - startTime!! + val m = (if (args.dev) "### [$time] " else "") + ll.message + when(ll.type) { + Type.LOG -> kobaltLog(ll.level, m, ll.newLine) + Type.WARN -> kobaltWarn(m) + Type.ERROR -> kobaltError(m) + } + } + + private fun emptyProjectLog(name: CharSequence?) { + val lines = logLines[name] + if (lines != null && lines.any()) { + debug("emptyProjectLog($name)") + lines.forEach { + displayLine(it) + } + lines.clear() + debug("Done emptyProjectLog($name)") +// logLines.remove(name) + } else if (lines == null) { + throw KobaltException("Didn't call onStartProject() for $name") + } + } + + private fun addLogLine(name: CharSequence, ll: LogLine) { + if (name != currentName) { + val list = logLines[name] ?: arrayListOf() + logLines[name] = list + list.add(ll) + } else { + emptyProjectLog(name) + displayLine(ll) + } + } + + override fun log(tag: CharSequence, level: Int, message: CharSequence, newLine: Boolean) { + if (args.sequential) { + kobaltLog(level, message, newLine) + } else { + addLogLine(tag, LogLine(tag, level, message, Type.LOG, newLine)) + } + } + + fun shutdown() { + runningProjects.forEach { + emptyProjectLog(it) + } + kobaltLog(1, "") + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/ParallelProjectRunner.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/ParallelProjectRunner.kt new file mode 100644 index 00000000..d98f0d8a --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/ParallelProjectRunner.kt @@ -0,0 +1,114 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.Args +import com.beust.kobalt.AsciiArt +import com.beust.kobalt.TaskResult +import com.beust.kobalt.api.ITask +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.api.Project +import com.beust.kobalt.api.ProjectBuildStatus +import com.beust.kobalt.misc.kobaltLog +import com.google.common.collect.ListMultimap +import com.google.common.collect.TreeMultimap +import java.util.concurrent.Callable + +/** + * Build the projects in parallel. + * + * The projects are sorted in topological order and then run by the DynamicGraphExecutor in background threads + * wherever appropriate. Inside a project, all the tasks are run sequentially. + */ +class ParallelProjectRunner(val tasksByNames: (Project) -> ListMultimap, + val dependsOn: TreeMultimap, + val reverseDependsOn: TreeMultimap, val runBefore: TreeMultimap, + val runAfter: TreeMultimap, + val alwaysRunAfter: TreeMultimap, val args: Args, val pluginInfo: PluginInfo, + val logger: ParallelLogger) + : BaseProjectRunner() { + override fun runProjects(taskInfos: List, projects: List) + : TaskManager .RunTargetResult { + class ProjectTask(val project: Project, val dryRun: Boolean) : Callable> { + override fun toString() = "[ProjectTask " + project.name + "]" + override fun hashCode() = project.hashCode() + override fun equals(other: Any?) : Boolean = + if (other is ProjectTask) other.project.name == project.name + else false + + override fun call(): TaskResult2 { + val context = Kobalt.context!! + runBuildListenersForProject(project, context, true) + val tasksByNames = tasksByNames(project) + val graph = createTaskGraph(project.name, taskInfos, tasksByNames, + dependsOn, reverseDependsOn, runBefore, runAfter, alwaysRunAfter, + ITask::name, + { task: ITask -> task.plugin.accept(project) }) + var lastResult = TaskResult() + logger.onProjectStarted(project.name) + context.logger.log(project.name, 1, AsciiArt.logBox("Building ${project.name}", indent = 5)) + while (graph.freeNodes.any()) { + val toProcess = graph.freeNodes + toProcess.forEach { node -> + val tasks = tasksByNames[node.name] + tasks.forEach { task -> + + runBuildListenersForTask(project, context, task.name, start = true) + logger.log(project.name, 1, + AsciiArt.taskColor(AsciiArt.horizontalSingleLine + " ${project.name}:${task.name}")) + val thisResult = if (dryRun) TaskResult2(true, value = task) else task.call() + if (lastResult.success) { + lastResult = thisResult + } + runBuildListenersForTask(project, context, task.name, start = false, + success = thisResult.success, testResult = thisResult.testResult) + } + } + graph.freeNodes.forEach { graph.removeNode(it) } + } + + logger.onProjectStopped(project.name) + runBuildListenersForProject(project, context, false, + if (lastResult.success) ProjectBuildStatus.SUCCESS else ProjectBuildStatus.FAILED) + + return TaskResult2(lastResult.success, errorMessage = lastResult.errorMessage, value = this) + } + + } + + val factory = object : IThreadWorkerFactory { + override fun createWorkers(nodes: Collection): List> { + val result = nodes.map { it -> + object: IWorker { + override val priority: Int get() = 0 + override val name: String get() = it.project.name + override fun call(): TaskResult2 { + val tr = it.call() + return tr + } + + } + } + return result + } + } + + val projectGraph = DynamicGraph().apply { + projects.forEach { project -> + addNode(ProjectTask(project, args.dryRun)) + project.allProjectDependedOn().forEach { + addEdge(ProjectTask(project, args.dryRun), ProjectTask(it, args.dryRun)) + } + } + } + + val executor = DynamicGraphExecutor(projectGraph, factory, 5) + kobaltLog(1, "Parallel build starting") + val taskResult = executor.run() + + logger.shutdown() + + if (! args.sequential) { + executor.dumpHistory() + } + return TaskManager.RunTargetResult(taskResult, emptyList()) + } +} diff --git a/src/main/kotlin/com/beust/kobalt/internal/PluginLoader.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/PluginLoader.kt similarity index 100% rename from src/main/kotlin/com/beust/kobalt/internal/PluginLoader.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/PluginLoader.kt diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/ProjectInfo.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/ProjectInfo.kt new file mode 100644 index 00000000..df865cec --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/ProjectInfo.kt @@ -0,0 +1,61 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.Variant +import com.beust.kobalt.api.BuildConfig +import com.beust.kobalt.api.BuildConfigField +import com.beust.kobalt.api.KobaltContext +import com.beust.kobalt.api.Project + +/** + * Data that is useful for projects to have but should not be specified in the DSL. + */ +interface IBuildConfig { + /** + * If at least one build config was found either on the project or the variant, this function + * will be used to generateAndSave the BuildConfig file with the correct language. + */ + fun generateBuildConfig(project: Project, context: KobaltContext, packageName: String, variant: Variant, + buildConfigs: List) : String + +} + +abstract class BaseBuildConfig : IBuildConfig { + abstract fun generate(field: BuildConfigField) : String + + /** + * Add all the fields found in 1) the field contributors 2) the build configs and 3) the default config + */ + fun generateCommonPart(project: Project, context: KobaltContext, buildConfigs: List) : List { + val result = arrayListOf() + + // Fields from the field contributors + result.addAll(generateFieldsFromContributors(project, context)) + + val seen = hashSetOf() + + // Fields from the build config + buildConfigs.forEach { + it.fields.forEach { field -> + result.add(generate(field.type, field.name, field.value)) + seen.add(field) + } + } + + // Add all the fields in the default config that haven't been added yet + project.defaultConfig?.let { + it.fields.filter { ! seen.contains(it) }.forEach { + result.add(generate(it.type, it.name, it.value)) + } + } + return result + } + + fun generate(type: String, name: String, value: Any) = generate(BuildConfigField(type, name, value)) + + fun generateFieldsFromContributors(project: Project, context: KobaltContext) + = context.pluginInfo.buildConfigFieldContributors.flatMap { + it.fieldsFor(project, context) + }.map { + generate(it) + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/SequentialProjectRunner.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/SequentialProjectRunner.kt new file mode 100644 index 00000000..ec99b723 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/SequentialProjectRunner.kt @@ -0,0 +1,96 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.Args +import com.beust.kobalt.AsciiArt +import com.beust.kobalt.TaskResult +import com.beust.kobalt.api.ITask +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.api.Project +import com.beust.kobalt.api.ProjectBuildStatus +import com.beust.kobalt.misc.Strings +import com.beust.kobalt.misc.kobaltError +import com.google.common.collect.ListMultimap +import com.google.common.collect.TreeMultimap +import java.util.* + +/** + * Build the projects in parallel. + * + * The projects are sorted in topological order and then run by the DynamicGraphExecutor in a single thread. + */ +class SequentialProjectRunner(val tasksByNames: (Project) -> ListMultimap, + val dependsOn: TreeMultimap, + val reverseDependsOn: TreeMultimap, val runBefore: TreeMultimap, + val runAfter: TreeMultimap, + val alwaysRunAfter: TreeMultimap, val args: Args, val pluginInfo: PluginInfo) + : BaseProjectRunner() { + + override fun runProjects(taskInfos: List, projects: List) + : TaskManager.RunTargetResult { + var result = TaskResult() + val failedProjects = hashSetOf() + val messages = Collections.synchronizedList(arrayListOf()) + + val context = Kobalt.context!! + + projects.forEach { project -> + val projectName = project.name + fun klog(level: Int, message: String) = context.logger.log(projectName, level, message) + klog(1, AsciiArt.logBox("Building $projectName", indent = 5)) + + // Does the current project depend on any failed projects? + val fp = project.allProjectDependedOn().filter { failedProjects.contains(it.name) }.map(Project::name) + + if (fp.size > 0) { + klog(2, "Marking project $projectName as skipped") + failedProjects.add(project.name) + runBuildListenersForProject(project, context, false, ProjectBuildStatus.SKIPPED) + kobaltError("Not building project ${project.name} since it depends on failed " + + Strings.pluralize(fp.size, "project") + + " " + fp.joinToString(",")) + } else { + runBuildListenersForProject(project, context, true) + + // There can be multiple tasks by the same name (e.g. PackagingPlugin and AndroidPlugin both + // define "install"), so use a multimap + val tasksByNames = tasksByNames(project) + + klog(3, "Tasks:") + tasksByNames.keys().forEach { + klog(3, " $it: " + tasksByNames.get(it)) + } + + val graph = createTaskGraph(project.name, taskInfos, tasksByNames, + dependsOn, reverseDependsOn, runBefore, runAfter, alwaysRunAfter, + ITask::name, + { task: ITask -> task.plugin.accept(project) }) + + // + // Now that we have a full graph, run it + // + klog(2, "About to run graph:\n ${graph.dump()} ") + + val factory = object : IThreadWorkerFactory { + override fun createWorkers(nodes: Collection) + = nodes.map { TaskWorker(listOf(it), args.dryRun, pluginInfo) } + } + + val executor = DynamicGraphExecutor(graph, factory) + val thisResult = executor.run() + if (! thisResult.success) { + klog(2, "Marking project ${project.name} as failed") + failedProjects.add(project.name) + } + + runBuildListenersForProject(project, context, false, + if (thisResult.success) ProjectBuildStatus.SUCCESS else ProjectBuildStatus.FAILED) + + if (result.success) { + result = thisResult + } + } + } + + return TaskManager.RunTargetResult(result, messages) + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/SourceSet.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/SourceSet.kt new file mode 100644 index 00000000..7d606906 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/SourceSet.kt @@ -0,0 +1,22 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.KobaltException +import com.beust.kobalt.api.Project +import com.beust.kobalt.misc.KFiles + +enum class SourceSet(val outputDir: String) { + MAIN(KFiles.CLASSES_DIR), + TEST(KFiles.TEST_CLASSES_DIR); + + fun correctSourceSet(project: Project) = when(this) { + SourceSet.MAIN -> project.sourceDirectories + SourceSet.TEST -> project.sourceDirectoriesTest + else -> unknown(this) + } + + companion object { + fun of(isTest: Boolean) = if (isTest) TEST else MAIN + private fun unknown(sourceSet: SourceSet) : Nothing = throw KobaltException("Unknown source set: $sourceSet") + } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/SpekRunner.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/SpekRunner.kt new file mode 100644 index 00000000..0c1ddad6 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/SpekRunner.kt @@ -0,0 +1,11 @@ +package com.beust.kobalt.internal + +/** + * SpekRunner triggers if it finds a dependency on org.jetbrains.spek but other than that, it just + * uses the regular JUnitRunner. + */ +class SpekRunner : JUnitRunner() { + override val dependencyName = "org.jetbrains.spek" + override val runnerName = "Spek" +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/TaskManager.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/TaskManager.kt new file mode 100644 index 00000000..99aa7624 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/TaskManager.kt @@ -0,0 +1,334 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.* +import com.beust.kobalt.api.* +import com.beust.kobalt.api.annotation.IncrementalTask +import com.beust.kobalt.api.annotation.Task +import com.beust.kobalt.misc.Topological +import com.beust.kobalt.misc.kobaltLog +import com.google.common.collect.ArrayListMultimap +import com.google.common.collect.ListMultimap +import com.google.common.collect.TreeMultimap +import java.lang.reflect.Method +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TaskManager @Inject constructor(val args: Args, + val incrementalManagerFactory: IncrementalManager.IFactory, + val kobaltLog: ParallelLogger) { + private val dependsOn = TreeMultimap.create() + private val reverseDependsOn = TreeMultimap.create() + private val runBefore = TreeMultimap.create() + private val runAfter = TreeMultimap.create() + private val alwaysRunAfter = TreeMultimap.create() + + /** + * Dependency: task2 depends on task 1. + */ + fun dependsOn(task1: String, task2: String) = dependsOn.put(task2, task1) + + /** + * Dependency: task2 depends on task 1. + */ + fun reverseDependsOn(task1: String, task2: String) = reverseDependsOn.put(task2, task1) + + /** + * Ordering: task1 runs before task 2. + */ + fun runBefore(task1: String, task2: String) = runBefore.put(task2, task1) + + /** + * Ordering: task2 runs after task 1. + */ + fun runAfter(task1: String, task2: String) = runAfter.put(task1, task2) + + /** + * Wrapper task: task2 runs after task 1. + */ + fun alwaysRunAfter(task1: String, task2: String) = alwaysRunAfter.put(task2, task1) + + data class TaskInfo(val id: String) { + constructor(project: String, task: String) : this(project + ":" + task) + + val project: String? + get() = if (id.contains(':')) id.split(':')[0] else null + val taskName: String + get() = if (id.contains(':')) id.split(':')[1] else id + + fun matches(projectName: String) = project == null || project == projectName + + override fun toString() = id + } + + class RunTargetResult(val taskResult: TaskResult, val timings: List) + + /** + * @return the list of tasks available for the given project. + * + * There can be multiple tasks by the same name (e.g. PackagingPlugin and AndroidPlugin both + * define "install"), so return a multimap. + */ + fun tasksByNames(project: Project): ListMultimap { + return ArrayListMultimap.create().apply { + annotationTasks.filter { + it.project.name == project.name + }.forEach { + put(it.name, it) + } + } + } + +// @Inject +// lateinit var pluginInfo: PluginInfo + + fun runTargets(passedTaskNames: List, allProjects: List): RunTargetResult { + // Check whether tasks passed at command line exist + passedTaskNames.forEach { + if (!hasTask(TaskInfo(it))) + throw KobaltException("Unknown task: $it") + } + + val pluginInfo = Kobalt.INJECTOR.getInstance(PluginInfo::class.java) + var taskInfos = calculateDependentTaskNames(passedTaskNames, allProjects) + + // Remove non existing tasks (e.g. dynamic task defined for a single project) + taskInfos = taskInfos.filter { hasTask(it) } + + val projectsToRun = findProjectsToRun(taskInfos, allProjects) + val projectRunner = + if (args.sequential) { + SequentialProjectRunner({ p -> tasksByNames(p) }, dependsOn, + reverseDependsOn, runBefore, runAfter, alwaysRunAfter, args, pluginInfo) + } else { + ParallelProjectRunner({ p -> tasksByNames(p) }, dependsOn, + reverseDependsOn, runBefore, runAfter, alwaysRunAfter, args, pluginInfo, kobaltLog) + } + return projectRunner.runProjects(taskInfos, projectsToRun) + } + + /** + * Determine which projects to run based on the request tasks. Also make sure that all the requested projects + * exist. + */ + private fun findProjectsToRun(taskInfos: List, projects: List) : List { + + // Validate projects + val result = LinkedHashSet() + val projectMap = HashMap().apply { + projects.forEach { put(it.name, it)} + } + + // Extract all the projects we need to run from the tasks + taskInfos.forEach { + val p = it.project + if (p != null && ! projectMap.containsKey(p)) { + throw KobaltException("Unknown project: ${it.project}") + } + result.add(projectMap[it.project]!!) + } + + // If at least one task didn't specify a project, run them all + return if (result.any()) result.toList() else projects + } + + class ProfilerInfo(val taskName: String, val durationMillis: Long) + + /** + * If the user wants to run a single task on a single project (e.g. "kobalt:assemble"), we need to + * see if that project depends on others and if it does, compile these projects first. This + * function returns all these task names (including the dependent ones). + */ + fun calculateDependentTaskNames(taskNames: List, projects: List): List { + return taskNames.flatMap { calculateDependentTaskNames(it, projects) } + } + + private fun calculateDependentTaskNames(taskName: String, projects: List): List { + fun sortProjectsTopologically(projects: List) : List { + val topological = Topological().apply { + projects.forEach { project -> + addNode(project) + project.allProjectDependedOn().forEach { + addEdge(project, it) + } + } + } + val sortedProjects = topological.sort() + return sortedProjects + } + + val ti = TaskInfo(taskName) + if (ti.project == null) { + val result = sortProjectsTopologically(projects).map { TaskInfo(it.name, taskName) } + return result + } else { + val rootProject = projects.find { it.name == ti.project }!! + val allProjects = DynamicGraph.transitiveClosure(rootProject, Project::allProjectDependedOn) + val sortedProjects = sortProjectsTopologically(allProjects) + val sortedMaps = sortedProjects.map { TaskInfo(it.name, "compile")} + val result = sortedMaps.subList(0, sortedMaps.size - 1) + listOf(ti) + return result + } + } + + ///// + // Manage the tasks + // + + // Both @Task and @IncrementalTask get stored as a TaskAnnotation so they can be treated uniformly. + // They only differ in the way they are invoked (see below) + private val taskAnnotations = arrayListOf() + + class TaskAnnotation(val method: Method, val plugin: IPlugin, val name: String, val description: String, + val group: String, + val dependsOn: Array, val reverseDependsOn: Array, + val runBefore: Array, val runAfter: Array, + val alwaysRunAfter: Array, + val callable: (Project) -> TaskResult) { + override fun toString() = "[TaskAnnotation $name]" + } + + /** + * Invoking a @Task means simply calling the method and returning its returned TaskResult. + */ + fun toTaskAnnotation(method: Method, plugin: IPlugin, ta: Task) + = TaskAnnotation(method, plugin, ta.name, ta.description, ta.group, ta.dependsOn, ta.reverseDependsOn, + ta.runBefore, ta.runAfter, ta.alwaysRunAfter, + { project -> + Kobalt.context?.variant = Variant() + method.invoke(plugin, project) as TaskResult + }) + + /** + * Invoking an @IncrementalTask means invoking the method and then deciding what to do based on the content + * of the returned IncrementalTaskInfo. + */ + fun toTaskAnnotation(method: Method, plugin: IPlugin, ta: IncrementalTask) + = TaskAnnotation(method, plugin, ta.name, ta.description, ta.group, ta.dependsOn, ta.reverseDependsOn, + ta.runBefore, ta.runAfter, ta.alwaysRunAfter, + incrementalManagerFactory.create().toIncrementalTaskClosure(ta.name, { project -> + method.invoke(plugin, project) as IncrementalTaskInfo + }, Variant())) + + /** Tasks annotated with @Task or @IncrementalTask */ + val annotationTasks = arrayListOf() + + /** Tasks provided by ITaskContributors */ + val dynamicTasks = arrayListOf() + + fun addAnnotationTask(plugin: IPlugin, method: Method, annotation: Task) = + taskAnnotations.add(toTaskAnnotation(method, plugin, annotation)) + + fun addIncrementalTask(plugin: IPlugin, method: Method, annotation: IncrementalTask) = + taskAnnotations.add(toTaskAnnotation(method, plugin, annotation)) + + /** + * Turn all the static and dynamic tasks into plug-in tasks, which are then suitable to be executed. + */ + fun computePluginTasks(projects: List) { + installAnnotationTasks(projects) + installDynamicTasks() + } + + private fun installDynamicTasks() { + dynamicTasks.forEach { task -> + addTask(task.plugin, task.project, task.name, task.doc, task.group, + task.dependsOn, task.reverseDependsOn, task.runBefore, task.runAfter, task.alwaysRunAfter, + task.closure) + } + } + + private fun installAnnotationTasks(projects: List) { + taskAnnotations.forEach { staticTask -> + val method = staticTask.method + + val methodName = method.declaringClass.toString() + "." + method.name + kobaltLog(3, " Found task:${staticTask.name} method: $methodName") + + val plugin = staticTask.plugin + projects.filter { plugin.accept(it) }.forEach { project -> + addAnnotationTask(plugin, project, staticTask, staticTask.callable) + } + } + } + + private fun addAnnotationTask(plugin: IPlugin, project: Project, annotation: TaskAnnotation, + task: (Project) -> TaskResult) { + addTask(plugin, project, annotation.name, annotation.description, annotation.group, + annotation.dependsOn.toList(), annotation.reverseDependsOn.toList(), + annotation.runBefore.toList(), annotation.runAfter.toList(), + annotation.alwaysRunAfter.toList(), task) + } + + fun addTask(plugin: IPlugin, project: Project, name: String, description: String = "", group: String, + dependsOn: List = listOf(), + reverseDependsOn: List = listOf(), + runBefore: List = listOf(), + runAfter: List = listOf(), + alwaysRunAfter: List = listOf(), + task: (Project) -> TaskResult) { + annotationTasks.add( + object : BasePluginTask(plugin, name, description, group, project) { + override fun call(): TaskResult2 { + val taskResult = task(project) + return TaskResult2(taskResult.success, errorMessage = taskResult.errorMessage, value = this, + testResult = taskResult.testResult) + } + }) + dependsOn.forEach { dependsOn(it, name) } + reverseDependsOn.forEach { reverseDependsOn(it, name) } + runBefore.forEach { runBefore(it, name) } + runAfter.forEach { runAfter(it, name) } + alwaysRunAfter.forEach { alwaysRunAfter(it, name) } + } + + fun hasTask(ti: TaskInfo): Boolean { + val taskName = ti.taskName + val project = ti.project + return annotationTasks.any { taskName == it.name && (project == null || project == it.project.name) } + } + + /** + * Invoked by the server whenever it's done processing a command so the state can be reset for the next command. + */ + fun cleanUp() { + annotationTasks.clear() + dynamicTasks.clear() + taskAnnotations.clear() + } + + // + // Manage the tasks + ///// +} + +class TaskWorker(val tasks: List, val dryRun: Boolean, val pluginInfo: PluginInfo) : IWorker { + + override fun call() : TaskResult2 { + if (tasks.size > 0) { + tasks[0].let { + kobaltLog(1, AsciiArt.taskColor(AsciiArt.horizontalSingleLine + " ${it.project.name}:${it.name}")) + } + } + var success = true + val errorMessages = arrayListOf() + val context = Kobalt.context!! + tasks.forEach { + val name = it.project.name + ":" + it.name + BaseProjectRunner.runBuildListenersForTask(it.project, context, name, start = true) + val tr = if (dryRun) TaskResult() else it.call() + BaseProjectRunner.runBuildListenersForTask(it.project, context, name, start = false, success = tr.success) + success = success and tr.success + tr.errorMessage?.let { + errorMessages.add(it) + } + } + return TaskResult2(success, errorMessage = errorMessages.joinToString("\n"), value = tasks[0]) + } + +// override val timeOut : Long = 10000 + + override val priority: Int = 0 + override val name: String get() = "[Taskworker " + tasks.map(ITask::toString).joinToString(",") + "]" +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/TestNgRunner.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/TestNgRunner.kt new file mode 100644 index 00000000..f4ee96f8 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/TestNgRunner.kt @@ -0,0 +1,268 @@ +package com.beust.kobalt.internal + +import com.beust.kobalt.AsciiArt +import com.beust.kobalt.TestConfig +import com.beust.kobalt.TestResult +import com.beust.kobalt.api.IClasspathDependency +import com.beust.kobalt.api.KobaltContext +import com.beust.kobalt.api.Project +import com.beust.kobalt.maven.aether.AetherDependency +import com.beust.kobalt.misc.* +import org.testng.remote.RemoteArgs +import org.testng.remote.strprotocol.JsonMessageSender +import org.testng.remote.strprotocol.MessageHelper +import org.testng.remote.strprotocol.MessageHub +import org.testng.remote.strprotocol.TestResultMessage +import org.w3c.dom.Attr +import org.w3c.dom.NodeList +import org.xml.sax.InputSource +import java.io.File +import java.io.FileReader +import java.io.IOException +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.xpath.XPathConstants +import javax.xml.xpath.XPathFactory + +class TestNgRunner : GenericTestRunner() { + + override val mainClass = "org.testng.TestNG" + override val dependencyName = "testng" + override val annotationPackage = "org.testng" + override val runnerName = "TestNG" + + private fun defaultOutputWithoutProjectDir(project: Project) + = KFiles.joinDir(project.buildDirectory, "test-output") + private fun defaultOutput(project: Project) + = KFiles.joinDir(project.directory, project.buildDirectory, "test-output") + + override fun args(project: Project, context: KobaltContext, classpath: List, + testConfig: TestConfig) = arrayListOf().apply { + + if (KobaltLogger.isQuiet) { + add("-log") + add("0") + } + + if (testConfig.testArgs.none { it == "-d" }) { + add("-d") + // Don't include the project directory here since the generic runner will cd to that directory before + // running the tests + add(defaultOutputWithoutProjectDir(project)) + } + + if (testConfig.testArgs.size == 0) { + // No arguments, so we'll do it ourselves. Either testng.xml or the list of classes + val testngXml = File(project.directory, KFiles.joinDir("src", "test", "resources", "testng.xml")) + if (testngXml.exists()) { + add(testngXml.absolutePath) + } else { + val testClasses = findTestClasses(project, context, testConfig) + if (testClasses.isNotEmpty()) { + addAll(testConfig.testArgs) + + add("-testclass") + add(testClasses.joinToString(",")) + } else { + if (!testConfig.isDefault) warn("Couldn't find any test classes for ${project.name}") + // else do nothing: since the user didn't specify an explicit test{} directive, not finding + // any test sources is not a problem + } + } + } else { + addAll(testConfig.testArgs) + } + } + + /** + * Extract test results from testng-results.xml and initialize shortMessage. + */ + override fun onFinish(project: Project) { + File(defaultOutput(project), "testng-results.xml").let { file -> + val ins = InputSource(FileReader(file)) + val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(ins) + + val root = doc.documentElement + var failed = 0 + var skipped = 0 + var passed = 0 + val xp = XPathFactory.newInstance().newXPath() + val testMethods = xp.compile("/testng-results/suite/test/class/test-method[@status='FAIL']") + .evaluate(doc, XPathConstants.NODESET) + as NodeList + val failedMethods = arrayListOf() + repeat(testMethods.length) { + val tm = testMethods.item(it) + failedMethods.add(tm.attributes.getNamedItem("signature").textContent) + } + repeat(root.attributes.length) { + val attribute = root.attributes.item(it) + if (attribute is Attr) when (attribute.name) { + "failed" -> failed = Integer.parseInt(attribute.value) + "skipped" -> skipped = Integer.parseInt(attribute.value) + "passed" -> passed = Integer.parseInt(attribute.value) + } + } + + if (failed == 0) { + shortMessage = "$passed tests" + } else if (failed > 0) { + shortMessage = "$failed failed" + (if (skipped > 0) ", $skipped skipped" else "") + " tests" + longMessage = "Failed tests:\n " + failedMethods.joinToString("\n ") + } + } + } + + val VERSION_6_10 = StringVersion("6.10") + + fun _runTests(project: Project, context: KobaltContext, classpath: List, +// override fun runTests(project: Project, context: KobaltContext, classpath: List, + configName: String): TestResult { + + val testConfig = project.testConfigs.firstOrNull { it.name == configName } + + if (testConfig != null) { + context.logger.log(project.name, 1, "Running enhanced TestNG runner") + + val testngDependency = (project.testDependencies.filter { it.id.contains("testng") } + .firstOrNull() as AetherDependency).version + val versions = findRemoteRunnerVersion(testngDependency) + val useOldRunner = System.getProperty("testng.oldRunner") != null + val result = + if (versions != null && ! useOldRunner) { + context.logger.log(project.name, 1, "Modern TestNG, displaying colors") + displayPrettyColors(project, context, classpath, testConfig, versions) + } else { + context.logger.log(project.name, 1, "Older TestNG ($testngDependency), using the old runner") + super.runTests(project, context, classpath, configName) + } + return result + } else { + return TestResult(true) + } + } + + private fun findRemoteRunnerVersion(testngVersion: String) : Pair? { + val tng = StringVersion(testngVersion) + val result = + if (tng >= VERSION_6_10) Pair(testngVersion, "testng-remote6_10") + else if (tng >= StringVersion("6.9.10")) Pair("6.9.10", "testng-remote6_9_10") + else if (tng >= StringVersion("6.9.7")) Pair("6.9.7", "testng-remote6_9_7") + else if (tng >= StringVersion("6.5.1")) Pair("6.5.1", "testng-remote6_5_0") + else if (tng >= StringVersion("6.0")) Pair("6.0", "testng-remote6_0") + else null + return result + } + + private fun displayPrettyColors(project: Project, context: KobaltContext, + classpath: List, testConfig: TestConfig, versions: Pair) + : TestResult { + val port = 2345 +// launchRemoteServer(project, context, classpath, testConfig, versions, port) + + val mh = MessageHub(JsonMessageSender("localhost", port, true)) + mh.setDebug(true) + mh.initReceiver() + val passed = arrayListOf() + + data class FailedTest(val method: String, val cls: String, val stackTrace: String) + + val failed = arrayListOf() + val skipped = arrayListOf() + + fun d(n: Int, color: String) + = AsciiArt.wrap(String.format("%4d", n), color) + + fun red(s: String) = AsciiArt.wrap(s, AsciiArt.RED) + fun green(s: String) = AsciiArt.wrap(s, AsciiArt.GREEN) + fun yellow(s: String) = AsciiArt.wrap(s, AsciiArt.YELLOW) + + try { + var message = mh.receiveMessage() + kobaltLog(1, "") + kobaltLog(1, green("PASSED") + " | " + red("FAILED") + " | " + yellow("SKIPPED")) + while (message != null) { + message = mh.receiveMessage() + if (message is TestResultMessage) { + when (message.result) { + MessageHelper.PASSED_TEST -> passed.add(message.name) + MessageHelper.FAILED_TEST -> failed.add(FailedTest(message.testClass, + message.method, message.stackTrace)) + MessageHelper.SKIPPED_TEST -> skipped.add(message.name) + } + } + if (!KobaltLogger.isQuiet) { + print("\r " + d(passed.size, AsciiArt.GREEN) + + " | " + d(failed.size, AsciiArt.RED) + + " | " + d(skipped.size, AsciiArt.YELLOW)) + } + } + } catch(ex: IOException) { + kobaltLog(1, "Exception: ${ex.message}") + } + kobaltLog(1, "\nPassed: " + passed.size + ", Failed: " + failed.size + ", Skipped: " + skipped.size) + failed.forEach { + val top = it.stackTrace.substring(0, it.stackTrace.indexOf("\n")) + kobaltLog(1, " " + it.cls + "." + it.method + "\n " + top) + } + return TestResult(failed.isEmpty() && skipped.isEmpty()) + } + + fun launchRemoteServer(project: Project, context: KobaltContext, classpath: List, + testConfig: TestConfig, versions: Pair, port: Int) { + val testngVersion = versions.first + val remoteRunnerVersion = versions.second + val dep = with(context.dependencyManager) { + val jf = create("org.testng.testng-remote:testng-remote:1.3.0") + val tr = create("org.testng.testng-remote:$remoteRunnerVersion:1.3.0") + val testng = create("org.testng:testng:6.11") + transitiveClosure(kotlin.collections.listOf(jf, tr /*, testng */)) + } + + val cp = (classpath + dep).distinct().map { it.jarFile.get() } + .joinToString(File.pathSeparator) + val calculatedArgs = args(project, context, classpath, testConfig) + + val jvmArgs = arrayListOf("-classpath", cp) + if (testConfig.jvmArgs.any()) { + jvmArgs.addAll(testConfig.jvmArgs) + } + val remoteArgs = listOf( + "org.testng.remote.RemoteTestNG", + "-serport", port.toString(), + "-version", testngVersion, + "-dontexit", + RemoteArgs.PROTOCOL, + "json") + + val passedArgs = jvmArgs + remoteArgs + calculatedArgs + + Thread { + runCommand { + command = "java" + directory = File(project.directory) + args = passedArgs + } + }.start() + +// Thread { +// val args2 = arrayOf("-serport", port.toString(), "-dontexit", RemoteArgs.PROTOCOL, "json", +// "-version", "6.10", +// "src/test/resources/testng.xml") +// RemoteTestNG.main(args2) +// }.start() + } +} + +fun main(args: Array) { + fun d(n: Int, color: String) + = AsciiArt.wrap(String.format("%4d", n), color) + + if (!KobaltLogger.isQuiet) { + println("PASSED | FAILED | SKIPPED") + repeat(20) { i -> + print("\r " + d(i, AsciiArt.GREEN) + " | " + d(i * 2, AsciiArt.RED) + " | " + d(i, AsciiArt.YELLOW)) + Thread.sleep(500) + } + println("") + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/build/BuildFile.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/build/BuildFile.kt new file mode 100644 index 00000000..a2eb91e9 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/build/BuildFile.kt @@ -0,0 +1,14 @@ +package com.beust.kobalt.internal.build + +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +/** + * Sometimes, build files are moved to temporary files, so we give them a specific name for clarity. + * @param path is the path where that file was moved, @param realPath is where the actual file is. + */ +class BuildFile(val path: Path, val name: String, val realPath: Path = path) { + fun exists() : Boolean = Files.exists(path) + + val directory : File get() = path.toFile().parentFile +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/build/BuildSources.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/build/BuildSources.kt new file mode 100644 index 00000000..78425f18 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/build/BuildSources.kt @@ -0,0 +1,61 @@ +package com.beust.kobalt.internal.build + +import com.beust.kobalt.homeDir +import java.io.File +import java.nio.file.* +import java.nio.file.attribute.BasicFileAttributes + +/** + * The abstraction to represent a directory that contains source files. @param{root} is typically + * the root of the project and build files are searched under root/kobalt/src/ *kt. + */ +interface IBuildSources { + fun findSourceFiles() : List + val root: File + fun exists(): Boolean +} + +class SingleFileBuildSources(val file: File) : IBuildSources { + override fun exists() = file.exists() + override fun findSourceFiles() = listOf(file) + override val root: File = file.parentFile.parentFile.parentFile + override fun toString() : String = file.path +} + +class BuildSources(val file: File = File("")) : IBuildSources { + + override val root = file + + override fun findSourceFiles() : List { + return findBuildFiles(listOf(file)) + } + + override fun exists() = findSourceFiles().isNotEmpty() + + override fun toString() = "{BuildSources " + findSourceFiles().joinToString(", ") + "}" + + fun findBuildFiles(roots: List) : List { + val result = arrayListOf() + roots.forEach { file -> + Files.walkFileTree(Paths.get(file.path), object : SimpleFileVisitor() { + override fun preVisitDirectory(dir: Path?, attrs: BasicFileAttributes?): FileVisitResult { + if (dir != null) { + val path = dir.toFile() + if (path.name == "src" && path.parentFile.name == "kobalt") { + val sources = path.listFiles().filter { it.name.endsWith(".kt") } + result.addAll(sources) + } + } + + return FileVisitResult.CONTINUE + } + }) + } + return result + } +} + +fun main(args: Array) { + val sources = BuildSources(File(homeDir("kotlin/kobalt"))).findSourceFiles() + println("sources: " + sources) +} \ No newline at end of file diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/build/VersionCheckTimestampFile.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/build/VersionCheckTimestampFile.kt new file mode 100644 index 00000000..ea0ff924 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/build/VersionCheckTimestampFile.kt @@ -0,0 +1,22 @@ +package com.beust.kobalt.internal.build + +import com.beust.kobalt.misc.KFiles +import java.io.File +import java.time.Instant + +class VersionCheckTimestampFile { + companion object { + private val KOBALT_VERSION_CHECK_TIMESTAMP_FILE = "versionCheckTimestamp.txt" + private val CHECK_TIMESTAMP_FILE = File(KFiles.KOBALT_DOT_DIR, KOBALT_VERSION_CHECK_TIMESTAMP_FILE) + + fun updateTimestamp(timestamp: Instant) = KFiles.saveFile(CHECK_TIMESTAMP_FILE, timestamp.toString()) + + val timestamp : Instant + get() = if (CHECK_TIMESTAMP_FILE.exists()) { + Instant.parse(CHECK_TIMESTAMP_FILE.readText()) + } else { + updateTimestamp(Instant.MIN) + Instant.MIN + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/beust/kobalt/internal/build/VersionFile.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/build/VersionFile.kt similarity index 100% rename from src/main/kotlin/com/beust/kobalt/internal/build/VersionFile.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/build/VersionFile.kt diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/eventbus/Events.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/eventbus/Events.kt new file mode 100644 index 00000000..a57be9a2 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/eventbus/Events.kt @@ -0,0 +1,5 @@ +package com.beust.kobalt.internal.eventbus + +import org.eclipse.aether.repository.ArtifactRepository + +class ArtifactDownloadedEvent(val artifactId: String, val repository: ArtifactRepository) diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/remote/KobaltServer.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/remote/KobaltServer.kt new file mode 100644 index 00000000..9c2935c5 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/remote/KobaltServer.kt @@ -0,0 +1,46 @@ +package com.beust.kobalt.internal.remote + +import com.beust.kobalt.api.Project +import com.google.gson.JsonObject + +/** + * All commands implement this interface. + */ +interface ICommand { + /** + * The name of this command. + */ + val name: String + + /** + * Run this command based on the information received from the client. When done, use + * the sender object to send back a response. + * @param initCallback The string is a path to the build file + */ + fun run(sender: ICommandSender, received: JsonObject, initCallback: (String) -> List) + + fun toCommandData(data: String, error: String? = null) = CommandData(name, data, error) +} + +/** + * Passed to a command in its `run` method so it can send information back to the caller. + * @param commandData The string content that will be sent in the "data" field. + */ +interface ICommandSender { + fun sendData(commandData: CommandData) +} + +/** + * The JSON payload that commands exchange follow the following pattern: + * { + * name: "nameOfTheCommand" + * data: a JSON string containing the payload itself + * } + * This allows commands to be tested for their name first, after which each command can + * decode its own specific payload by parsing the JSON in the "data" field and mapping + * it into a Kotlin *Data class. The downside of this approach is a double parsing, + * but since the data part is parsed as a string first, this is probably not a huge deal. + */ +class CommandData(val name: String, val data: String?, val error: String? = null) + + diff --git a/src/main/kotlin/com/beust/kobalt/internal/remote/PingCommand.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/remote/PingCommand.kt similarity index 85% rename from src/main/kotlin/com/beust/kobalt/internal/remote/PingCommand.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/remote/PingCommand.kt index ad9b2f21..555ded39 100644 --- a/src/main/kotlin/com/beust/kobalt/internal/remote/PingCommand.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/remote/PingCommand.kt @@ -1,6 +1,6 @@ package com.beust.kobalt.internal.remote -import com.beust.kobalt.misc.log +import com.beust.kobalt.api.Project import com.google.gson.Gson import com.google.gson.JsonObject @@ -15,7 +15,7 @@ import com.google.gson.JsonObject class PingCommand() : ICommand { override val name = "ping" - override fun run(sender: ICommandSender, received: JsonObject) { + override fun run(sender: ICommandSender, received: JsonObject, initCallback: (String) -> List) { sender.sendData(toCommandData(Gson().toJson(PingData(received.toString())))) } diff --git a/src/main/kotlin/com/beust/kobalt/wrapper/ParentLastClassLoader.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/kotlin/ParentLastClassLoader.kt similarity index 86% rename from src/main/kotlin/com/beust/kobalt/wrapper/ParentLastClassLoader.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/kotlin/ParentLastClassLoader.kt index 6c25abb8..6d5fa9d2 100644 --- a/src/main/kotlin/com/beust/kobalt/wrapper/ParentLastClassLoader.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/kotlin/ParentLastClassLoader.kt @@ -1,4 +1,4 @@ -package com.beust.kobalt.wrapper +package com.beust.kobalt.kotlin import java.net.URL import java.net.URLClassLoader @@ -9,20 +9,20 @@ import java.net.URLClassLoader * Will probably be made obsolete by making the wrapper a standalone module instead of * being inside Kobalt itself. */ -public class ParentLastClassLoader(val classpath: List) +class ParentLastClassLoader(val classpath: List) : ClassLoader(Thread.currentThread().contextClassLoader) { private val childClassLoader: ChildURLClassLoader init { val urls : Array = classpath.toTypedArray() - childClassLoader = ChildURLClassLoader(urls, FindClassClassLoader(this.parent) ) + childClassLoader = ChildURLClassLoader(urls, FindClassClassLoader(this.parent)) } /** * This class makes it possible to call findClass on a classloader */ private class FindClassClassLoader(parent: ClassLoader) : ClassLoader(parent) { - override public fun findClass(name: String) = super.findClass(name) + override fun findClass(name: String) = super.findClass(name) } /** @@ -43,7 +43,7 @@ public class ParentLastClassLoader(val classpath: List) } } - override public @Synchronized fun loadClass(name: String, resolve: Boolean) : Class<*> { + override @Synchronized fun loadClass(name: String, resolve: Boolean) : Class<*> { try { // first we try to find a class inside the child classloader return childClassLoader.findClass(name) diff --git a/src/main/kotlin/com/beust/kobalt/maven/CompletedFuture.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/CompletedFuture.kt similarity index 85% rename from src/main/kotlin/com/beust/kobalt/maven/CompletedFuture.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/CompletedFuture.kt index f64c32b5..1e24cd8e 100644 --- a/src/main/kotlin/com/beust/kobalt/maven/CompletedFuture.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/CompletedFuture.kt @@ -3,7 +3,7 @@ package com.beust.kobalt.maven import java.util.concurrent.Future import java.util.concurrent.TimeUnit -public class CompletedFuture(val value: T) : Future { +class CompletedFuture(val value: T) : Future { override fun cancel(mayInterruptIfRunning: Boolean) = true override fun get(): T = value override fun get(timeout: Long, unit: TimeUnit): T = value diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/DependencyManager.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/DependencyManager.kt new file mode 100644 index 00000000..ff2d9fd9 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/DependencyManager.kt @@ -0,0 +1,303 @@ +package com.beust.kobalt.maven + +import com.beust.kobalt.KobaltException +import com.beust.kobalt.api.* +import com.beust.kobalt.maven.aether.Filters +import com.beust.kobalt.maven.aether.KobaltMavenResolver +import com.beust.kobalt.maven.aether.Scope +import com.beust.kobalt.maven.dependency.FileDependency +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.KobaltExecutors +import com.google.common.collect.ArrayListMultimap +import org.eclipse.aether.graph.DependencyFilter +import org.eclipse.aether.util.filter.OrDependencyFilter +import java.io.File +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DependencyManager @Inject constructor(val executors: KobaltExecutors, + val resolver: KobaltMavenResolver) : IDependencyManager { + + companion object { + fun create(id: String, optional: Boolean = false, projectDirectory: String? = null) = + Kobalt.INJECTOR.getInstance(DependencyManager::class.java).create(id, optional, projectDirectory) + } + + /** + * Parse the id and return the correct IClasspathDependency + */ + override fun create(id: String, optional: Boolean, projectDirectory: String?) : IClasspathDependency { + if (id.startsWith(FileDependency.PREFIX_FILE)) { + val path = if (projectDirectory != null) { + val idPath = id.substring(FileDependency.PREFIX_FILE.length) + if (! File(idPath).isAbsolute) { + // If the project directory is relative, we might not be in the correct directory to locate + // that file, so we'll use the absolute directory deduced from the build file path. Pick + // the first one that produces an actual file + val result = listOf(File(projectDirectory), Kobalt.context?.internalContext?.absoluteDir).map { + File(it, idPath) + }.firstOrNull { + it.exists() + } + result ?: throw KobaltException("Couldn't find $id") + + } else { + File(idPath) + } + } else { + File(id.substring(FileDependency.PREFIX_FILE.length)) + } + return createFile(path.path) + } else { + // Convert to a Kobalt id first (so that if it doesn't have a version, it gets translated to + // an Aether ranged id "[0,)") + return createMaven(MavenId.create(id).toId, optional) + } + } + + /** + * Create an IClasspathDependency from a Maven id. + */ + override fun createMaven(id: String, optional: Boolean) : IClasspathDependency = resolver.create(id, optional) + + /** + * Create an IClasspathDependency from a path. + */ + override fun createFile(path: String) : IClasspathDependency = FileDependency(path) + + /** + * @return the source dependencies for this project, including the contributors. + */ + override fun dependencies(project: Project, context: KobaltContext, scopes: List) + = privateDependencies(project, context, listOf(Scope.COMPILE)) + + /** + * @return the test dependencies for this project, including the contributors. + */ + override fun testDependencies(project: Project, context: KobaltContext) + = privateDependencies(project, context, listOf(Scope.COMPILE, Scope.TEST)) + + /** + * Transitive dependencies for the compilation of this project. + */ +// fun calculateTransitiveDependencies(project: Project, context: KobaltContext) +// = calculateDependencies(project, context, project.dependentProjects, +// project.compileDependencies + project.compileRuntimeDependencies) + + /** + * @return the classpath for this project, including the IClasspathContributors. Excluded dependencies + * are removed from the result. + * + * @param{allDependencies} is typically either compileDependencies or testDependencies. If no dependencies + * are passed, they are calculated from the scope filters. + */ + override fun calculateDependencies(project: Project?, context: KobaltContext, + dependencyFilter: DependencyFilter, + scopes: List, + vararg passedDependencies: List): List { + val result = arrayListOf() + + /** + * Extract the correct dependencies from the project based on the scope filters. + */ + fun filtersToDependencies(project: Project, scopes: Collection): List { + val result = arrayListOf().apply { + if (scopes.contains(Scope.COMPILE)) { + addAll(project.compileDependencies) + addAll(project.compileProvidedDependencies) + } + if (scopes.contains(Scope.COMPILEONLY)) { + addAll(project.compileOnlyDependencies) + } + if (scopes.contains(Scope.RUNTIME)) { + addAll(project.compileRuntimeDependencies) + } + if (scopes.contains(Scope.TEST)) { + addAll(project.testDependencies) + } + } + return result.filter { ! it.optional } + } + + val allDependencies : Array> = + if (project == null || passedDependencies.any()) passedDependencies + else arrayOf(filtersToDependencies(project, scopes)) + + // Make sure that classes/ and test-classes/ are always at the top of this classpath, + // so that older versions of that project on the classpath don't shadow them + if (project != null && scopes.contains(Scope.TEST)) { + result.add(FileDependency(KFiles.makeOutputDir(project).path)) + result.add(FileDependency(KFiles.makeOutputTestDir(project).path)) + } + + allDependencies.forEach { dependencies -> + result.addAll(transitiveClosure(dependencies, dependencyFilter, project?.name)) + } + result.addAll(runClasspathContributors(project, context)) + result.addAll(dependentProjectDependencies(project, context, dependencyFilter, scopes)) + + /** + * Naïve implementation: just exclude all dependencies that start with one of the excluded dependencies. + * Should probably make exclusion more generic (full on string) or allow exclusion to be specified + * formally by groupId or artifactId. + */ + fun isDependencyExcluded(dep: IClasspathDependency, excluded: List): Boolean { + excluded.any { excluded -> dep.id == excluded.id }.let { result -> + if (result) { + context.logger.log(project?.name ?: "", 2, " Excluding dependency $dep") + } + return result + } + } + + // Dependencies get reordered by transitiveClosure() but since we just added a bunch of new ones, + // we need to reorder them again in case we're adding dependencies that are already present + // but with a different version + val shortResult = + if (project != null) { + result.filter { ! isDependencyExcluded(it, project.excludedDependencies) } + } else { + result + }.toHashSet() + val reordered = reorderDependencies(shortResult) + return reordered + } + + private fun runClasspathContributors(project: Project?, context: KobaltContext) : + Collection { + val result = hashSetOf() + context.pluginInfo.classpathContributors.forEach { it: IClasspathContributor -> + result.addAll(it.classpathEntriesFor(project, context)) + } + return result + } + + /** + * Return the transitive closure of the dependencies *without* running the classpath contributors. + * TODO: This should be private, everyone should be calling calculateDependencies(). + */ + fun transitiveClosure(dependencies : List, + filter: DependencyFilter = Filters.EXCLUDE_OPTIONAL_FILTER, + requiredBy: String? = null): List { + val result = arrayListOf() + dependencies.forEach { dependency -> + result.add(dependency) + if (dependency.isMaven) { + val resolved = resolver.resolveToIds(dependency.id, null, filter).map { create(it) } + result.addAll(resolved) + } + } + val reordered = reorderDependencies(result) + return reordered + } + + /** + * Reorder dependencies so that if an artifact appears several times, only the one with the higest version + * is included. + */ + private fun reorderDependencies(dependencies: Collection): List { + val result = arrayListOf() + val map : ArrayListMultimap = ArrayListMultimap.create() + // The multilist maps each artifact to a list of all the versions found + // (e.g. {org.testng:testng -> (6.9.5, 6.9.4, 6.1.1)}), then we return just the first one + dependencies.forEach { + map.put(it.shortId, it) + } + for (k in map.keySet()) { + val l = map.get(k) + Collections.sort(l, Collections.reverseOrder()) + result.add(l[0]) + } + return result + } + + /** + * If this project depends on other projects, we need to include their jar file and also + * their own dependencies + */ + private fun dependentProjectDependencies(project: Project?, context: KobaltContext, + dependencyFilter: DependencyFilter, scopes: List): List { + if (project == null) { + return emptyList() + } else { + val result = arrayListOf() + + fun maybeAddClassDir(classDir: String) { + // A project is allowed not to have any kobaltBuild/classes or test-classes directory if it doesn't have + // any sources + if (File(classDir).exists()) { + result.add(FileDependency(classDir)) + } + } + + val isTest = scopes.contains(Scope.TEST) + + project.dependsOn.forEach { p -> + maybeAddClassDir(KFiles.joinDir(p.directory, p.classesDir(context))) + if (isTest) maybeAddClassDir(KFiles.makeOutputTestDir(project).path) + val otherDependencies = calculateDependencies(p, context, dependencyFilter, scopes) + result.addAll(otherDependencies) + } + + if (isTest) { + project.testsDependOn.forEach { p -> + val otherDependencies = calculateDependencies(p, context, dependencyFilter, scopes) + result.addAll(otherDependencies) + } + } + return result + } + } + + private fun privateDependencies(project: Project, context: KobaltContext, passedScopes: List) + : List { + val isTest = passedScopes.contains(Scope.TEST) + val transitive = hashSetOf() + with(project) { + val scopeFilters : ArrayList = arrayListOf(Scope.COMPILE) + context.variant.let { variant -> + val deps: ArrayList> = + if (passedScopes.contains(Scope.COMPILE)) { + arrayListOf(compileDependencies, compileProvidedDependencies, + variant.buildType.compileDependencies, + variant.buildType.compileProvidedDependencies, + variant.productFlavor.compileDependencies, + variant.productFlavor.compileProvidedDependencies) + } else if (passedScopes.contains(Scope.RUNTIME)) { + arrayListOf(compileRuntimeDependencies) + } else { + arrayListOf(arrayListOf()) + } + val runtimeDeps = arrayListOf(compileRuntimeDependencies) + if (isTest) { + deps.add(testDependencies) + deps.add(testProvidedDependencies) + scopeFilters.add(Scope.TEST) + } + val filter = + if (isTest) OrDependencyFilter(Filters.COMPILE_FILTER, Filters.TEST_FILTER) + else Filters.COMPILE_FILTER + runtimeDeps.filter { it.any() }.forEach { + transitive.addAll(calculateDependencies(project, context, filter, + passedScopes, // scopes = Scope.toScopes(isTest), + passedDependencies = it)) + } + } + } + + // Make sure that classes/ and test-classes/ are always at the top of this classpath, + // so that older versions of that project on the classpath don't shadow them + val result = arrayListOf().apply { + if (isTest) { + add(FileDependency(KFiles.makeOutputDir(project).path)) + add(FileDependency(KFiles.makeOutputTestDir(project).path)) + } + addAll(reorderDependencies(transitive)) + } + + return result + } + +} diff --git a/src/main/kotlin/com/beust/kobalt/maven/Gpg.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Gpg.kt similarity index 71% rename from src/main/kotlin/com/beust/kobalt/maven/Gpg.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Gpg.kt index 5fb85508..60a4335c 100644 --- a/src/main/kotlin/com/beust/kobalt/maven/Gpg.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Gpg.kt @@ -1,16 +1,18 @@ package com.beust.kobalt.maven import com.beust.kobalt.OperatingSystem +import com.beust.kobalt.misc.LocalProperties import com.beust.kobalt.misc.error -import com.beust.kobalt.misc.log +import com.beust.kobalt.misc.kobaltLog import com.beust.kobalt.misc.warn +import com.google.inject.Inject import com.google.inject.Singleton import java.io.BufferedReader import java.io.File import java.io.InputStreamReader @Singleton -public class Gpg { +class Gpg @Inject constructor(val localProperties: LocalProperties) { val COMMANDS = listOf("gpg", "gpg2") fun findGpgCommand() : String? { @@ -36,18 +38,33 @@ public class Gpg { val result = arrayListOf() val gpg = findGpgCommand() if (gpg != null) { - val directory = files.get(0).parentFile.absoluteFile + val directory = files[0].parentFile.absoluteFile files.forEach { file -> val ascFile = File(file.absolutePath + ".asc") ascFile.delete() val allArgs = arrayListOf() allArgs.add(gpg) + + fun maybeAdd(prop: String, f: (String) -> Unit) = localProperties.getNoThrows(prop)?.let { + f(it) + } + + maybeAdd("gpg.password") { + allArgs.addAll(listOf("--passphrase", it, "--batch", "--yes")) + } + maybeAdd("gpg.keyId") { + allArgs.addAll(listOf("--local-user", it)) + } + maybeAdd("gpg.secretKeyRingFile") { + allArgs.addAll(listOf("--secret-keyring", "\"$it\"")) + } + allArgs.add("-ab") allArgs.add(file.absolutePath) val pb = ProcessBuilder(allArgs) pb.directory(directory) - log(2, "Signing file: " + allArgs.joinToString(" ")) + kobaltLog(2, "Signing file: " + allArgs.joinToString(" ")) val process = pb.start() val br = BufferedReader(InputStreamReader(process.errorStream)) diff --git a/src/main/kotlin/com/beust/kobalt/maven/Http.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Http.kt similarity index 63% rename from src/main/kotlin/com/beust/kobalt/maven/Http.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Http.kt index a591fead..df286a47 100644 --- a/src/main/kotlin/com/beust/kobalt/maven/Http.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Http.kt @@ -1,17 +1,28 @@ package com.beust.kobalt.maven import com.beust.kobalt.KobaltException +import com.beust.kobalt.internal.KobaltSettings import com.beust.kobalt.misc.CountingFileRequestBody +import com.beust.kobalt.misc.kobaltLog import com.beust.kobalt.misc.log -import com.squareup.okhttp.* -import retrofit.mime.TypedFile +import com.google.inject.Inject +import okhttp3.* +import java.io.File import java.io.IOException import javax.inject.Singleton @Singleton -public class Http { - public fun get(user: String?, password: String?, url: String) : Response { - val client = OkHttpClient(); +class Http @Inject constructor(val settings:KobaltSettings) { + companion object { + // HTTP statuses + val CREATED = 201 + } + class TypedFile(val mimeType: String, val file: File) { + override fun toString() = file.name + } + + fun get(user: String?, password: String?, url: String) : Response { + val client = OkHttpClient.Builder().proxy(settings.proxyConfigs?.firstOrNull()?.toProxy()).build() val request = Request.Builder().url(url) if (user != null) { request.header("Authorization", Credentials.basic(user, password)) @@ -24,14 +35,14 @@ public class Http { } } - public fun get(url: String) : Response { + fun get(url: String) : Response { return get(null, null, url) } fun percentProgressCallback(totalSize: Long) : (Long) -> Unit { return { num: Long -> val progress = num * 100 / totalSize - log(1, "\rUploaded: $progress%", newLine = false) + kobaltLog(1, "\rUploaded: $progress%", newLine = false) } } @@ -39,7 +50,7 @@ public class Http { error("Couldn't upload file: " + r.message()) } - public fun uploadFile(user: String? = null, password: String? = null, url: String, file: TypedFile, + fun uploadFile(user: String? = null, password: String? = null, url: String, file: TypedFile, post: Boolean, progressCallback: (Long) -> Unit = {}, headers: Headers = Headers.of(), @@ -47,7 +58,7 @@ public class Http { error: (Response) -> Unit = DEFAULT_ERROR_RESPONSE) { val fullHeaders = Headers.Builder() - fullHeaders.set("Content-Type", file.mimeType()) + fullHeaders.set("Content-Type", file.mimeType) headers.names().forEach { fullHeaders.set(it, headers.get(it)) } user?.let { @@ -59,13 +70,13 @@ public class Http { .url(url) val request = (if (post) - requestBuilder.post(CountingFileRequestBody(file.file(), file.mimeType(), progressCallback)) + requestBuilder.post(CountingFileRequestBody(file.file, file.mimeType, progressCallback)) else - requestBuilder.put(CountingFileRequestBody(file.file(), file.mimeType(), progressCallback))) + requestBuilder.put(CountingFileRequestBody(file.file, file.mimeType, progressCallback))) .build() - log(2, "Uploading $file to $url") - val response = OkHttpClient().newCall(request).execute() + kobaltLog(2, "Uploading $file to $url") + val response = OkHttpClient.Builder().proxy(settings.proxyConfigs?.firstOrNull()?.toProxy()).build().newCall(request).execute() if (! response.isSuccessful) { error(response) } else { diff --git a/src/main/kotlin/com/beust/kobalt/maven/Kurl.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Kurl.kt similarity index 50% rename from src/main/kotlin/com/beust/kobalt/maven/Kurl.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Kurl.kt index 0d13b8b5..909c18c5 100644 --- a/src/main/kotlin/com/beust/kobalt/maven/Kurl.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Kurl.kt @@ -1,30 +1,53 @@ package com.beust.kobalt.maven +import com.beust.kobalt.HostConfig +import com.beust.kobalt.maven.aether.KobaltMavenResolver import com.beust.kobalt.maven.dependency.FileDependency -import com.google.inject.assistedinject.Assisted import java.io.* import java.net.HttpURLConnection import java.net.URL import java.net.URLConnection -import javax.inject.Inject +import java.util.* /** * Abstracts a URL so that it works transparently on either http:// or file:// */ -class Kurl @Inject constructor(@Assisted val url: String) { - val connection : URLConnection by lazy { - URL(url).openConnection() +class Kurl(val hostInfo: HostConfig) { + companion object { + const val KEY = "authUrl" + const val VALUE_USER = "username" + const val VALUE_PASSWORD = "password" } - interface IFactory { - fun create(url: String) : Kurl + init { + KobaltMavenResolver.initAuthentication(hostInfo) + } + + override fun toString() = hostInfo.toString() + + val connection : URLConnection + get() { + val result = URL(hostInfo.url).openConnection() + if (hostInfo.hasAuth()) { + val userPass = hostInfo.username + ":" + hostInfo.password + val basicAuth = "Basic " + String(Base64.getEncoder().encode(userPass.toByteArray())) + result.setRequestProperty("Authorization", basicAuth) + } + return result + } + + val inputStream : InputStream by lazy { + connection.inputStream } val exists : Boolean get() { + val url = hostInfo.url val result = if (connection is HttpURLConnection) { - (connection as HttpURLConnection).responseCode == 200 + val responseCode = (connection as HttpURLConnection).responseCode + checkResponseCode(responseCode) + responseCode == 200 || responseCode == 301 } else if (url.startsWith(FileDependency.PREFIX_FILE)) { val fileName = url.substring(FileDependency.PREFIX_FILE.length) File(fileName).exists() @@ -34,11 +57,22 @@ class Kurl @Inject constructor(@Assisted val url: String) { return result } + private fun checkResponseCode(responseCode: Int) { + if (responseCode == 401) { + if (hostInfo.hasAuth()) { + error("Bad credentials supplied for ${hostInfo.url}") + } else { + error("This repo requires authentication: ${hostInfo.url}") + } + } + + } + /** The Kotlin compiler is about 17M and downloading it with the default buffer size takes forever */ private val estimatedSize: Int - get() = if (url.contains("kotlin-compiler")) 18000000 else 1000000 + get() = if (hostInfo.url.contains("kotlin-compiler")) 18000000 else 1000000 - fun toOutputStream(os: OutputStream, progress: (Long) -> Unit) = copy(connection.inputStream, os, progress) + fun toOutputStream(os: OutputStream, progress: (Long) -> Unit) = copy(inputStream, os, progress) fun toFile(file: File, progress: (Long) -> Unit = {}) = FileOutputStream(file).use { toOutputStream(it, progress) @@ -71,14 +105,12 @@ class Kurl @Inject constructor(@Assisted val url: String) { val string: String get() { val sb = StringBuilder() - connection.inputStream.use { inputStream -> - val reader = BufferedReader(InputStreamReader(inputStream)) + val reader = BufferedReader(InputStreamReader(inputStream)) - var line: String? = reader.readLine() - while (line != null) { - sb.append(line).append('\n') - line = reader.readLine() - } + var line: String? = reader.readLine() + while (line != null) { + sb.append(line).append('\n') + line = reader.readLine() } return sb.toString() diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/LocalDep.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/LocalDep.kt new file mode 100644 index 00000000..53cb1c89 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/LocalDep.kt @@ -0,0 +1,9 @@ +package com.beust.kobalt.maven + +open class LocalDep(override val mavenId: MavenId, open val localRepo: LocalRepo) + : SimpleDep(mavenId) { + + fun toAbsoluteJarFilePath(v: String) = localRepo.toFullPath(toJarFile(v)) + + fun toAbsolutePomFile(v: String) = localRepo.toFullPath(listOf(toPomFile(v)).joinToString("/")) +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/LocalRepo.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/LocalRepo.kt new file mode 100644 index 00000000..35f8c50a --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/LocalRepo.kt @@ -0,0 +1,16 @@ +package com.beust.kobalt.maven + +import com.beust.kobalt.internal.KobaltSettings +import com.google.inject.Inject +import java.io.File +import javax.inject.Singleton + +@Singleton +open class LocalRepo @Inject constructor(val kobaltSettings: KobaltSettings) { + val localRepo: File + get() = kobaltSettings.localCache + + fun toFullPath(path: String): String = File(localRepo, path).absolutePath +} + + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/MavenId.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/MavenId.kt new file mode 100644 index 00000000..13fff154 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/MavenId.kt @@ -0,0 +1,74 @@ +package com.beust.kobalt.maven + +import com.beust.kobalt.api.Kobalt +import org.eclipse.aether.artifact.DefaultArtifact + +/** + * Encapsulate a Maven id captured in one string, as used by Gradle or Ivy, e.g. "org.testng:testng:6.9.9". + * These id's are somewhat painful to manipulate because on top of containing groupId, artifactId + * and version, they also accept an optional packaging (e.g. "aar") and qualifier (e.g. "no_aop"). + * Determining which is which in an untyped string separated by colons is not clearly defined so + * this class does a best attempt at deconstructing an id but there's surely room for improvement. + * + * This class accepts a versionless id, which needs to end with a :, e.g. "com.beust:jcommander:" (which + * usually means "latest version") but it doesn't handle version ranges yet. + */ +class MavenId private constructor(val groupId: String, val artifactId: String, val packaging: String?, + val classifier: String?, val version: String?) { + + companion object { + fun isMavenId(id: String) = if (id.startsWith("file://")) { + false + } else { + with(id.split(':')) { + size >= 3 && size <= 5 + } + } + + fun isRangedVersion(s: String): Boolean { + return s.first() in listOf('[', '(') && s.last() in listOf(']', ')') + } + + /** + * Similar to create(MavenId) but don't run IMavenIdInterceptors. + */ + fun createNoInterceptors(id: String) : MavenId = DefaultArtifact(id).run { + MavenId(groupId, artifactId, extension, classifier, version) + } + + fun toMavenId(id: String) = if (id.endsWith(":")) id + "(0,]" else id + + /** + * The main entry point to create Maven Id's. Id's created by this function + * will run through IMavenIdInterceptors. + */ + fun create(originalId: String) : MavenId { + val id = toMavenId(originalId) + var originalMavenId = createNoInterceptors(id) + var interceptedMavenId = originalMavenId + val interceptors = Kobalt.context?.pluginInfo?.mavenIdInterceptors + if (interceptors != null) { + interceptedMavenId = interceptors.fold(originalMavenId, { + id, interceptor -> interceptor.intercept(id) }) + } + + return interceptedMavenId + } + + fun create(groupId: String, artifactId: String, packaging: String?, classifier: String?, version: String?) = + create(toId(groupId, artifactId, packaging, classifier, version)) + + fun toId(groupId: String, artifactId: String, packaging: String? = null, classifier: String? = null, + version: String?) = + "$groupId:$artifactId" + + (if (packaging != null && packaging != "") ":$packaging" else "") + + (if (classifier != null && classifier != "") ":$classifier" else "") + + ":$version" + } + + + val hasVersion = version != null + + val toId = MavenId.toId(groupId, artifactId, packaging, classifier, version) + +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Md5.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Md5.kt new file mode 100644 index 00000000..efa34944 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Md5.kt @@ -0,0 +1,75 @@ +package com.beust.kobalt.maven + +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.kobaltLog +import java.io.File +import java.security.MessageDigest +import javax.xml.bind.DatatypeConverter + +class Md5 { + companion object { +// private fun md5(file: File) : String { +// if (file.isDirectory) { +// println("PROBLEM") +// } +// val md5 = MessageDigest.getInstance("MD5") +// val bytes = file.readBytes() +// md5.update(bytes, 0, bytes.size) +// return DatatypeConverter.printHexBinary(md5.digest()).toLowerCase() +// } + + /** + * Calculate a checksum for all the files/directories. The conversion from File to + * bytes can be customized by the @param{toBytes} parameter. The default implementation calculates + * a checksum of the last modified timestamp. + */ + fun toMd5Directories(filesOrDirectories: List, + toBytes: (File) -> ByteArray = { "${it.path} ${it.lastModified()} ${it.length()}".toByteArray() } ) + : String? { + if (filesOrDirectories.any(File::exists)) { + MessageDigest.getInstance("MD5").let { md5 -> + var fileCount = 0 + filesOrDirectories.filter(File::exists).forEach { file -> + if (file.isFile) { + kobaltLog(3, " Calculating checksum of $file") + val bytes = toBytes(file) + md5.update(bytes, 0, bytes.size) + fileCount++ + } else { + val files = KFiles.findRecursively(file) // , { f -> f.endsWith("java")}) + kobaltLog(3, " Calculating checksum of ${files.size} files in $file") + files.map { + File(file, it) + }.filter { + it.isFile + }.forEach { + fileCount++ + val bytes = toBytes(it) + md5.update(bytes, 0, bytes.size) + } + } + } + + // The output directory might exist but with no files in it, in which case + // we must run the task + if (fileCount > 0) { + val result = DatatypeConverter.printHexBinary(md5.digest()).toLowerCase() + return result + } else { + return null + } + } + } else { + return null + } + } + + fun toMd5(file: File) = MessageDigest.getInstance("MD5").let { md5 -> + file.forEachBlock { bytes, size -> + md5.update(bytes, 0, size) + } + DatatypeConverter.printHexBinary(md5.digest()).toLowerCase() + } + } +} + diff --git a/src/main/kotlin/com/beust/kobalt/maven/Pom.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Pom.kt similarity index 61% rename from src/main/kotlin/com/beust/kobalt/maven/Pom.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Pom.kt index 5866192d..a6263e22 100644 --- a/src/main/kotlin/com/beust/kobalt/maven/Pom.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Pom.kt @@ -1,16 +1,14 @@ package com.beust.kobalt.maven -import com.beust.kobalt.misc.log import com.beust.kobalt.misc.toString +import com.beust.kobalt.misc.warn import com.google.inject.assistedinject.Assisted -import kotlinx.dom.childElements import org.w3c.dom.Element import org.w3c.dom.NodeList -import org.xml.sax.InputSource -import java.io.FileReader +import javax.xml.parsers.DocumentBuilderFactory import javax.xml.xpath.XPathConstants -public class Pom @javax.inject.Inject constructor(@Assisted val id: String, +class Pom @javax.inject.Inject constructor(@Assisted val id: String, @Assisted documentFile: java.io.File) { val XPATH_FACTORY = javax.xml.xpath.XPathFactory.newInstance() val XPATH = XPATH_FACTORY.newXPath() @@ -18,39 +16,47 @@ public class Pom @javax.inject.Inject constructor(@Assisted val id: String, var artifactId: String? = null var packaging: String? = null var version: String? = null + + /** + * If the version is a string, extract it, look it up and evaluate it if we find it. Otherwise, error. + */ + private fun calculateVersion(s: String) : String { + val v = extractVar(s) + if (v != null) { + val value = properties[v] + if (value != null) { + return value + } else { + warn("Unknown variable for version: " + s) + return "" + } + } else { + return s + } + } + + private fun extractVar(s: String) : String? { + if (s.startsWith("\${") && s.endsWith("}")) { + return s.substring(2, s.length - 1) + } else { + return null + } + } + var name: String? = null var properties = sortedMapOf() var repositories = listOf() - public interface IFactory { + interface IFactory { fun create(@Assisted id: String, @Assisted documentFile: java.io.File): Pom } - data public class Dependency(val groupId: String, val artifactId: String, val packaging: String?, + data class Dependency(val groupId: String, val artifactId: String, val packaging: String?, val version: String, val optional: Boolean = false, val scope: String? = null) { - /** When a variable is used in a maven file, e.g. ${version} */ - private val VAR = "$" + "{" - val mustDownload: Boolean get() = !optional && "provided" != scope && "test" != scope - val isValid: Boolean - get() { - var result = false - if (version.contains(VAR)) { - log(3, "Skipping variable version ${this}") - } else if (groupId.contains(VAR)) { - log(3, "Skipping variable groupId ${this}") - } else if (artifactId.contains(VAR)) { - log(3, "Skipping variable artifactId ${this}") - } else { - result = true - } - return result - } - - val id: String = "$groupId:$artifactId:$version" } @@ -58,8 +64,8 @@ public class Pom @javax.inject.Inject constructor(@Assisted val id: String, init { val DEPENDENCIES = XPATH.compile("/project/dependencies/dependency") + val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(documentFile) - val document = kotlinx.dom.parseXml(InputSource(FileReader(documentFile))) groupId = XPATH.compile("/project/groupId").evaluate(document) artifactId = XPATH.compile("/project/artifactId").evaluate(document) version = XPATH.compile("/project/version").evaluate(document) @@ -67,11 +73,12 @@ public class Pom @javax.inject.Inject constructor(@Assisted val id: String, var repositoriesList = XPATH.compile("/project/repositories").evaluate(document, XPathConstants.NODESET) as NodeList var repoElem = repositoriesList.item(0) as Element? - repositories = repoElem.childElements().map({ it.getElementsByTagName("url").item(0).textContent }) + repositories = childElements(repoElem).map({ it.getElementsByTagName("url").item(0) + .textContent }) val propertiesList = XPATH.compile("/project/properties").evaluate(document, XPathConstants.NODESET) as NodeList var propsElem = propertiesList.item(0) as Element? - propsElem.childElements().forEach { + childElements(propsElem).forEach { properties.put(it.nodeName, it.textContent) } @@ -81,7 +88,7 @@ public class Pom @javax.inject.Inject constructor(@Assisted val id: String, var groupId: String? = null var artifactId: String? = null var packaging: String? = null - var version: String = "" + var readVersion: String = "" var optional: Boolean? = false var scope: String? = null for (j in 0..d.length - 1) { @@ -91,17 +98,30 @@ public class Pom @javax.inject.Inject constructor(@Assisted val id: String, "groupId" -> groupId = e.textContent "artifactId" -> artifactId = e.textContent "type" -> packaging = e.textContent - "version" -> version = e.textContent + "version" -> readVersion = e.textContent "optional" -> optional = "true".equals(e.textContent, true) "scope" -> scope = e.textContent } } } - log(3, "Done parsing: $groupId $artifactId $version") + val version = calculateVersion(readVersion) val tmpDependency = Dependency(groupId!!, artifactId!!, packaging, version, optional!!, scope) dependencies.add(tmpDependency) } } - override public fun toString() = toString("Pom", id, "id") + private fun childElements(repoElem: Element?): List { + val result = arrayListOf() + if (repoElem != null) { + for (i in 0..repoElem.childNodes.length - 1) { + val elem = repoElem.childNodes.item(i) + if (elem is Element) { + result.add(elem) + } + } + } + return result + } + + override fun toString() = toString("Pom", "id", id) } diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Pom2.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Pom2.kt new file mode 100644 index 00000000..879f6d51 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Pom2.kt @@ -0,0 +1,163 @@ +package com.beust.kobalt.maven + +import com.beust.kobalt.misc.kobaltLog +import org.w3c.dom.Element +import org.xml.sax.InputSource +import java.io.File +import java.io.FileReader +import javax.xml.bind.JAXBContext +import javax.xml.bind.annotation.* +import javax.xml.parsers.SAXParserFactory +import javax.xml.transform.sax.SAXSource + +@XmlRootElement(name = "project") +class PomProject { + var modelVersion: String? = null + var groupId: String? = null + var artifactId: String? = null + var version: String? = null + var name: String? = null + var description: String? = null + var url: String? = null + var packaging: String? = null + var properties: Properties? = null + var parent: Parent? = null + @XmlElement(name = "dependencies") @JvmField + var pomDependencies: Dependencies? = null + val dependencies: List + get() = + if (pomDependencies != null) pomDependencies!!.dependencies + else emptyList() + + var pluginRepositories: PluginRepositories? = null + + val propertyMap = hashMapOf() + fun propertyValue(s: String) : String? { + if (propertyMap.isEmpty()) { + properties?.properties?.forEach { + propertyMap.put(it.nodeName, it.textContent.trim()) + } + } + return propertyMap[s] + } +} + +class Either(val exception: E?, val value: V?) + +class Pom2(val pomProject: PomProject) { + companion object { + fun parse(documentFile: File, dependencyManager: DependencyManager): Either { + val jaxbContext = JAXBContext.newInstance(PomProject::class.java) + val unmarshaller = jaxbContext.createUnmarshaller() + + val sax = SAXParserFactory.newInstance() + sax.isNamespaceAware = false + val reader = sax.newSAXParser().xmlReader + val er = SAXSource(reader, InputSource(FileReader(documentFile))) + + try { + val result = unmarshaller.unmarshal(er) as PomProject + result.parent?.let { + val id = with(it) { + groupId + ":" + artifactId + ":" + version + } + val dep = dependencyManager.createMaven(id) + } + return Either(null, Pom2(result)) + } catch(ex: Exception) { + return Either(ex, null) + } + } + } +} + +class Properties { + @XmlAnyElement @JvmField + val properties = arrayListOf() +} + +class Developers { + @XmlElement(name = "developer") @JvmField + val developers = arrayListOf() +} + +class Developer { + var name: String? = null + var id: String? = null +} + +class Licenses { + @XmlElement(name = "license") @JvmField + val licenses = arrayListOf() +} + +class License { + var name: String? = null + var url: String? = null + var distribution: String? = null +} + +class PluginRepositories { + @XmlElement(name = "pluginRepository") @JvmField + val pluginRepository = arrayListOf() +} + +class PluginRepository { + var id: String? = null + var name: String? = null + var url: String? = null +} + +class Dependencies { + @XmlElement(name = "dependency") @JvmField + val dependencies = arrayListOf() +} + +class Dependency { + @JvmField + var groupId: String = "" + fun groupId(pom: Pom2) : String = expandVariable(groupId, pom) + + @JvmField + var artifactId: String = "" + fun artifactId(pom: Pom2) : String = expandVariable(artifactId, pom) + + @JvmField + var version: String = "" + fun version(pom: Pom2) : String = expandVariable(version, pom) + + @JvmField + var optional: String = "false" + @JvmField + var scope: String = "" + @JvmField + var packaging: String = "" + + fun id(pom: Pom2) = groupId(pom) + ":" + artifactId(pom) + ":" + version(pom) + + val mustDownload: Boolean + get() = ! optional.toBoolean() && "provided" != scope && "test" != scope + + val isValid : Boolean get() = true //! isVariable(groupId) && ! isVariable(artifactId) && ! isVariable(version) + + private fun extractVariable(s: String) = if (s.startsWith("\${") && s.endsWith("}")) s.substring(2, s.length - 1) + else null + + private fun expandVariable(s: String, pom: Pom2) : String { + val variable = extractVariable(s) + if (variable != null) { + kobaltLog(2, "Expanding variable $variable") + val value = pom.pomProject.propertyValue(variable) + return s + } else { + return s + } + } +} + +class Parent { + var groupId: String? = null + var artifactId: String? = null + var version: String? = null +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/PomGenerator.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/PomGenerator.kt new file mode 100644 index 00000000..79c5e4b8 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/PomGenerator.kt @@ -0,0 +1,113 @@ +package com.beust.kobalt.maven + +import com.beust.kobalt.SystemProperties +import com.beust.kobalt.api.IClasspathDependency +import com.beust.kobalt.api.Project +import com.beust.kobalt.misc.KFiles +import com.beust.kobalt.misc.kobaltLog +import com.google.inject.assistedinject.Assisted +import org.apache.maven.model.Developer +import org.apache.maven.model.Model +import org.apache.maven.model.io.xpp3.MavenXpp3Writer +import java.io.File +import java.io.StringWriter +import java.nio.charset.Charset +import javax.inject.Inject + +class PomGenerator @Inject constructor(@Assisted val project: Project) { + interface IFactory { + fun create(project: Project) : PomGenerator + } + + /** + * Generate the POM file and save it. + */ + fun generateAndSave() { + requireNotNull(project.version, { "version is mandatory on project ${project.name}" }) + requireNotNull(project.group, { "group is mandatory on project ${project.name}" }) + requireNotNull(project.artifactId, { "artifactId is mandatory on project ${project.name}" }) + + val buildDir = KFiles.makeDir(project.directory, project.buildDirectory) + val outputDir = KFiles.makeDir(buildDir.path, "libs") + val NO_CLASSIFIER = null + val mavenId = MavenId.create(project.group!!, project.artifactId!!, project.packaging, NO_CLASSIFIER, + project.version!!) + val pomFile = SimpleDep(mavenId).toPomFileName() + val outputFile = File(outputDir, pomFile) + outputFile.writeText(generate(), Charset.defaultCharset()) + kobaltLog(1, " Created $outputFile") + } + + /** + * @return the text content of the POM file. + */ + fun generate() : String { + val pom = (project.pom ?: Model()).apply { + // Make sure the pom has reasonable default values + if (name == null) name = project.name + if (artifactId == null) artifactId = project.artifactId + if (groupId == null) groupId = project.group + if (modelVersion == null) modelVersion = "4.0.0" + if (version == null) version = project.version + if (description == null) description = project.description + if (url == null) url = project.url + if (developers == null) { + developers = listOf(Developer().apply { + name = SystemProperties.username + }) + } + } + + // + // Dependencies + // + pom.dependencies = arrayListOf() + + /** + * optional and provided dependencies are added both to the compile dependencies (since they are needed + * to build the project) and to their respective list as well (for POM generation). Make sure they + * don't get added twice to the .pom in such cases. + */ + fun providedOrOptional(dep: IClasspathDependency) = + project.compileProvidedDependencies.contains(dep) || + project.optionalDependencies.contains(dep) + + // Compile dependencies + project.compileDependencies.filterNot(::providedOrOptional).forEach { dep -> + pom.dependencies.add(dep.toMavenDependencies()) + } + + // Optional compile dependencies + project.optionalDependencies.forEach { dep -> + pom.dependencies.add(dep.toMavenDependencies()) + } + + // Provided dependencies + project.compileProvidedDependencies.forEach { dep -> + pom.dependencies.add(dep.toMavenDependencies("provided")) + } + + // Test dependencies + project.testDependencies.forEach { dep -> + pom.dependencies.add(dep.toMavenDependencies("test")) + } + + // Test provided dependencies + project.testProvidedDependencies.forEach { dep -> + pom.dependencies.add(dep.toMavenDependencies("test")) + } + + // Project dependencies + project.dependsOn.forEach { + pom.dependencies.add(org.apache.maven.model.Dependency().apply { + version = it.version + groupId = it.group + artifactId = it.artifactId + }) + } + + val s = StringWriter() + MavenXpp3Writer().write(s, pom) + return s.toString() + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/SimpleDep.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/SimpleDep.kt new file mode 100644 index 00000000..7c93f2ea --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/SimpleDep.kt @@ -0,0 +1,45 @@ +package com.beust.kobalt.maven + +import com.beust.kobalt.misc.Version + +open class SimpleDep(open val mavenId: MavenId) : UnversionedDep(mavenId.groupId, mavenId.artifactId) { + companion object { + fun create(id: String) = MavenId.create(id).let { + SimpleDep(it) + } + } + + override fun toString() = mavenId.toId + + val version: String get() = + if (mavenId.version == null) { + "" +// throw AssertionError("Version should have been resolved: ${mavenId.toId}") + } else { + mavenId.version!! + } + + private fun toFile(version: Version, suffix: String): String { + val dir = toDirectory(version.version, false, trailingSlash = false) + val list = + if (version.snapshotTimestamp != null) { + listOf(dir, artifactId + "-" + version.noSnapshotVersion + "-" + version.snapshotTimestamp + suffix) + } else { + listOf(dir, artifactId + "-" + version.version + suffix) + } + return list.joinToString("/").replace("//", "/") + } + + fun toPomFile(v: String) = toFile(Version.of(v), ".pom") + + fun toJarFile(v: String = version) = toFile(Version.of(v), suffix) + fun toAarFile(v: String = version) = toFile(Version.of(v), ".aar") + + fun toPomFileName() = "$artifactId-$version.pom" + + val suffix : String + get() { + val packaging = mavenId.packaging + return if (packaging != null && ! packaging.isNullOrBlank()) ".$packaging" else ".jar" + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/UnversionedDep.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/UnversionedDep.kt new file mode 100644 index 00000000..40d9c524 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/UnversionedDep.kt @@ -0,0 +1,29 @@ +package com.beust.kobalt.maven + +import java.io.File + +/** + * Represents a dependency that doesn't have a version: "org.testng:testng:". Such dependencies + * eventually resolve to the latest version of the artifact. + */ +open class UnversionedDep(open val groupId: String, open val artifactId: String) { + open fun toMetadataXmlPath(fileSystem: Boolean = true, isLocal: Boolean, version: String? = null) : String { + var result = toDirectory("", fileSystem) + if (isLocal) "maven-metadata-local.xml" else "maven-metadata.xml" + if (! File(result).exists() && version != null) { + result = toDirectory("", fileSystem) + version + File.separator + + if (isLocal) "maven-metadata-local.xml" else "maven-metadata.xml" + } + return result + } + + /** + * Turn this dependency to a directory. If fileSystem is true, use the file system + * dependent path separator, otherwise, use '/' (used to create URL's). + */ + fun toDirectory(v: String, fileSystem: Boolean = true, trailingSlash: Boolean = true): String { + val sep = if (fileSystem) File.separator else "/" + val l = listOf(groupId.replace(".", sep), artifactId, v) + val result = l.joinToString(sep) + return if (trailingSlash && ! result.endsWith(sep)) result + sep else result + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/AetherDependency.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/AetherDependency.kt new file mode 100644 index 00000000..825a8ae8 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/AetherDependency.kt @@ -0,0 +1,94 @@ +package com.beust.kobalt.maven.aether + +import com.beust.kobalt.Args +import com.beust.kobalt.api.Dependencies +import com.beust.kobalt.api.IClasspathDependency +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.maven.CompletedFuture +import com.beust.kobalt.misc.StringVersion +import com.beust.kobalt.misc.warn +import org.eclipse.aether.artifact.Artifact +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.resolution.DependencyResolutionException +import java.io.File +import java.util.concurrent.Future + +class AetherDependency(val artifact: Artifact, override val optional: Boolean = false, val args: Args? = null) + : IClasspathDependency, Comparable { + val aether: KobaltMavenResolver get() = Kobalt.INJECTOR.getInstance(KobaltMavenResolver::class.java) + + override val id: String = toId(artifact) + + override val version: String = artifact.version + + override val isMaven = true + + private fun toId(a: Artifact) = a.toString() + + override val jarFile: Future + get() { + resolveSourcesIfNeeded() + return if (artifact.file != null) { + CompletedFuture(artifact.file) + } else { + val td = aether.resolve(artifact) + CompletedFuture(td.root.artifact.file) + } + } + + private fun resolveSourcesIfNeeded() { + if (args?.downloadSources ?: false) { + listOf(artifact.toSourcesArtifact(), artifact.toJavaDocArtifact()).forEach { artifact -> + if (artifact.file == null) { + try { + aether.resolve(artifact) + } catch(e: DependencyResolutionException) { + // Ignore + } + } + } + } + } + + override fun toMavenDependencies(scope: String?) : org.apache.maven.model.Dependency { + val passedScope = scope + val op = this.optional + return org.apache.maven.model.Dependency().apply { + groupId = artifact.groupId + artifactId = artifact.artifactId + version = artifact.version + if (op) optional = op.toString() + if (passedScope != null) this.scope = passedScope + } + } + + override fun directDependencies(): List { + val result = arrayListOf() + val deps = aether.directDependencies(artifact) + if (deps != null) { + deps.root.children.forEach { + result.add(AetherDependency(it.artifact, it.dependency.optional)) + } + } else { + warn("Couldn't resolve $artifact") + } + return result + } + + override val shortId = artifact.groupId + ":" + artifact.artifactId + ":" + artifact.classifier + + override val excluded = arrayListOf() + + override fun compareTo(other: AetherDependency): Int { + return StringVersion(artifact.version).compareTo(StringVersion(other.artifact.version)) + } + + override fun hashCode() = id.hashCode() + + override fun equals(other: Any?) = if (other is AetherDependency) other.id == id else false + + override fun toString() = id + + fun Artifact.toSourcesArtifact() = DefaultArtifact(groupId, artifactId, "sources", extension, version) + fun Artifact.toJavaDocArtifact() = DefaultArtifact(groupId, artifactId, "javadoc", extension, version) +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Booter.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Booter.kt new file mode 100644 index 00000000..02917929 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Booter.kt @@ -0,0 +1,55 @@ +package com.beust.kobalt.maven.aether + +import com.beust.kobalt.internal.KobaltSettings +import com.google.common.eventbus.EventBus +import com.beust.kobalt.Args +import org.eclipse.aether.DefaultRepositorySystemSession +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.repository.LocalRepository +import java.io.File + +object Booter { + + fun newRepositorySystem(): RepositorySystem { + return ManualRepositorySystemFactory.newRepositorySystem() + // return org.eclipse.aether.examples.guice.GuiceRepositorySystemFactory.newRepositorySystem(); + // return org.eclipse.aether.examples.sisu.SisuRepositorySystemFactory.newRepositorySystem(); + // return org.eclipse.aether.examples.plexus.PlexusRepositorySystemFactory.newRepositorySystem(); + } + +// fun newRepositorySystemSession(system: RepositorySystem): DefaultRepositorySystemSession { +// val session = org.apache.maven.repository.internal.MavenRepositorySystemUtils.newSession() +// +// val localRepo = LocalRepository("target/local-repo") +// session.localRepositoryManager = system.newLocalRepositoryManager(session, localRepo) +// +// session.transferListener = ConsoleTransferListener() +// session.repositoryListener = ConsoleRepositoryListener(System.out, EventBus()) +// +// // uncomment to generate dirty trees +// // session.setDependencyGraphTransformer( null ); +// +// return session +// } + + fun newRepositorySystemSession(system: RepositorySystem, repo: File, settings: KobaltSettings, + args: Args, eventBus: EventBus): DefaultRepositorySystemSession { + val session = MavenRepositorySystemUtils.newSession(settings) + session.isOffline = args.offline + + val localRepo = LocalRepository(repo.absolutePath) + session.localRepositoryManager = system.newLocalRepositoryManager(session, localRepo) + + session.transferListener = ConsoleTransferListener() + session.repositoryListener = ConsoleRepositoryListener(eventBus = eventBus) + + // uncomment to generate dirty trees + // session.setDependencyGraphTransformer( null ); + + return session + } + +// fun newRepositories(repositories: Collection) +// = repositories.map { RemoteRepository.Builder("maven", "default", it).build() } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/ConsoleRepositoryListener.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/ConsoleRepositoryListener.kt new file mode 100644 index 00000000..fdbdbac7 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/ConsoleRepositoryListener.kt @@ -0,0 +1,93 @@ +package com.beust.kobalt.maven.aether + +import com.beust.kobalt.internal.eventbus.ArtifactDownloadedEvent +import com.beust.kobalt.misc.kobaltLog +import com.google.common.eventbus.EventBus +import org.eclipse.aether.AbstractRepositoryListener +import org.eclipse.aether.RepositoryEvent +import java.io.PrintStream + +/** + * A simplistic repository listener that logs events to the console. + */ +class ConsoleRepositoryListener @JvmOverloads constructor(out: PrintStream? = null, val eventBus: EventBus) + : AbstractRepositoryListener() { + companion object { + val LOG_LEVEL = 4 + } + + override fun artifactDeployed(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Deployed " + event!!.artifact + " to " + event.repository) + } + + override fun artifactDeploying(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Deploying " + event!!.artifact + " to " + event.repository) + } + + override fun artifactDescriptorInvalid(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Invalid artifact descriptor for " + event!!.artifact + ": " + + event.exception.message) + } + + override fun artifactDescriptorMissing(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Missing artifact descriptor for " + event!!.artifact) + } + + override fun artifactInstalled(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Installed " + event!!.artifact + " to " + event.file) + } + + override fun artifactInstalling(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Installing " + event!!.artifact + " to " + event.file) + } + + override fun artifactResolved(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Resolved artifact " + event!!.artifact + " from " + event.repository) + } + + override fun artifactDownloading(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Downloading artifact " + event!!.artifact + " from " + event.repository) + } + + override fun artifactDownloaded(event: RepositoryEvent?) { + if (event?.file != null && event?.artifact != null) { + val artifact = event!!.artifact + kobaltLog(1, "Downloaded artifact " + artifact + " from " + event.repository) + eventBus.post(ArtifactDownloadedEvent(artifact.toString(), event.repository)) + } + } + + override fun artifactResolving(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Resolving artifact " + event!!.artifact) + } + + override fun metadataDeployed(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Deployed " + event!!.metadata + " to " + event.repository) + } + + override fun metadataDeploying(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Deploying " + event!!.metadata + " to " + event.repository) + } + + override fun metadataInstalled(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Installed " + event!!.metadata + " to " + event.file) + } + + override fun metadataInstalling(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Installing " + event!!.metadata + " to " + event.file) + } + + override fun metadataInvalid(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Invalid metadata " + event!!.metadata) + } + + override fun metadataResolved(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Resolved metadata " + event!!.metadata + " from " + event.repository) + } + + override fun metadataResolving(event: RepositoryEvent?) { + kobaltLog(LOG_LEVEL, "Resolving metadata " + event!!.metadata + " from " + event.repository) + } + +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/ConsoleTransferListener.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/ConsoleTransferListener.kt new file mode 100644 index 00000000..46d8eb15 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/ConsoleTransferListener.kt @@ -0,0 +1,133 @@ +package com.beust.kobalt.maven.aether + +import com.beust.kobalt.misc.KobaltLogger +import com.beust.kobalt.misc.kobaltLog +import org.eclipse.aether.transfer.AbstractTransferListener +import org.eclipse.aether.transfer.MetadataNotFoundException +import org.eclipse.aether.transfer.TransferEvent +import org.eclipse.aether.transfer.TransferResource +import java.io.PrintStream +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +class ConsoleTransferListener @JvmOverloads constructor(out: PrintStream? = null) : AbstractTransferListener() { + + private val out: PrintStream + + private val downloads = ConcurrentHashMap() + + private var lastLength: Int = 0 + + init { + this.out = out ?: System.out + } + + override fun transferInitiated(event: TransferEvent?) { + val message = if (event!!.requestType == TransferEvent.RequestType.PUT) "Uploading" else "Downloading" + + kobaltLog(2, message + ": " + event.resource.repositoryUrl + event.resource.resourceName) + } + + val PROPERTY_NO_ANIMATIONS = "com.beust.kobalt.noAnimations" + + override fun transferProgressed(event: TransferEvent?) { + // Not on a terminal: don't display the progress + if (System.console() == null || System.getProperty(PROPERTY_NO_ANIMATIONS) != null) return + + val resource = event!!.resource + downloads.put(resource, java.lang.Long.valueOf(event.transferredBytes)) + + val buffer = StringBuilder(64) + + for (entry in downloads.entries) { + val total = entry.key.contentLength + val complete = entry.value + + buffer.append(getStatus(complete, total)).append(" ") + } + + val pad = lastLength - buffer.length + lastLength = buffer.length + pad(buffer, pad) + buffer.append('\r') + + out.print(buffer) + } + + private fun getStatus(complete: Long, total: Long): String { + if (total >= 1024) { + return toKB(complete).toString() + "/" + toKB(total) + " KB " + } else if (total >= 0) { + return complete.toString() + "/" + total + " B " + } else if (complete >= 1024) { + return toKB(complete).toString() + " KB " + } else { + return complete.toString() + " B " + } + } + + private fun pad(buffer: StringBuilder, spaces: Int) { + var spaces = spaces + val block = " " + while (spaces > 0) { + val n = Math.min(spaces, block.length) + buffer.append(block, 0, n) + spaces -= n + } + } + + override fun transferSucceeded(event: TransferEvent) { + transferCompleted(event) + + val resource = event.resource + val contentLength = event.transferredBytes + if (contentLength >= 0) { + val type = if (event.requestType == TransferEvent.RequestType.PUT) "Uploaded" else "Downloaded" + val len = if (contentLength >= 1024) toKB(contentLength).toString() + " KB" + else contentLength.toString() + " B" + + var throughput = "" + val duration = System.currentTimeMillis() - resource.transferStartTime + if (duration > 0) { + val bytes = contentLength - resource.resumeOffset + val format = DecimalFormat("0.0", DecimalFormatSymbols(Locale.ENGLISH)) + val kbPerSec = bytes / 1024.0 / (duration / 1000.0) + throughput = " at " + format.format(kbPerSec) + " KB/sec" + } + + kobaltLog(2, type + ": " + resource.repositoryUrl + resource.resourceName + " (" + len + + throughput + ")") + } + } + + override fun transferFailed(event: TransferEvent) { + transferCompleted(event) + + if (event.exception !is MetadataNotFoundException) { + if (KobaltLogger.LOG_LEVEL > 2) { + Exceptions.printStackTrace(event.exception) + } + } + } + + private fun transferCompleted(event: TransferEvent) { + downloads.remove(event.resource) + + val buffer = StringBuilder(64) + pad(buffer, lastLength) + buffer.append('\r') + out.print(buffer) + } + + override fun transferCorrupted(event: TransferEvent?) { + Exceptions.printStackTrace(event!!.exception) + } + + fun toKB(bytes: Long): Long { + return (bytes + 1023) / 1024 + } + +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Exceptions.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Exceptions.kt new file mode 100644 index 00000000..37fcda39 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Exceptions.kt @@ -0,0 +1,14 @@ +package com.beust.kobalt.maven.aether + +object Exceptions { + fun printStackTrace(t: Throwable) { + t.printStackTrace(System.out) + +// println("PRINT STACK TRACE FOR $t") +// t.printStackTrace(System.out) +// println(t.message) +// t.stackTrace.forEach { +// println(" " + it) +// } + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Filters.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Filters.kt new file mode 100644 index 00000000..0a661c2c --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Filters.kt @@ -0,0 +1,25 @@ +package com.beust.kobalt.maven.aether + +import com.beust.kobalt.misc.kobaltLog +import org.eclipse.aether.graph.DependencyFilter +import org.eclipse.aether.graph.DependencyNode +import org.eclipse.aether.util.artifact.JavaScopes + +object Filters { + val COMPILE_FILTER = DependencyFilter { p0, p1 -> + p0.dependency.scope == "" || p0.dependency.scope == JavaScopes.COMPILE + } + val TEST_FILTER = DependencyFilter { p0, p1 -> p0.dependency.scope == JavaScopes.TEST } + + val EXCLUDE_OPTIONAL_FILTER = object: DependencyFilter { + override fun accept(p0: DependencyNode, p1: MutableList): Boolean { + val result = p0.dependency != null && ! p0.dependency.optional + if (! result) { + kobaltLog(3, "Excluding from optional filter: $p0") + } + return result + } + + override fun toString() = "EXCLUDE_OPTIONAL_FILTER" + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/KobaltMavenResolver.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/KobaltMavenResolver.kt new file mode 100644 index 00000000..7c8b705f --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/KobaltMavenResolver.kt @@ -0,0 +1,180 @@ +package com.beust.kobalt.maven.aether + +import com.beust.kobalt.Args +import com.beust.kobalt.HostConfig +import com.beust.kobalt.KobaltException +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.internal.KobaltSettings +import com.beust.kobalt.internal.getProxy +import com.beust.kobalt.maven.Kurl +import com.beust.kobalt.maven.LocalRepo +import com.beust.kobalt.maven.MavenId +import com.beust.kobalt.misc.LocalProperties +import com.google.common.eventbus.EventBus +import com.google.inject.Inject +import org.eclipse.aether.artifact.Artifact +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.collection.CollectRequest +import org.eclipse.aether.collection.CollectResult +import org.eclipse.aether.graph.DefaultDependencyNode +import org.eclipse.aether.graph.Dependency +import org.eclipse.aether.graph.DependencyFilter +import org.eclipse.aether.repository.RemoteRepository +import org.eclipse.aether.resolution.DependencyRequest +import org.eclipse.aether.resolution.DependencyResult +import org.eclipse.aether.resolution.VersionRangeRequest +import org.eclipse.aether.resolution.VersionRangeResult +import org.eclipse.aether.util.repository.AuthenticationBuilder +import java.util.* + +class KobaltMavenResolver @Inject constructor(val settings: KobaltSettings, + val args: Args, + localRepo: LocalRepo, eventBus: EventBus) { + + companion object { + fun artifactToId(artifact: Artifact) = artifact.let { + MavenId.toId(it.groupId, it.artifactId, it.extension, it.classifier, it.version) + } + fun isRangeVersion(id: String) = id.contains(",") + + fun initAuthentication(hostInfo: HostConfig) { + // See if the URL needs to be authenticated. Look in local.properties for keys + // of the format authUrl..user=xxx and authUrl..password=xxx + val properties = LocalProperties().localProperties + val host = java.net.URL(hostInfo.url).host + properties.entries.forEach { + val key = it.key.toString() + if (key == "${Kurl.KEY}.$host.${Kurl.VALUE_USER}") { + hostInfo.username = properties.getProperty(key) + } else if (key == "${Kurl.KEY}.$host.${Kurl.VALUE_PASSWORD}") { + hostInfo.password = properties.getProperty(key) + } + } + fun error(s1: String, s2: String) { + throw KobaltException("Found \"$s1\" but not \"$s2\" in local.properties for ${Kurl.KEY}.$host", + docUrl = "https://beust.com/kobalt/documentation/index.html#maven-repos-authenticated") + } + if (! hostInfo.username.isNullOrBlank() && hostInfo.password.isNullOrBlank()) { + error("username", "password") + } else if(hostInfo.username.isNullOrBlank() && ! hostInfo.password.isNullOrBlank()) { + error("password", "username") + } + + } + } + + fun resolveToArtifact(id: String, scope: Scope? = null, + filter: DependencyFilter = Filters.EXCLUDE_OPTIONAL_FILTER) : Artifact + = resolve(id, scope, filter).root.artifact + + fun resolve(passedId: String, scope: Scope? = null, + filter: DependencyFilter = Filters.EXCLUDE_OPTIONAL_FILTER, + repos: List = emptyList()): DependencyResult { + val mavenId = MavenId.toMavenId(passedId) + val id = + if (isRangeVersion(mavenId)) { + val artifact = DefaultArtifact(mavenId) + val request = VersionRangeRequest(artifact, createRepos(repos), null) + val rr = system.resolveVersionRange(session, request) + if (rr.highestVersion != null) { + val newArtifact = DefaultArtifact(artifact.groupId, artifact.artifactId, artifact.classifier, + artifact.extension, rr.highestVersion.toString()) + artifactToId(newArtifact) + } else { + throw KobaltException("Couldn't resolve $passedId") + } + } else { + passedId + } + + val collectRequest = createCollectRequest(id, scope, repos) + val dependencyRequest = DependencyRequest(collectRequest, filter) + val result = system.resolveDependencies(session, dependencyRequest) + // GraphUtil.displayGraph(listOf(result.root), { it -> it.children }, + // { it: DependencyNode, indent: String -> println(indent + it.toString()) }) + return result + } + + fun resolve(artifact: Artifact, scope: Scope? = null, + filter: DependencyFilter = Filters.EXCLUDE_OPTIONAL_FILTER) + = resolve(artifactToId(artifact), scope, filter) + + fun resolveToIds(id: String, scope: Scope? = null, + filter: DependencyFilter = Filters.EXCLUDE_OPTIONAL_FILTER, + seen: HashSet = hashSetOf()) : List { + val rr = resolve(id, scope, filter) + val children = + rr.root.children.filter { + filter.accept(DefaultDependencyNode(it.dependency), emptyList()) + }.filter { + it.dependency.scope != Scope.SYSTEM.scope + } + val result = listOf(artifactToId(rr.root.artifact)) + children.flatMap { + val thisId = artifactToId(it.artifact) + if (! seen.contains(thisId)) { + seen.add(thisId) + resolveToIds(thisId, scope, filter, seen) + } else { + emptyList() + } + } + return result + } + + fun directDependencies(id: String, scope: Scope? = null): CollectResult? + = system.collectDependencies(session, createCollectRequest(id, scope)) + + fun directDependencies(artifact: Artifact, scope: Scope? = null): CollectResult? + = artifactToId(artifact).let { id -> + directDependencies(id, scope) + } + + fun resolveRange(artifact: Artifact): VersionRangeResult? { + val request = VersionRangeRequest(artifact, kobaltRepositories, null) + val result = system.resolveVersionRange(session, request) + return result + } + + /** + * Create an IClasspathDependency from a Kobalt id. + */ + fun create(id: String, optional: Boolean) = AetherDependency(DefaultArtifact(id), optional, args) + + private val system = Booter.newRepositorySystem() + private val session = Booter.newRepositorySystemSession(system, localRepo.localRepo, settings, args, eventBus) + + private fun createRepo(hostConfig: HostConfig) : RemoteRepository { + val builder = RemoteRepository.Builder(hostConfig.name, "default", hostConfig.url) + if (hostConfig.hasAuth()) { + val auth = AuthenticationBuilder() + .addUsername(hostConfig.username) + .addPassword(hostConfig.password) + .build() + builder.setAuthentication(auth) + } + return builder.build() + } + + private val kobaltRepositories: List + get() = Kobalt.repos.map { + createRepo(it).let { repository -> + val proxyConfigs = settings.proxyConfigs ?: return@map repository + RemoteRepository.Builder(repository).apply { + setProxy(proxyConfigs.getProxy(repository.protocol)?.toAetherProxy()) + }.build() + } + } + + private fun createRepos(repos: List) : List + = kobaltRepositories + repos.map { createRepo(HostConfig(it)) } + + private fun createCollectRequest(id: String, scope: Scope? = null, repos: List = emptyList()) + = CollectRequest().apply { + val allIds = arrayListOf(MavenId.toMavenId(id)) + + dependencies = allIds.map { Dependency(DefaultArtifact(it), scope?.scope) } + + root = Dependency(DefaultArtifact(MavenId.toMavenId(id)), scope?.scope) + repositories = createRepos(repos) + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/ManualRepositorySystemFactory.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/ManualRepositorySystemFactory.kt new file mode 100644 index 00000000..7f019533 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/ManualRepositorySystemFactory.kt @@ -0,0 +1,39 @@ +package com.beust.kobalt.maven.aether + +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory +import org.eclipse.aether.impl.DefaultServiceLocator +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory +import org.eclipse.aether.spi.connector.transport.TransporterFactory +import org.eclipse.aether.transport.file.FileTransporterFactory +import org.eclipse.aether.transport.http.HttpTransporterFactory + +/** + * A factory for repository system instances that employs Aether's built-in service locator infrastructure to wire up + * the system's components. + */ +object ManualRepositorySystemFactory { + + fun newRepositorySystem(): RepositorySystem { + /* + * Aether's components implement org.eclipse.aether.spi.locator.Service to ease manual wiring and using the + * prepopulated DefaultServiceLocator, we only need to register the repository connector and transporter + * factories. + */ + val locator = MavenRepositorySystemUtils.newServiceLocator() + locator.addService(RepositoryConnectorFactory::class.java, BasicRepositoryConnectorFactory::class.java) + locator.addService(TransporterFactory::class.java, FileTransporterFactory::class.java) + locator.addService(TransporterFactory::class.java, HttpTransporterFactory::class.java) + + locator.setErrorHandler(object : DefaultServiceLocator.ErrorHandler() { + override fun serviceCreationFailed(type: Class<*>, impl: Class<*>, exception: Throwable) { + Exceptions.printStackTrace(exception) + } + }) + + return locator.getService(RepositorySystem::class.java) + } + +} + + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/MavenRepositorySystemUtils.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/MavenRepositorySystemUtils.kt new file mode 100644 index 00000000..1b8a5e0f --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/MavenRepositorySystemUtils.kt @@ -0,0 +1,70 @@ +package com.beust.kobalt.maven.aether + +import com.beust.kobalt.internal.KobaltSettings +import org.apache.maven.repository.internal.* +import org.eclipse.aether.DefaultRepositorySystemSession +import org.eclipse.aether.artifact.DefaultArtifactType +import org.eclipse.aether.impl.* +import org.eclipse.aether.util.artifact.DefaultArtifactTypeRegistry +import org.eclipse.aether.util.graph.manager.ClassicDependencyManager +import org.eclipse.aether.util.graph.selector.AndDependencySelector +import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector +import org.eclipse.aether.util.graph.selector.OptionalDependencySelector +import org.eclipse.aether.util.graph.selector.ScopeDependencySelector +import org.eclipse.aether.util.graph.transformer.* +import org.eclipse.aether.util.graph.traverser.FatArtifactTraverser +import org.eclipse.aether.util.repository.DefaultProxySelector +import org.eclipse.aether.util.repository.SimpleArtifactDescriptorPolicy + +object MavenRepositorySystemUtils { + + fun newServiceLocator(): DefaultServiceLocator { + val locator = DefaultServiceLocator() + locator.addService(ArtifactDescriptorReader::class.java, DefaultArtifactDescriptorReader::class.java) + locator.addService(VersionResolver::class.java, DefaultVersionResolver::class.java) + locator.addService(VersionRangeResolver::class.java, DefaultVersionRangeResolver::class.java) + locator.addService(MetadataGeneratorFactory::class.java, SnapshotMetadataGeneratorFactory::class.java) + locator.addService(MetadataGeneratorFactory::class.java, VersionsMetadataGeneratorFactory::class.java) + return locator + } + + fun newSession(settings: KobaltSettings): DefaultRepositorySystemSession { + val session = DefaultRepositorySystemSession() + val depTraverser = FatArtifactTraverser() + session.dependencyTraverser = depTraverser + val depManager = ClassicDependencyManager() + session.dependencyManager = depManager + val depFilter = AndDependencySelector(*arrayOf(ScopeDependencySelector(*arrayOf("test", "provided")), OptionalDependencySelector(), ExclusionDependencySelector())) + session.dependencySelector = depFilter + val transformer = ConflictResolver(NearestVersionSelector(), JavaScopeSelector(), SimpleOptionalitySelector(), JavaScopeDeriver()) + ChainedDependencyGraphTransformer(*arrayOf(transformer, JavaDependencyContextRefiner())) + session.dependencyGraphTransformer = transformer + val stereotypes = DefaultArtifactTypeRegistry() + stereotypes.add(DefaultArtifactType("pom")) + stereotypes.add(DefaultArtifactType("maven-plugin", "jar", "", "java")) + stereotypes.add(DefaultArtifactType("jar", "jar", "", "java")) + stereotypes.add(DefaultArtifactType("ejb", "jar", "", "java")) + stereotypes.add(DefaultArtifactType("ejb-client", "jar", "client", "java")) + stereotypes.add(DefaultArtifactType("test-jar", "jar", "tests", "java")) + stereotypes.add(DefaultArtifactType("javadoc", "jar", "javadoc", "java")) + stereotypes.add(DefaultArtifactType("java-source", "jar", "sources", "java", false, false)) + stereotypes.add(DefaultArtifactType("war", "war", "", "java", false, true)) + stereotypes.add(DefaultArtifactType("ear", "ear", "", "java", false, true)) + stereotypes.add(DefaultArtifactType("rar", "rar", "", "java", false, true)) + stereotypes.add(DefaultArtifactType("par", "par", "", "java", false, true)) + session.artifactTypeRegistry = stereotypes + session.artifactDescriptorPolicy = SimpleArtifactDescriptorPolicy(true, true) + val sysProps = System.getProperties() + session.setSystemProperties(sysProps) + session.setConfigProperties(sysProps) + settings.proxyConfigs?.let { proxyConfigs-> + session.proxySelector = DefaultProxySelector().apply { + proxyConfigs.forEach {config-> + add(config.toAetherProxy(), config.nonProxyHosts) + } + } + } + return session + } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Scope.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Scope.kt new file mode 100644 index 00000000..7822159e --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Scope.kt @@ -0,0 +1,19 @@ +package com.beust.kobalt.maven.aether + +import com.beust.kobalt.api.IClasspathDependency +import com.beust.kobalt.api.Project +import org.eclipse.aether.util.artifact.JavaScopes + +sealed class Scope(val scope: String, val dependencyLambda: (Project) -> List) { + + companion object { + fun toScopes(isTest: Boolean) = if (isTest) listOf(TEST, COMPILE) else listOf(COMPILE) + } + + object COMPILE : Scope(JavaScopes.COMPILE, Project::compileDependencies) + object PROVIDED : Scope(JavaScopes.PROVIDED, Project::compileProvidedDependencies) + object COMPILEONLY : Scope("compileOnly", Project::compileOnlyDependencies) + object SYSTEM : Scope(JavaScopes.SYSTEM, { project -> emptyList() }) + object RUNTIME : Scope(JavaScopes.RUNTIME, Project::compileRuntimeDependencies) + object TEST : Scope(JavaScopes.TEST, Project::testDependencies) +} \ No newline at end of file diff --git a/src/main/kotlin/com/beust/kobalt/maven/dependency/FileDependency.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/dependency/FileDependency.kt similarity index 70% rename from src/main/kotlin/com/beust/kobalt/maven/dependency/FileDependency.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/dependency/FileDependency.kt index 368e37a4..047754e2 100644 --- a/src/main/kotlin/com/beust/kobalt/maven/dependency/FileDependency.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/dependency/FileDependency.kt @@ -1,22 +1,29 @@ package com.beust.kobalt.maven.dependency +import com.beust.kobalt.api.Dependencies import com.beust.kobalt.api.IClasspathDependency import com.beust.kobalt.maven.CompletedFuture import org.apache.maven.model.Dependency import java.io.File -open public class FileDependency(open val fileName: String) : IClasspathDependency, Comparable { +open class FileDependency(open val fileName: String, override val optional: Boolean = false) + : IClasspathDependency, Comparable { companion object { val PREFIX_FILE: String = "file://" } override val id = PREFIX_FILE + fileName + override val version = "0.0" + + override val isMaven = false + override val jarFile = CompletedFuture(File(fileName)) - override fun toMavenDependencies(): Dependency { + override fun toMavenDependencies(scope: String?): Dependency { with(Dependency()) { systemPath = jarFile.get().absolutePath + this.scope = scope return this } } @@ -25,6 +32,8 @@ open public class FileDependency(open val fileName: String) : IClasspathDependen override fun directDependencies() = arrayListOf() + override val excluded = arrayListOf() + override fun compareTo(other: FileDependency) = fileName.compareTo(other.fileName) override fun toString() = fileName diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Benchmarks.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Benchmarks.kt new file mode 100644 index 00000000..7209c51e --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Benchmarks.kt @@ -0,0 +1,15 @@ +package com.beust.kobalt.misc + +fun benchmarkMillis(run: () -> T) : Pair { + val start = System.currentTimeMillis() + val result = run() + return Pair(System.currentTimeMillis() - start, result) +} + +fun benchmarkSeconds(run: () -> T) : Pair { + val result = benchmarkMillis(run) + return Pair(result.first / 1000, result.second) +} + + + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/BlockExtractor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/BlockExtractor.kt new file mode 100644 index 00000000..a854156a --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/BlockExtractor.kt @@ -0,0 +1,119 @@ +package com.beust.kobalt.misc + +import java.io.File +import java.util.regex.Pattern + +class Section(val start: Int, val end: Int) { + override fun toString() = "$start-$end" +} + +class IncludedBuildSourceDir(val line: Int, val dirs: List) + +class BuildScriptInfo(val file: File, val fullBuildFile: List, val sections: List
, + val imports: List, val topLines: List) { + fun isInSection(lineNumber: Int): Boolean { + sections.forEach { + if (lineNumber >= it.start && lineNumber <= it.end) return true + } + return false + } + + val includedBuildSourceDirs = arrayListOf() + + fun addBuildSourceDir(dir: IncludedBuildSourceDir) = includedBuildSourceDirs.add(dir) + + fun includedBuildSourceDirsForLine(line: Int): List { + val result = includedBuildSourceDirs.find { it.line == line }?.dirs + return result ?: emptyList() + } +} + +/** + * Used to extract a keyword followed by opening and closing tags out of a list of strings, + * e.g. buildScript { ... }. + */ +class BlockExtractor(val regexp: Pattern, val opening: Char, val closing: Char) { + fun extractBlock(file: File, lines: List): BuildScriptInfo? { + var currentLineNumber = 0 + // First line of the buildScript block + var startLine = 0 + // Last line of the buildScript block + var endLine = 0 + var foundKeyword = false + var foundClosing = false + var count = 0 + val buildScript = arrayListOf() + val topLines = arrayListOf() + val finalTopLines = arrayListOf() + + fun updateCount(line: String) { + val currentLine = StringBuffer() + line.toCharArray().forEach { c -> + if (c == opening) { + count++ + } + if (c == closing) { + count-- + if (count == 0) { + currentLine.append(closing).append("\n") + foundClosing = true + endLine = currentLineNumber + } + } + if (foundKeyword && count > 0) currentLine.append(c) + } + + if (currentLine.isNotEmpty() && foundKeyword) buildScript.add(currentLine.toString()) + } + + val imports = arrayListOf() + val sections = arrayListOf
() + lines.forEach { line -> + val found = regexp.matcher(line).matches() + if (found) { + startLine = currentLineNumber + foundKeyword = true + count = 1 + buildScript.add(line) + finalTopLines.addAll(topLines) + } else { + if (line.startsWith("import")) { + if (isAllowedImport(line)) { + imports.add(line) + } + } else { + topLines.add(line) + } + updateCount(line) + } + + if (foundKeyword && foundClosing && count == 0) { + sections.add(Section(startLine, endLine)) + foundKeyword = false + foundClosing = false + count = 0 + startLine = 0 + endLine = 0 + } + + currentLineNumber++ + } + + if (sections.isNotEmpty()) { + val result = (imports.distinct() + buildScript).joinToString("\n") + "\n" + + return BuildScriptInfo(file, lines, sections, imports, finalTopLines) + } else { + return null + } + } + + companion object { + private val allowedImports = listOf("com.beust", "java") + private val disallowedImports = listOf("com.beust.kobalt.plugin") + + fun isAllowedImport(line: String) : Boolean { + return allowedImports.any { line.contains(it) } && !disallowedImports.any { line.contains(it) } + } + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/CheckVersions.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/CheckVersions.kt new file mode 100644 index 00000000..d15e8056 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/CheckVersions.kt @@ -0,0 +1,53 @@ +package com.beust.kobalt.misc + +import com.beust.kobalt.KobaltException +import com.beust.kobalt.api.Project +import com.beust.kobalt.maven.DependencyManager +import com.beust.kobalt.maven.MavenId +import com.beust.kobalt.maven.aether.AetherDependency +import com.beust.kobalt.maven.aether.KobaltMavenResolver +import javax.inject.Inject + +/** + * Find out if any newer versions of the dependencies are available. + */ +class CheckVersions @Inject constructor(val depManager: DependencyManager, + val executors : KobaltExecutors, val resolver: KobaltMavenResolver) { + + fun run(projects: List) = projects.forEach { run(it) } + + fun run(project: Project) { + val executor = executors.newExecutor("CheckVersions", 5) + + val newVersions = hashSetOf() + listOf(project.compileDependencies, project.testDependencies).forEach { cds -> + cds.forEach { dep -> + if (MavenId.isMavenId(dep.id)) { + try { + val latestDep = depManager.create(dep.shortId, false, project.directory) + val artifact = (latestDep as AetherDependency).artifact + val rangeResult = resolver.resolveRange(artifact) + + if (rangeResult != null) { + val highest = rangeResult.highestVersion?.toString() + if (highest != null && highest != dep.id + && StringVersion(highest) > StringVersion(dep.version)) { + newVersions.add(artifact.groupId + ":" + artifact.artifactId + ":" + highest) + } + } + } catch(e: KobaltException) { + kobaltLog(1, " Cannot resolve ${dep.shortId}. ignoring") + } + } + } + } + + if (newVersions.size > 0) { + kobaltLog(1, " New versions found:") + newVersions.forEach { kobaltLog(1, " $it") } + } else { + kobaltLog(1, " All dependencies up to date") + } + executor.shutdown() + } +} diff --git a/src/main/kotlin/com/beust/kobalt/misc/CountingFileRequestBody.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/CountingFileRequestBody.kt similarity index 72% rename from src/main/kotlin/com/beust/kobalt/misc/CountingFileRequestBody.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/CountingFileRequestBody.kt index fd90564c..ca64d3d6 100644 --- a/src/main/kotlin/com/beust/kobalt/misc/CountingFileRequestBody.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/CountingFileRequestBody.kt @@ -1,7 +1,7 @@ package com.beust.kobalt.misc -import com.squareup.okhttp.MediaType -import com.squareup.okhttp.RequestBody +import okhttp3.MediaType +import okhttp3.RequestBody import okio.BufferedSink import okio.Okio import java.io.File @@ -19,15 +19,17 @@ class CountingFileRequestBody(val file: File, val contentType: String, override fun contentType() = MediaType.parse(contentType) override fun writeTo(sink: BufferedSink) { - Okio.source(file).use { source -> - var total = 0L - var read: Long = source.read(sink.buffer(), SEGMENT_SIZE) + file.inputStream().use { fis -> + Okio.source(fis).use { source -> + var total = 0L + var read: Long = source.read(sink.buffer(), SEGMENT_SIZE) - while (read != -1L) { - total += read - sink.flush(); - listenerCallback(total) - read = source.read(sink.buffer(), SEGMENT_SIZE) + while (read != -1L) { + total += read + sink.flush(); + listenerCallback(total) + read = source.read(sink.buffer(), SEGMENT_SIZE) + } } } } @@ -52,7 +54,7 @@ class CountingFileRequestBody(val file: File, val contentType: String, // // .post(requestBody) // .build(); // -// val response = OkHttpClient().newCall(request).execute() +// val response = OkHttpClient.Builder().proxy(Kobalt.proxyConfig.toProxy()).build().newCall(request).execute() // if (! response.isSuccessful) { // println("ERROR") // } else { diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Git.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Git.kt new file mode 100644 index 00000000..cf6b5885 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Git.kt @@ -0,0 +1,52 @@ +package com.beust.kobalt.misc + +import com.beust.kobalt.TaskResult +import com.beust.kobalt.api.Project +import com.google.inject.Inject +import java.io.File + +class Git @Inject constructor() { + fun maybeTagRelease(project: Project, uploadResult: TaskResult, enabled: Boolean, annotated: Boolean, + push: Boolean, tag: String, message: String) : TaskResult { + val result = + if (uploadResult.success && enabled) { + val tagSuccess = tagRelease(project, annotated, push, tag, message) + if (! tagSuccess) { + TaskResult(false, errorMessage = "Couldn't tag the project") + } else { + TaskResult() + } + } else { + TaskResult() + } + return result + } + + private fun tagRelease(project: Project, annotated: Boolean, push: Boolean, tag: String, message: String) : Boolean { + val version = if (tag.isNullOrBlank()) project.version else tag + val success = try { + log(2, "Tagging this release as \"$version\"") + val repo = org.eclipse.jgit.storage.file.FileRepositoryBuilder() + .setGitDir(File(KFiles.joinDir(project.directory, ".git"))) + .readEnvironment() + .findGitDir() + .build() + val git = org.eclipse.jgit.api.Git(repo) + // jGit library will complain and not tag if setAnnotated(false) + var ref = if (annotated) { + git.tag().setAnnotated(annotated).setName(version).setMessage(message).call() + } else { + git.tag().setName(version).setMessage(message).call() + } + if (push) { + git.push().setPushTags().call() + } + true + } catch(ex: Exception) { + warn("Couldn't create tag ${version}: ${ex.message}", ex) + false + } + + return success + } +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/GithubApi2.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/GithubApi2.kt new file mode 100644 index 00000000..b33286e0 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/GithubApi2.kt @@ -0,0 +1,171 @@ +package com.beust.kobalt.misc + +import com.beust.kobalt.Args +import com.beust.kobalt.KobaltException +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.internal.DocUrl +import com.beust.kobalt.internal.KobaltSettings +import com.beust.kobalt.internal.build.VersionCheckTimestampFile +import com.beust.kobalt.maven.Http +import com.beust.kobalt.maven.aether.Exceptions +import com.google.gson.Gson +import com.google.gson.JsonParser +import com.google.gson.annotations.SerializedName +import com.google.inject.Inject +import okhttp3.OkHttpClient +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.* +import rx.Observable +import java.io.File +import java.time.Duration +import java.time.Instant +import java.util.* +import java.util.concurrent.Callable +import java.util.concurrent.Future + +class GithubApi2 @Inject constructor( + val executors: KobaltExecutors, val localProperties: LocalProperties, val http: Http, + val settings:KobaltSettings, val args: Args) { + + companion object { + const val PROPERTY_ACCESS_TOKEN = "github.accessToken" + const val PROPERTY_USERNAME = "github.username" + } + + private val DOC_URL = DocUrl.PUBLISH_PLUGIN_URL + + // + // JSON mapped classes that get sent up and down + // + class CreateRelease(@SerializedName("tag_name") var tagName: String? = null, + var name: String? = tagName) + class CreateReleaseResponse(var id: String? = null, @SerializedName("upload_url") var uploadUrl: String?) + class UploadAssetResponse(var id: String? = null, val name: String? = null) + class ReleasesResponse(@SerializedName("tag_name") var tagName: String? = null, + var name: String? = tagName) + + interface Api { + @POST("/repos/{owner}/{repo}/releases") + fun createRelease(@Path("owner") owner: String, + @Path("repo") repo: String, + @Query("access_token") accessToken: String, + @Body createRelease: CreateRelease): Call + + @GET("/repos/{owner}/{repo}/releases") + fun getReleases(@Path("owner") owner: String, + @Path("repo") repo: String, + @Query("access_token") accessToken: String): Call> + + @GET("/repos/{owner}/{repo}/releases") + fun getReleasesNoAuth(@Path("owner") owner: String, + @Path("repo") repo: String): Call> + } + + // + // Read only Api + // + private val service = Retrofit.Builder() + .client(OkHttpClient.Builder().proxy(settings.proxyConfigs?.firstOrNull()?.toProxy()).build()) + .baseUrl("https://api.github.com") + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(Api::class.java) + + // JSON Retrofit error + class Error(val code: String) + class RetrofitError(var message: String = "", var errors : List = arrayListOf()) + + fun uploadRelease(packageName: String, tagName: String, zipFile: File) { + kobaltLog(1, "Uploading release ${zipFile.name}") + + val username = localProperties.get(PROPERTY_USERNAME, DOC_URL) + val accessToken = localProperties.get(PROPERTY_ACCESS_TOKEN, DOC_URL) + val response = service.createRelease(username, packageName, accessToken, CreateRelease(tagName)) + .execute() + val code = response.code() + if (code != Http.CREATED) { + val error = Gson().fromJson(response.errorBody()?.string(), RetrofitError::class.java) + throw KobaltException("Couldn't upload release, ${error.message}: " + error.errors[0].code) + } else { + val body = response.body() + + uploadAsset(accessToken, body?.uploadUrl!!, Http.TypedFile("application/zip", zipFile), tagName) + .toBlocking() + .forEach { action -> + kobaltLog(1, "\n${zipFile.name} successfully uploaded") + } + } + } + + private fun uploadAsset(token: String, uploadUrl: String, typedFile: Http.TypedFile, tagName: String) + : Observable { + val strippedUrl = uploadUrl.substring(0, uploadUrl.indexOf("{")) + val fileName = typedFile.file.name + val url = "$strippedUrl?name=$fileName&label=$fileName" + val headers = okhttp3.Headers.of("Authorization", "token $token") + val totalSize = typedFile.file.length() + http.uploadFile(url = url, file = typedFile, headers = headers, post = true, // Github requires POST + progressCallback = http.percentProgressCallback(totalSize)) + + return Observable.just(UploadAssetResponse(tagName, tagName)) + } + + val latestKobaltVersion: Future + get() { + val callable = Callable { + var result = Kobalt.version + if (! args.dev && Duration.ofMinutes(10L) > + Duration.between(VersionCheckTimestampFile.timestamp, Instant.now())) { + kobaltLog(2, "Skipping GitHub latest release check, too soon.") + } else { + val username = localProperties.getNoThrows(PROPERTY_USERNAME) + val accessToken = localProperties.getNoThrows(PROPERTY_ACCESS_TOKEN) + try { + val req = + if (username != null && accessToken != null) { + service.getReleases(username, "kobalt", accessToken) + } else { + service.getReleasesNoAuth("cbeust", "kobalt") + } + val ex = req.execute() + val errorBody = ex.errorBody() + if (errorBody != null) { + val jsonError = JsonParser().parse(errorBody.string()) + warn("Couldn't call Github.getReleases(): $jsonError") + } else { + val releases = ex.body() + if (releases != null) { + releases.firstOrNull()?.let { + result = try { + listOf(it.name, it.tagName).filterNotNull().first { !it.isBlank() } + } catch(ex: NoSuchElementException) { + throw KobaltException("Couldn't find the latest release") + } + } + } else { + warn("Didn't receive any body in the response to GitHub.getReleases()") + } + } + } catch(e: Exception) { + kobaltLog(1, "Couldn't retrieve releases from github: " + e.message) + Exceptions.printStackTrace(e) +// val error = parseRetrofitError(e) +// val details = if (error.errors != null) { +// error.errors[0] +// } else { +// null +// } +// // TODO: If the credentials didn't work ("bad credentials"), should start again +// // using cbeust/kobalt, like above. Right now, just bailing. +// kobaltLog(2, "Couldn't retrieve releases from github, ${error.message ?: e}: " +// + details?.code + " field: " + details?.field) + } + } + result + } + + return executors.miscExecutor.submit(callable) + } +} \ No newline at end of file diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Io.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Io.kt new file mode 100644 index 00000000..8d2b0580 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Io.kt @@ -0,0 +1,84 @@ +package com.beust.kobalt.misc + +import java.io.File +import java.nio.file.* + +class Io(val dryRun: Boolean = false) { + fun mkdirs(dir: String) { + kobaltLog("mkdirs $dir") + if (! dryRun) { + File(dir).mkdirs() + } + } + + fun rm(path: String) { + kobaltLog("rm $path") + + if (! dryRun) { + File(path).deleteRecursively() + } + } + + fun moveFile(from: File, toDir: File) { + kobaltLog("mv $from $toDir") + if (! dryRun) { + require(from.exists(), { -> "$from should exist" }) + require(from.isFile, { -> "$from should be a file" }) + require(toDir.isDirectory, { -> "$toDir should be a file"}) + + val toFile = File(toDir, from.name) + Files.move(Paths.get(from.absolutePath), Paths.get(toFile.absolutePath), StandardCopyOption.ATOMIC_MOVE) + } + } + + fun rename(from: File, to: File) { + kobaltLog("rename $from $to") + moveFile(from, to.parentFile) + if (from.name != to.name) { + File(to, from.name).renameTo(to) + } + } + + fun copyDirectory(from: File, toDir: File) { + kobaltLog("cp -r $from $toDir") + + if (! dryRun) { + KFiles.copyRecursively(from, toDir) + require(from.exists(), { -> "$from should exist" }) + require(from.isDirectory, { -> kobaltLog(1, "$from should be a directory")}) + require(toDir.isDirectory, { -> kobaltLog(1, "$toDir should be a file")}) + + } + } + + fun rmDir(dir: File, keep: (File) -> Boolean = { t -> false }) = rmDir(dir, keep, " ") + + private fun rmDir(dir: File, keep: (File) -> Boolean, indent : String) { + kobaltLog("rm -rf $dir") + + require(dir.isDirectory, { -> kobaltLog(1, "$dir should be a directory")}) + + dir.listFiles({ p0 -> ! keep(p0!!) }).forEach { + if (it.isDirectory) { + rmDir(it, keep, indent + " ") + it.deleteRecursively() + } + else { + kobaltLog(indent + "rm $it") + if (! dryRun) it.delete() + } + } + } + + fun mkdir(dir: File) { + kobaltLog("mkdir $dir") + if (! dryRun) { + dir.mkdirs() + } + } + + private fun kobaltLog(s: String) { + kobaltLog(1, "[Io] $s") + } + +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/JarUtils.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/JarUtils.kt new file mode 100644 index 00000000..ba83b0a6 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/JarUtils.kt @@ -0,0 +1,107 @@ +package com.beust.kobalt.misc + +import com.beust.kobalt.From +import com.beust.kobalt.IFileSpec +import com.beust.kobalt.IncludedFile +import com.beust.kobalt.To +import com.beust.kobalt.archive.MetaArchive +import com.google.common.io.CharStreams +import java.io.File +import java.io.FileOutputStream +import java.io.InputStreamReader +import java.util.jar.JarFile +import java.util.zip.ZipFile + +class JarUtils { + companion object { + val DEFAULT_HANDLER: (Exception) -> Unit = { ex: Exception -> + // Ignore duplicate entry exceptions + if (! ex.message?.contains("duplicate")!!) { + throw ex + } + } + + fun addFiles(directory: String, files: List, metaArchive: MetaArchive, + expandJarFiles: Boolean, + onError: (Exception) -> Unit = DEFAULT_HANDLER) { + files.forEach { + addSingleFile(directory, it, metaArchive, expandJarFiles, onError) + } + } + + fun addSingleFile(directory: String, file: IncludedFile, metaArchive: MetaArchive, + expandJarFiles: Boolean, onError: (Exception) -> Unit = DEFAULT_HANDLER) { + val foundFiles = file.allFromFiles(directory) + foundFiles.forEach { foundFile -> + + // Turn the found file into the local physical file that will be put in the jar file + val fromFile = file.from(foundFile.path) + val localFile = if (fromFile.isAbsolute) fromFile + else File(directory, fromFile.path) + + if (!localFile.exists()) { + throw AssertionError("File should exist: $localFile") + } + + if (foundFile.isDirectory) { + kobaltLog(2, " Writing contents of directory $foundFile") + + // Directory + val includedFile = IncludedFile(From(""), To(""), listOf(IFileSpec.GlobSpec("**"))) + addSingleFile(localFile.path, includedFile, metaArchive, expandJarFiles) + } else { + try { + if (file.expandJarFiles && foundFile.name.endsWith(".jar") && !file.from.contains("resources")) { + kobaltLog(2, " Writing contents of jar file $foundFile") + metaArchive.addArchive(foundFile) + } else { + val toPath = File(file.to).normalize().path + val finalPath = if (toPath.isEmpty()) null else (toPath + "/") + metaArchive.addFile(File(directory, fromFile.path), foundFile, finalPath) + } + } catch(ex: Exception) { + onError(ex) + } + } + } + } + + fun extractTextFile(zip : ZipFile, fileName: String) : String? { + val enumEntries = zip.entries() + while (enumEntries.hasMoreElements()) { + val file = enumEntries.nextElement() + if (file.name == fileName) { + kobaltLog(2, "Found $fileName in ${zip.name}") + zip.getInputStream(file).use { ins -> + return CharStreams.toString(InputStreamReader(ins, "UTF-8")) + } + } + } + return null + } + + fun extractJarFile(file: File, destDir: File) = extractZipFile(JarFile(file), destDir) + + fun extractZipFile(zipFile: ZipFile, destDir: File) { + val enumEntries = zipFile.entries() + while (enumEntries.hasMoreElements()) { + val file = enumEntries.nextElement() + val f = File(destDir.path + File.separator + file.name) + if (file.isDirectory) { + f.mkdir() + continue + } + + zipFile.getInputStream(file).use { ins -> + f.parentFile.mkdirs() + FileOutputStream(f).use { fos -> + while (ins.available() > 0) { + fos.write(ins.read()) + } + } + } + } + } + } +} + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KFiles.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KFiles.kt new file mode 100644 index 00000000..fcf5b86a --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KFiles.kt @@ -0,0 +1,383 @@ +package com.beust.kobalt.misc + +import com.beust.kobalt.* +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.api.Project +import com.beust.kobalt.maven.Md5 +import org.apache.commons.io.FileUtils +import java.io.* +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import java.util.* +import java.util.jar.JarInputStream +import java.util.regex.Pattern + + +class KFiles { + /** + * This actually returns a list of strings because in development mode, we are not pointing to a single + * jar file but to a set of classes/directories. + */ + val kobaltJar : List + get() { + val PATTERN = Pattern.compile("kobalt-([-.0-9]+)") + + fun latestInstalledVersion() : StringVersion { + val versions = File(distributionsDir).listFiles().map { it.name }.map { + val matcher = PATTERN.matcher(it) + val result = + if (matcher.matches()) matcher.group(1) + else null + result + }.filterNotNull().map(::StringVersion) + Collections.sort(versions, reverseOrder()) + return versions[0] + } + + val envJar = System.getenv("KOBALT_JAR") + if (envJar != null) { + debug("Using kobalt jar $envJar") + return listOf(File(envJar).absolutePath) + } else { + val jar = joinDir(distributionsDir, "kobalt-" + Kobalt.version, + "kobalt/wrapper/kobalt-" + Kobalt.version + ".jar") + val jarFile = File(jar) + if (jarFile.exists()) { + return listOf(jarFile.absolutePath) + } else { + // In development mode, keep your kobalt.properties version to a nonexistent version + // kobalt.properties: kobalt.version=0.828 + // kobalt-wrapper.properties: kobalt.version=0.827 + // When Kobalt can't find the newest jar file, it will instead use the classes produced by IDEA + // in the directories specified here: + val previousVersion = latestInstalledVersion().version + val previousJar = joinDir(distributionsDir, "kobalt-" + previousVersion, + "kobalt/wrapper/kobalt-$previousVersion.jar") + latestInstalledVersion() + val result = listOf("", "modules/kobalt-plugin-api", "modules/wrapper").map { + File(homeDir(KFiles.joinDir("kotlin", "kobalt", it, "kobaltBuild", "classes"))) //kobalt build dirs + .absolutePath + } + listOf("modules/kobalt", "modules/kobalt-plugin-api", "modules/wrapper").map { + File(homeDir(KFiles.joinDir("kotlin", "kobalt", it, "target", "classes"))) //maven build dirs + .absolutePath + } + listOf(previousJar) + debug("Couldn't find ${jarFile.absolutePath}, using\n " + result.joinToString(" ")) + return result.filter { File(it).exists() } + } + } + } + + init { + File(KOBALT_DOT_DIR).mkdirs() + } + + companion object { + const val KOBALT_DOT_DIR : String = ".kobalt" + val KOBALT_DIR : String = "kobalt" + val HOME_KOBALT_DIR = makeDir(homeDir(".config", KOBALT_DIR)) + val KOBALT_BUILD_DIR = "kobaltBuild" + + /** Where all the .zip files are extracted */ + val distributionsDir = homeDir(KOBALT_DOT_DIR, "wrapper", "dist") + + // Directories under ./.kobalt + val SCRIPT_BUILD_DIR : String = "build" + val CLASSES_DIR : String = "classes" + + /** Where build file and support source files go, under KOBALT_DIR */ + private val SRC = "src" + + val TEST_CLASSES_DIR : String = "test-classes" + + val NATIVES_DIR : String = "native" + + fun nativeBuildDir(project: Project) = KFiles.joinDir(project.directory, project.buildDirectory, NATIVES_DIR) + + fun generatedSourceDir(project: Project, variant: Variant, name: String) = + KFiles.joinDir(project.directory, project.buildDirectory, "generated", "source", name, + variant.toIntermediateDir()) + + fun buildDir(project: Project) = KFiles.makeDir(project.directory, project.buildDirectory) + + /** + * Join the paths elements with the file separator. + */ + fun joinDir(paths: List): String = paths.joinToString(File.separator) + + /** + * Join the paths elements with the file separator. + */ + fun joinDir(vararg ts: String): String = ts.toMutableList().joinToString(File.separator) + + val LIBS_DIR = "libs" + + /** + * Where assemblies get generated ("kobaltBuild/libs") + */ + fun libsDir(project: Project): String = KFiles.makeDir(KFiles.buildDir(project).path, LIBS_DIR).path + + /** + * The paths elements are expected to be a directory. Make that directory and join the + * elements with the file separator. + */ + fun joinAndMakeDir(paths: List) = joinDir(paths).apply { File(this).mkdirs() } + + /** + * The paths elements are expected to be a directory. Make that directory and join the + * elements with the file separator. + */ + fun joinAndMakeDir(vararg ts: String) = joinAndMakeDir(ts.toList()) + + /** + * The paths elements are expected to be a file. Make that parent directory of that file and join the + * elements with the file separator. + */ + fun joinFileAndMakeDir(vararg ts: String) = joinDir(joinAndMakeDir(ts.slice(0..ts.size - 2)), ts[ts.size - 1]) + + fun fixSlashes(f: File) = f.normalize().path.replace('\\', '/') + fun fixSlashes(s: String) = s.replace('\\', '/') + + fun makeDir(dir: String, s: String? = null) = + (if (s != null) File(dir, s) else File(dir)).apply { mkdirs() } + + fun findRecursively(rootDir: File) : List = + findRecursively(rootDir, arrayListOf(), { _ -> true }) + + fun findRecursively(rootDir: File, directories: List, + function: Function1): List { + val result = arrayListOf() + + val allDirs = arrayListOf() + if (directories.isEmpty()) { + allDirs.add(rootDir) + } else { + allDirs.addAll(directories.map { File(rootDir, it.path) }) + } + + val seen = hashSetOf() + allDirs.forEach { dir -> + if (! dir.exists()) { + kobaltLog(2, "Couldn't find directory $dir") + } else if (! dir.isDirectory) { + throw IllegalArgumentException("$dir is not a directory") + } else { + val files = findRecursively(dir, function) + files.map { Paths.get(it) }.forEach { + val rel = Paths.get(dir.path).relativize(it) + if (! seen.contains(rel)) { + result.add(File(dir, rel.toFile().path).path) + seen.add(rel) + } else { + kobaltLog(2, "Skipped file already seen in previous flavor: $rel") + } + } + } + } + // Return files relative to rootDir + val r = result.map { it.substring(rootDir.path.length + 1)} + return r + } + + fun findRecursively(directory: File, function: Function1): List { + val result = arrayListOf() + directory.listFiles().forEach { + if (it.isFile && function(it.path)) { + result.add(it.path) + } else if (it.isDirectory) { + result.addAll(findRecursively(it, function)) + } + } + return result + } + + /** + * List the files contained in a directory or a jar file. + */ + fun listFiles(file: File, block: (String) -> Unit) { + if (file.isDirectory) { + KFiles.findRecursively(file).forEach { + block(it) + } + } else if (file.name.endsWith(".jar")) { + FileInputStream(file).use { + JarInputStream(it).use { stream -> + var entry = stream.nextJarEntry + while (entry != null) { + block(entry.name) + entry = stream.nextJarEntry; + } + } + } + + } else { + throw KobaltException("Can't list files of a file: " + file) + } + } + + /** + * The build location for build scripts is .kobalt/build + */ + fun findBuildScriptDir(parent: String = ".") : File { + val result = File(joinAndMakeDir(parent, KFiles.dotKobaltDir.path, KFiles.SCRIPT_BUILD_DIR)) + kobaltLog(2, " Script jar files in: $result") + return result + } + + fun saveFile(file: File, text: String) { + var canCreate = true + with(file.absoluteFile.parentFile) { + if (!exists()) { + val success = mkdirs() + if (!success) { + warn("Couldn't create directory to save $file") + canCreate = false + } + } + } + if (canCreate) { + file.writeText(text) + kobaltLog(2, "Created $file") + } + } + + private fun isWindows() = System.getProperty("os.name").contains("Windows") + + fun copy(from: Path?, to: Path?, option: StandardCopyOption = StandardCopyOption.REPLACE_EXISTING) { + try { + if (from != null && to != null) { + if (!Files.exists(to) || Md5.toMd5(from.toFile()) != Md5.toMd5(to.toFile())) { + kobaltLog(3, "Copy from $from to $to") + Files.copy(from, to, option) + } else { + kobaltLog(3, " Not copying, indentical files: $from $to") + } + } + } catch(ex: IOException) { + // Windows is anal about this + kobaltLog(1, "Couldn't copy $from to $to: ${ex.message}") + } + } + + fun copy(from: InputStream, to: OutputStream) { + var read = from.read() + while (read != -1) { + to.write(read) + read = from.read() + } + } + + fun createTempBuildFileInTempDirectory(deleteOnExit: Boolean = false) : File = + File(createTempDirectory("kobalt", deleteOnExit), Constants.BUILD_FILE_NAME).let { + if (deleteOnExit) it.deleteOnExit() + return it + } + + fun createTempDirectory(prefix : String = "kobalt", deleteOnExit: Boolean = false) : File = + Files.createTempDirectory(prefix).let { + if (deleteOnExit) it.toFile().deleteOnExit() + return it.toFile() + } + + fun src(filePath: String): String = KFiles.joinDir(KOBALT_DIR, SRC, filePath) + + fun makeDir(project: Project, suffix: String) : File { + return File(project.directory, project.buildDirectory + File.separator + suffix).apply { mkdirs() } + } + + fun makeOutputDir(project: Project) : File = makeDir(project, KFiles.CLASSES_DIR) + + fun makeOutputTestDir(project: Project) : File = makeDir(project, KFiles.TEST_CLASSES_DIR) + + fun isExcluded(file: String, excludes: Glob) = isExcluded(file, listOf(excludes)) + + fun isExcluded(file: File, excludes: List) = isExcluded(file.path, excludes) + + fun isExcluded(file: String, excludes: List): Boolean = excludes.any { it.matches(file) } + + /** + * TODO: cache these per project so we don't do it more than once. + */ + fun findSourceFiles(projectDirectory: String, sourceDirectories: Collection, + suffixes: List) : Set { + val result = hashSetOf() + sourceDirectories.forEach { source -> + val sourceDir = File(KFiles.joinDir(projectDirectory, source)) + if (sourceDir.exists()) { + KFiles.findRecursively(sourceDir, { file -> + val ind = file.lastIndexOf(".") + if (ind >= 0) { + val suffix = file.substring(ind + 1) + if (suffixes.contains(suffix)) { + result.add(file) + } + } + false + }) + } else { + kobaltLog(3, "Skipping nonexistent source directory $sourceDir") + } + } + return result + } + + fun isResource(name: String) = name.contains("res") || name.contains("resources") + + /** + * @return true as soon as a file meeting the condition is found. + */ + fun containsCertainFile(dir: File, condition: (File) -> Boolean) : Boolean { + if (dir.isDirectory) { + val directories = arrayListOf() + dir.listFiles().forEach { + if (condition(it)) return true + if (it.isDirectory) directories.add(it) + } + return directories.any { containsCertainFile(it, condition) } + } else { + return false + } + } + + val dotKobaltDir = File(KFiles.joinAndMakeDir(KFiles.KOBALT_DOT_DIR)) + + /** + * Turn the IncludedFiles into actual Files + */ + fun materializeIncludedFiles(project: Project, includedFiles: List) : List { + val result = includedFiles.fold(arrayListOf()) { files, includedFile: IncludedFile -> + val foundFiles = includedFile.allFromFiles(project.directory) + val absFiles = foundFiles.map { + if (it.isAbsolute) { + it + } else if (File(includedFile.from).isAbsolute) { + File(includedFile.from, it.path) + } else { + File(KFiles.joinDir(project.directory, includedFile.from, it.path)) + } + } + files.addAll(absFiles) + files + } + return result + } + + fun copyRecursively(from: File, to: File, replaceExisting: Boolean = true, deleteFirst: Boolean = false) { +// fun copy(relativePath: String, sourceDir: File, targetDir: File) = +// sourceDir.resolve(relativePath).copyRecursively(targetDir.resolve(relativePath), overwrite = true) + if (from.isFile) FileUtils.copyFileToDirectory(from, to) + else FileUtils.copyDirectory(from, to) + } + + } + + fun findRecursively(directory: File, function: Function1): List { + return KFiles.findRecursively(directory, function) + } + + fun findRecursively(rootDir: File, directories: List, + function: Function1): List { + return KFiles.findRecursively(rootDir, directories, function) + } +} diff --git a/src/main/kotlin/com/beust/kobalt/misc/KobaltExecutors.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltExecutors.kt similarity index 83% rename from src/main/kotlin/com/beust/kobalt/misc/KobaltExecutors.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltExecutors.kt index eb41ec5b..1dc9ab41 100644 --- a/src/main/kotlin/com/beust/kobalt/misc/KobaltExecutors.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltExecutors.kt @@ -5,11 +5,10 @@ import java.util.concurrent.* class NamedThreadFactory(val n: String) : ThreadFactory { private val PREFIX = "K-" - public val name: String + val name: String get() = PREFIX + n - override - public fun newThread(r: Runnable) : Thread { + override fun newThread(r: Runnable) : Thread { val result = Thread(r) result.name = name + "-" + result.id return result @@ -20,7 +19,7 @@ class KobaltExecutor(name: String, threadCount: Int) : ThreadPoolExecutor(threadCount, threadCount, 5L, TimeUnit.SECONDS, LinkedBlockingQueue(), NamedThreadFactory(name)) { - override protected fun afterExecute(r: Runnable, t: Throwable?) { + override fun afterExecute(r: Runnable, t: Throwable?) { super.afterExecute(r, t) var ex : Throwable? = null if (t == null && r is Future<*>) { @@ -35,13 +34,13 @@ class KobaltExecutor(name: String, threadCount: Int) } } if (ex != null) { - error(ex.toString()) + error(ex.toString(), ex) } } } -public class KobaltExecutors { - public fun newExecutor(name: String, threadCount: Int) : ExecutorService +class KobaltExecutors { + fun newExecutor(name: String, threadCount: Int) : ExecutorService = KobaltExecutor(name, threadCount) val dependencyExecutor = newExecutor("Dependency", 5) @@ -67,14 +66,14 @@ public class KobaltExecutors { progress(r) result.add(r) remainingMs -= (System.currentTimeMillis() - start) - log(2, "Received $r, remaining: $remainingMs ms") + kobaltLog(3, "Received $r, remaining: $remainingMs ms") i++ } if (remainingMs < 0) { warn("Didn't receive all the results in time: $i / ${tasks.size}") } else { - log(2, "Received all results in ${maxMs - remainingMs} ms") + kobaltLog(2, "Received all results in ${maxMs - remainingMs} ms") } executor.shutdown() diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltLogger.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltLogger.kt new file mode 100644 index 00000000..e2bd89de --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltLogger.kt @@ -0,0 +1,118 @@ +package com.beust.kobalt.misc + +import com.beust.kobalt.Args +import com.beust.kobalt.AsciiArt +import com.beust.kobalt.Constants +import com.beust.kobalt.KobaltException +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.maven.aether.Exceptions +import java.lang.Exception +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +fun Any.log(level: Int, text: CharSequence, newLine : Boolean = true) { + if (level <= KobaltLogger.LOG_LEVEL && !KobaltLogger.isQuiet) { + KobaltLogger.logger.log(javaClass.simpleName, text, newLine) + } +} + +fun Any.kobaltLog(level: Int, text: CharSequence, newLine : Boolean = true) = log(level, text, newLine) +fun Any.kobaltWarn(text: CharSequence) = warn(text) +fun Any.kobaltError(text: CharSequence) = error(text) +fun Any.kobaltLog(tag: String, text: CharSequence, newLine : Boolean = true) { + if (Kobalt.INJECTOR.getInstance(Args::class.java).logTags.split(',').contains(tag)) { + log(1, text, newLine) + } +} + +fun Any.logWrap(level: Int, text1: CharSequence, text2: CharSequence, function: () -> Unit) { + if (level <= KobaltLogger.LOG_LEVEL && !KobaltLogger.isQuiet) { + KobaltLogger.logger.log(javaClass.simpleName, text1, newLine = false) + } + function() + if (level <= KobaltLogger.LOG_LEVEL && !KobaltLogger.isQuiet) { + KobaltLogger.logger.log(javaClass.simpleName, text2, newLine = true) + } +} + +fun Any.debug(text: CharSequence) { + KobaltLogger.logger.debug(javaClass.simpleName, text) +} + +fun Any.warn(text: CharSequence, exception: Exception? = null) { + KobaltLogger.logger.warn(javaClass.simpleName, text, exception) +} + +fun Any.kobaltError(text: CharSequence, e: Throwable? = null) = error(text, e) + +fun Any.error(text: CharSequence, e: Throwable? = null) { + KobaltLogger.logger.error(javaClass.simpleName, text, e) +} + +object KobaltLogger { + var LOG_LEVEL: Int = 1 + + val isQuiet: Boolean get() = (LOG_LEVEL == Constants.LOG_QUIET_LEVEL) + + val logger: Logger get() = + if (Kobalt.context != null) { + Logger(Kobalt.context!!.args.dev) + } else { + Logger(false) + } + + fun setLogLevel(args: Args) { + LOG_LEVEL = when { + args.log < Constants.LOG_QUIET_LEVEL -> Constants.LOG_DEFAULT_LEVEL + args.log > Constants.LOG_MAX_LEVEL -> Constants.LOG_MAX_LEVEL + else -> args.log + } + } +} + +class Logger(val dev: Boolean) { + val FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss.SSS") + + private fun getPattern(shortTag: String, shortMessage: CharSequence, longMessage: CharSequence, tag: String) = + if (dev) { + val ts = LocalDateTime.now().format(FORMAT) + "$shortTag/$ts [" + Thread.currentThread().name + "] $tag - $shortMessage" + } else { + longMessage + } + + fun debug(tag: String, message: CharSequence) = + println(getPattern("D", message, message, tag)) + + fun error(tag: String, message: CharSequence, e: Throwable? = null) { + val docUrl = if (e is KobaltException && e.docUrl != null) e.docUrl else null + val text = + if (message.isNotBlank()) message + else if (e != null && (! e.message.isNullOrBlank())) e.message + else { e?.toString() } + val shortMessage = "***** E $text " + if (docUrl != null) " Documentation: $docUrl" else "" + val longMessage = "*****\n***** ERROR $text\n*****" + + println(AsciiArt.errorColor(getPattern("E", shortMessage, longMessage, tag))) + if (KobaltLogger.LOG_LEVEL > 1 && e != null) { + Exceptions.printStackTrace(e) + } + } + + fun warn(tag: String, message: CharSequence, e: Throwable? = null) { + val fullMessage = "***** WARNING " + + if (message.isNotBlank()) message + else if (e != null && (!e.message.isNullOrBlank())) e.message + else e?.toString() + println(AsciiArt.Companion.warnColor(getPattern("W", fullMessage, fullMessage, tag))) + if (KobaltLogger.LOG_LEVEL > 1 && e != null) { + Exceptions.printStackTrace(e) + } + } + + fun log(tag: String, message: CharSequence, newLine: Boolean) = + with(getPattern("L", message, message, tag)) { + if (newLine) println(this) + else print("\r" + this) + } +} diff --git a/src/main/kotlin/com/beust/kobalt/misc/KobaltWrapperProperties.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltWrapperProperties.kt similarity index 90% rename from src/main/kotlin/com/beust/kobalt/misc/KobaltWrapperProperties.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltWrapperProperties.kt index 8af0da44..a7b5177d 100644 --- a/src/main/kotlin/com/beust/kobalt/misc/KobaltWrapperProperties.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltWrapperProperties.kt @@ -16,7 +16,7 @@ class KobaltWrapperProperties @Inject constructor() { private val PROPERTY_DOWNLOAD_URL = "kobalt.downloadUrl" fun create(version: String) { - log(2, "Creating $file with $version and ${defaultUrlFor(version)}") + kobaltLog(2, "Creating $file with $version and ${defaultUrlFor(version)}") KFiles.saveFile(file, listOf( "$PROPERTY_VERSION=$version" // "$PROPERTY_DOWNLOAD_URL=${defaultUrlFor(version)}" @@ -24,7 +24,7 @@ class KobaltWrapperProperties @Inject constructor() { } private fun defaultUrlFor(version: String) = - "http://beust.com/kobalt/kobalt-$version.zip" + "https://beust.com/kobalt/kobalt-$version.zip" private val file: File get() = File("$WRAPPER_DIR/$KOBALT_WRAPPER_PROPERTIES") diff --git a/src/main/kotlin/com/beust/kobalt/misc/LocalProperties.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/LocalProperties.kt similarity index 68% rename from src/main/kotlin/com/beust/kobalt/misc/LocalProperties.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/LocalProperties.kt index 371d106d..2cf0775e 100644 --- a/src/main/kotlin/com/beust/kobalt/misc/LocalProperties.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/LocalProperties.kt @@ -6,18 +6,16 @@ import java.nio.file.Files import java.nio.file.Paths import java.util.* +/** + * Encapsulate read access to local.properties. + */ @Singleton class LocalProperties { - var docUrl: String? = null val localProperties: Properties by lazy { val result = Properties() val filePath = Paths.get("local.properties") - if (! Files.exists(filePath)) { - throw KobaltException("Couldn't find a local.properties file", docUrl = docUrl) - } - filePath.let { path -> - if (Files.exists(path)) { + if (path.toFile().exists()) { Files.newInputStream(path).use { result.load(it) } @@ -27,10 +25,11 @@ class LocalProperties { result } + fun getNoThrows(name: String): String? = localProperties.getProperty(name) + fun get(name: String, docUrl: String? = null) : String { - this.docUrl = docUrl - val result = localProperties[name] + val result = getNoThrows(name) ?: throw KobaltException("Couldn't find $name in local.properties", docUrl = docUrl) - return result as String + return result } } diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/MainModule.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/MainModule.kt new file mode 100644 index 00000000..fcd316d9 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/MainModule.kt @@ -0,0 +1,15 @@ +package com.beust.kobalt.misc + +import com.google.inject.BindingAnnotation + +//@Singleton +//class TaskManagerProvider @Inject constructor(val plugins: Plugins) : Provider { +// override fun get(): TaskManager? { +// return TaskManager(plugins) +// } +//} + +@BindingAnnotation +@Retention(AnnotationRetention.RUNTIME) +annotation class DependencyExecutor + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/NewRunCommand.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/NewRunCommand.kt new file mode 100644 index 00000000..e105133b --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/NewRunCommand.kt @@ -0,0 +1,144 @@ +package com.beust.kobalt.misc + +import java.io.BufferedReader +import java.io.File +import java.io.InputStream +import java.io.InputStreamReader +import java.util.concurrent.TimeUnit + +class RunCommandInfo { + lateinit var command: String + var args : List = arrayListOf() + var directory : File = File("") + var env : Map = hashMapOf() + + /** + * Some commands fail but return 0, so the only way to find out if they failed is to look + * at the error stream. However, some commands succeed but output text on the error stream. + * This field is used to specify how errors are caught. + */ + var useErrorStreamAsErrorIndicator : Boolean = true + var useInputStreamAsErrorIndicator : Boolean = false + var ignoreExitValue : Boolean = false + + var errorCallback: Function1, Unit> = NewRunCommand.DEFAULT_ERROR + var successCallback: Function1, Unit> = NewRunCommand.DEFAULT_SUCCESS + + var isSuccess: (Boolean, List, List) -> Boolean = { + isSuccess: Boolean, + input: List, + error: List -> + var hasErrors = ! isSuccess + if (useErrorStreamAsErrorIndicator && ! hasErrors) { + hasErrors = hasErrors || error.size > 0 + } + if (useInputStreamAsErrorIndicator && ! hasErrors) { + hasErrors = hasErrors || input.size > 0 + } + + ! hasErrors + } + + var containsErrors: ((List) -> Boolean)? = null +} + +fun runCommand(init: RunCommandInfo.() -> Unit) = NewRunCommand(RunCommandInfo().apply { init() }).invoke() + +open class NewRunCommand(val info: RunCommandInfo) { + + companion object { + val DEFAULT_SUCCESS = { output: List -> } + // val DEFAULT_SUCCESS_VERBOSE = { output: List -> kobaltLog(2, "Success:\n " + output.joinToString + // ("\n"))} + // val defaultSuccess = DEFAULT_SUCCESS + val DEFAULT_ERROR = { output: List -> + kobaltError(output.joinToString("\n ")) + } + } + + // fun useErrorStreamAsErrorIndicator(f: Boolean) : RunCommand { + // useErrorStreamAsErrorIndicator = f + // return this + // } + + fun invoke() : Int { + val allArgs = arrayListOf() + allArgs.add(info.command) + allArgs.addAll(info.args) + + val pb = ProcessBuilder(allArgs) + pb.directory(info.directory) + kobaltLog(2, "Running command in directory ${info.directory.absolutePath}" + + "\n " + allArgs.joinToString(" ").replace("\\", "/")) + pb.environment().let { pbEnv -> + info.env.forEach { + pbEnv.put(it.key, it.value) + } + } + + val process = pb.start() + + // Run the command and collect the return code and streams + val processFinished = process.waitFor(120, TimeUnit.SECONDS) + + if (!processFinished) + kobaltError("process timed out!") + + val input = + if (process.inputStream.available() > 0) fromStream(process.inputStream) + else listOf() + val error = + if (process.errorStream.available() > 0) fromStream(process.errorStream) + else listOf() + + kobaltLog(3, "info contains errors: " + (info.containsErrors != null)) + + // Check to see if the command succeeded + val isSuccess = + if (info.containsErrors != null) ! info.containsErrors!!(error) + else isSuccess(if (info.ignoreExitValue) true else processFinished, input, error) + + if (isSuccess) { + if (!info.useErrorStreamAsErrorIndicator) { + info.successCallback(error + input) + } else { + info.successCallback(input) + } + } else { + info.errorCallback(error + input) + } + + return if (isSuccess) 0 else 1 + } + + /** + * Subclasses can override this method to do their own error handling, since commands can + * have various ways to signal errors. + */ + open protected fun isSuccess(isSuccess: Boolean, input: List, error: List) : Boolean { + var hasErrors: Boolean = ! isSuccess + if (info.useErrorStreamAsErrorIndicator && ! hasErrors) { + hasErrors = hasErrors || error.isNotEmpty() + } + if (info.useInputStreamAsErrorIndicator && ! hasErrors) { + hasErrors = hasErrors || input.isNotEmpty() + } + + return ! hasErrors + } + + /** + * Turn the given InputStream into a list of strings. + */ + private fun fromStream(ins: InputStream) : List { + val result = arrayListOf() + val br = BufferedReader(InputStreamReader(ins)) + var line = br.readLine() + + while (line != null) { + result.add(line) + line = br.readLine() + } + return result + } +} diff --git a/src/main/kotlin/com/beust/kobalt/misc/Node.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Node.kt similarity index 64% rename from src/main/kotlin/com/beust/kobalt/misc/Node.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Node.kt index 4fd2f216..26707f03 100644 --- a/src/main/kotlin/com/beust/kobalt/misc/Node.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Node.kt @@ -1,10 +1,10 @@ package com.beust.kobalt.misc -public data class Node(val value: T) { +data class Node(val value: T) { val children = arrayListOf>() var parent: Node? = null - public fun addChildren(values: List>) { + fun addChildren(values: List>) { values.forEach { it.parent = this children.add(it) @@ -12,15 +12,15 @@ public data class Node(val value: T) { } private fun p(s: String) { - println(s) + kobaltLog(1, s) } - public fun dump(r: T, children: List>, indent: Int) { + fun dump(r: T, children: List>, indent: Int) { p(" ".repeat(indent) + r) children.forEach { dump(it.value, it.children, indent + 2) } } - public fun dump(indent: Int = 0) { + fun dump(indent: Int = 0) { dump(value, children, indent) } } diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/StringVersion.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/StringVersion.kt new file mode 100644 index 00000000..b421e558 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/StringVersion.kt @@ -0,0 +1,50 @@ +package com.beust.kobalt.misc + +import java.lang.Long +import java.lang.NumberFormatException +import java.util.* + +/** + * Compare string versions, e.g. "1.2.0", "0.9", etc... + */ +class StringVersion(val version: String) : Comparable { + override fun compareTo(other: StringVersion): Int { + val s1 = arrayListOf().apply { addAll(version.split('.')) } + val s2 = arrayListOf().apply { addAll(other.version.split('.')) } + + // Normalize both strings, so they have the same length, e.g. 1 -> 1.0.0 + val max = Math.max(s1.size, s2.size) + val shorterList : ArrayList = if (s1.size == max) s2 else s1 + repeat(max - shorterList.size) { + shorterList.add("0") + } + + // Compare each section + repeat(max) { index -> + try { + fun parse(s: String) = Long.parseLong(s.filter(Char::isDigit)) + + val v1 = parse(s1[index]) + val v2 = parse(s2[index]) + if (v1 < v2) return -1 + else if (v1 > v2) return 1 + } catch(ex: NumberFormatException) { + if (version == other.toString()) { + return 0 + } else { + log(2, "Couldn't parse version $version or $other") + return -1 + } + } + } + return 0 + } + + override fun equals(other: Any?) = + if (other is StringVersion) this.compareTo(other) == 0 + else false + + override fun hashCode() = version.hashCode() + + override fun toString() = version +} diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Strings.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Strings.kt new file mode 100644 index 00000000..01f39a17 --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Strings.kt @@ -0,0 +1,18 @@ +package com.beust.kobalt.misc + +import com.google.common.base.CharMatcher + +class Strings { + companion object { + fun pluralize(n:Int, string: String, plural: String = string + "s") = if (n != 1) plural else string + fun pluralizeAll(n:Int, string: String, plural: String = string + "s") = "$n " + pluralize(n, string, plural) + } + +} + +/** + * @Return the number of times the given character occurs in the string + */ +infix fun String.countChar(c: Char) : Int { + return CharMatcher.`is`(c).countIn(this) +} diff --git a/src/main/kotlin/com/beust/kobalt/misc/ToString.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/ToString.kt similarity index 100% rename from src/main/kotlin/com/beust/kobalt/misc/ToString.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/ToString.kt diff --git a/src/main/kotlin/com/beust/kobalt/misc/Topological.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Topological.kt similarity index 84% rename from src/main/kotlin/com/beust/kobalt/misc/Topological.kt rename to modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Topological.kt index 0003b4a4..38c4c503 100644 --- a/src/main/kotlin/com/beust/kobalt/misc/Topological.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Topological.kt @@ -10,22 +10,25 @@ import java.util.* */ class Topological { private val dependingOn = ArrayListMultimap.create() + private val nodes = hashSetOf() + + fun addNode(t: T) = nodes.add(t) fun addEdge(t: T, other: T) { + addNode(t) + addNode(other) dependingOn.put(t, other) } - fun addEdge(t: T, others: Array) { - dependingOn.putAll(t, others.toArrayList()) - } - /** * @return the Ts sorted topologically. */ - fun sort(all: ArrayList) : List { + fun sort() : List { + val all = ArrayList(nodes) val result = arrayListOf() var dependMap = HashMultimap.create() dependingOn.keySet().forEach { dependMap.putAll(it, dependingOn.get(it))} + nodes.forEach { dependMap.putAll(it, emptyList())} while (all.size > 0) { val freeNodes = all.filter { dependMap.get(it).isEmpty() diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Versions.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Versions.kt new file mode 100644 index 00000000..205c2ced --- /dev/null +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Versions.kt @@ -0,0 +1,353 @@ +package com.beust.kobalt.misc + +import com.beust.kobalt.maven.MavenId +import java.lang.* +import java.math.BigInteger +import java.util.* + +class Version(val version: String, val snapshotTimestamp: String? = null): Comparable { + + companion object { + private val comparator = VersionComparator() + fun of(string: String): Version { + return Version(string) + } + } + + val noSnapshotVersion: String + get() = version.replace("-SNAPSHOT", "") + + internal val items: List + + private var hash: Int = -1 + + init { + items = parse(version) + } + + private fun parse(version: String): List { + val items = arrayListOf() + + val tokenizer = Tokenizer(version) + while (tokenizer.next()) { + items.add(tokenizer.toItem()) + } + + trimPadding(items) + + return items + } + + private fun trimPadding(items: MutableList) { + var number: Boolean? = null + var end = items.size - 1 + for (i in end downTo 1) { + val item = items[i] + if (item.isNumber != number) { + end = i + number = item.isNumber + } + if (end == i && (i == items.size - 1 || items[i - 1].isNumber == item.isNumber) && item.compareTo(null) == 0) { + items.removeAt(i) + end-- + } + } + } + + override fun compareTo(other: Version) = comparator.compare(this, other) + + override fun equals(other: Any?) = (other is Version) && comparator.compare(this, other) == 0 + + override fun hashCode(): Int { + if ( hash == -1 ) hash = Arrays.hashCode(items.toTypedArray()) + return hash + } + + override fun toString() = version + + fun isSnapshot() = items.firstOrNull { it.isSnapshot } != null + + fun isRangedVersion() = MavenId.isRangedVersion(version) + + fun select(list: List): Version? { + if (!(version.first() in listOf('[', '(') && version.last() in listOf(']', ')'))) { + return this + } + var lowerExclusive = version.startsWith("(") + var upperExclusive = version.endsWith(")") + + val split = version.drop(1).dropLast(1).split(',') + + val lower = Version.of(split[0].substring(1)) + val upper = if(split.size > 1) { + Version.of(if (split[1].isNotBlank()) split[1] else Int.MAX_VALUE.toString()) + } else { + lower + } + var filtered = list.filter { comparator.compare(it, lower) >= 0 && comparator.compare(it, upper) <= 0 } + if (lowerExclusive && lower.equals(filtered.firstOrNull())) { + filtered = filtered.drop(1) + } + if (upperExclusive && upper.equals(filtered.lastOrNull())) { + filtered = filtered.dropLast(1) + } + + return filtered.lastOrNull(); + } + + +} + +class VersionComparator: Comparator { + override fun compare(left: Version, right: Version): Int { + val these = left.items + val those = right.items + + var number = true + + var index = 0 + while (true) { + if (index >= these.size && index >= those.size) { + return 0 + } else if (index >= these.size) { + return -comparePadding(those, index, null) + } else if (index >= those.size) { + return comparePadding(these, index, null) + } + + val thisItem = these[index] + val thatItem = those[index] + + if (thisItem.isNumber != thatItem.isNumber) { + if (number == thisItem.isNumber) { + return comparePadding(these, index, number) + } else { + return -comparePadding(those, index, number) + } + } else { + val rel = thisItem.compareTo(thatItem) + if (rel != 0) { + return rel + } + number = thisItem.isNumber + } + index++ + } + } + + private fun comparePadding(items: List, index: Int, number: Boolean?): Int { + var rel = 0 + for (i in index..items.size - 1) { + val item = items[i] + if (number != null && number !== item.isNumber) { + break + } + rel = item.compareTo(null) + if (rel != 0) { + break + } + } + return normalize(rel) + } + + +} +internal class Item(private val kind: Int, private val value: Any) { + + // i.e. kind != string/qualifier + val isNumber: Boolean + get() = (kind and KIND_QUALIFIER) == 0 + + val isSnapshot: Boolean + get() = (kind and KIND_QUALIFIER) != 0 && value == Tokenizer.QUALIFIER_SNAPSHOT + + operator fun compareTo(that: Item?): Int { + var rel: Int + if (that == null) { + // null in this context denotes the pad item (0 or "ga") + when (kind) { + KIND_MIN -> rel = -1 + KIND_MAX, KIND_BIGINT, KIND_STRING -> rel = 1 + KIND_INT, KIND_QUALIFIER -> rel = value as Int + else -> throw IllegalStateException("unknown version item kind " + kind) + } + } else { + rel = kind - that.kind + if (rel == 0) { + when (kind) { + KIND_MAX, KIND_MIN -> { + } + KIND_BIGINT -> rel = (value as BigInteger).compareTo(that.value as BigInteger) + KIND_INT, KIND_QUALIFIER -> rel = (value as Int).compareTo(that.value as Int) + KIND_STRING -> rel = (value as String).compareTo(that.value as String, ignoreCase = true) + else -> throw IllegalStateException("unknown version item kind " + kind) + } + } + } + return rel + } + + override fun equals(other: Any?): Boolean { + return (other is Item) && compareTo(other as Item?) == 0 + } + + override fun hashCode(): Int { + return value.hashCode() + kind * 31 + } + + override fun toString(): String { + return value.toString() + } + + companion object { + val KIND_MAX = 8 + val KIND_BIGINT = 5 + val KIND_INT = 4 + val KIND_STRING = 3 + val KIND_QUALIFIER = 2 + val KIND_MIN = 0 + val MAX = Item(KIND_MAX, "max") + val MIN = Item(KIND_MIN, "min") + } +} + +internal class Tokenizer(version: String) { + + private val version: String + + private var index: Int = 0 + + private var token: String = "" + + private var number: Boolean = false + + private var terminatedByNumber: Boolean = false + + init { + this.version = if (version.length > 0) version else "0" + } + + operator fun next(): Boolean { + val n = version.length + if (index >= n) { + return false + } + + var state = -2 + + var start = index + var end = n + terminatedByNumber = false + + while (index < n) { + val c = version[index] + + if (c == '.' || c == '-' || c == '_') { + end = index + index++ + break + } else { + val digit = Character.digit(c, 10) + if (digit >= 0) { + if (state == -1) { + end = index + terminatedByNumber = true + break + } + if (state == 0) { + // normalize numbers and strip leading zeros (prereq for Integer/BigInteger handling) + start++ + } + state = if ((state > 0 || digit > 0)) 1 else 0 + } else { + if (state >= 0) { + end = index + break + } + state = -1 + } + } + index++ + + } + + if (end - start > 0) { + token = version.substring(start, end) + number = state >= 0 + } else { + token = "0" + number = true + } + + return true + } + + override fun toString(): String { + return token.toString() + } + + fun toItem(): Item { + if (number) { + try { + if (token.length < 10) { + return Item(Item.KIND_INT, Integer.parseInt(token)) + } else { + return Item(Item.KIND_BIGINT, BigInteger(token)) + } + } catch (e: NumberFormatException) { + throw IllegalStateException(e) + } + + } else { + if (index >= version.length) { + if ("min".equals(token, ignoreCase = true)) { + return Item.MIN + } else if ("max".equals(token, ignoreCase = true)) { + return Item.MAX + } + } + if (terminatedByNumber && token.length == 1) { + when (token[0]) { + 'a', 'A' -> return Item(Item.KIND_QUALIFIER, QUALIFIER_ALPHA) + 'b', 'B' -> return Item(Item.KIND_QUALIFIER, QUALIFIER_BETA) + 'm', 'M' -> return Item(Item.KIND_QUALIFIER, QUALIFIER_MILESTONE) + } + } + val qualifier = QUALIFIERS[token] + if (qualifier != null) { + return Item(Item.KIND_QUALIFIER, qualifier) + } else { + return Item(Item.KIND_STRING, token.toLowerCase(Locale.ENGLISH)) + } + } + } + + companion object { + internal val QUALIFIER_ALPHA = -5 + internal val QUALIFIER_BETA = -4 + internal val QUALIFIER_MILESTONE = -3 + internal val QUALIFIER_SNAPSHOT = -1 + private val QUALIFIERS = TreeMap(String.CASE_INSENSITIVE_ORDER) + + init { + QUALIFIERS.put("alpha", QUALIFIER_ALPHA) + QUALIFIERS.put("beta", QUALIFIER_BETA) + QUALIFIERS.put("milestone", QUALIFIER_MILESTONE) + QUALIFIERS.put("snapshot", QUALIFIER_SNAPSHOT) + QUALIFIERS.put("cr", -2) + QUALIFIERS.put("rc", -2) + QUALIFIERS.put("ga", 0) + QUALIFIERS.put("final", 0) + QUALIFIERS.put("", 0) + QUALIFIERS.put("sp", 1) + } + } +} + +private fun normalize(value: Int): Int { + return when { + value == 0 -> 0 + value > 0 -> 1 + else -> -1 + } +} diff --git a/modules/kobalt/build.gradle b/modules/kobalt/build.gradle new file mode 100644 index 00000000..57e009c7 --- /dev/null +++ b/modules/kobalt/build.gradle @@ -0,0 +1,79 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.2.71' + id 'com.github.johnrengelman.shadow' version '5.0.0' +} + +dependencies { + implementation project(':wrapper') + implementation project(':kobalt-plugin-api') + implementation "biz.aQute.bnd:biz.aQute.bndlib:$bndlib" + implementation 'com.github.spullara.mustache.java:compiler:0.9.5' + implementation "com.google.code.findbugs:jsr305:$findbugs" + implementation "com.sparkjava:spark-core:$spark" + implementation "com.squareup.okhttp3:logging-interceptor:$okhttp" + implementation 'com.sun.activation:javax.activation:1.2.0' + implementation "com.sun.xml.bind:jaxb-core:$jaxb" + implementation "com.sun.xml.bind:jaxb-impl:$jaxb" + implementation "javax.inject:javax.inject:$inject" + implementation "javax.xml.bind:jaxb-api:$jaxb" + implementation "org.apache.maven.resolver:maven-resolver-spi:$mavenResolver" + implementation "org.codehaus.groovy:groovy:$groovy" + implementation "com.beust:jcommander:$jcommander" + implementation "com.google.code.gson:gson:$gson" + implementation "com.google.inject:guice:$guice" + implementation "com.google.inject.extensions:guice-assistedinject:$guice" + implementation "com.squareup.retrofit2:converter-gson:$retrofit" + implementation "com.squareup.retrofit2:retrofit:$retrofit" + implementation "org.apache.maven:maven-model:$maven" + implementation "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin" + testImplementation 'org.assertj:assertj-core:3.8.0' + testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin" + testImplementation "org.testng:testng:$testng" +} + +sourceSets { + main.kotlin.srcDirs += "${rootProject.projectDir}../../src/main/kotlin" + test.kotlin.srcDirs += "${rootProject.projectDir}../../src/test/kotlin" +} + +shadowJar { + classifier = null +} + +test { + useTestNG() +} + +publishing { + publications { + shadow(MavenPublication) { publication -> + project.shadow.component(publication) + artifact sourcesJar + artifact javadocJar + + pom { + name = project.name + description = 'A build system in Kotlin' + url = 'https://beust.com/kobalt' + licenses { + license { + name = 'Apache-2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0' + } + } + developers { + developer { + name = 'Cedric Beust' + email = 'cedric@beust.com' + } + } + scm { + connection = 'scm:https://github.com/cbeust/kobalt.git' + developerConnection = 'scm:git@github.com:cbeust/kobalt.git' + url = 'https://github.com/cbeust/kobalt' + } + } + } + } +} diff --git a/modules/kobalt/pom.xml b/modules/kobalt/pom.xml new file mode 100644 index 00000000..44dc799c --- /dev/null +++ b/modules/kobalt/pom.xml @@ -0,0 +1,231 @@ + + 4.0.0 + + com.beust + kobalt-pom + 1.1.0 + ../.. + + + kobalt + jar + 1.1.0 + + + + com.beust + kobalt-plugin-api + 1.1.0 + + + com.beust + wrapper + 1.1.0 + + + org.jetbrains.kotlin + kotlin-compiler-embeddable + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + com.github.spullara.mustache.java + compiler + 0.9.5 + compile + + + javax.inject + javax.inject + 1 + compile + + + com.google.inject + guice + 4.2.2 + + + com.google.inject.extensions + guice-assistedinject + 4.2.2 + + + com.beust + jcommander + 1.72 + + + org.apache.maven + maven-model + 3.5.2 + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + com.google.code.gson + gson + 2.8.2 + + + com.squareup.retrofit2 + retrofit + 2.3.0 + + + com.squareup.retrofit2 + converter-gson + 2.3.0 + + + biz.aQute.bnd + biz.aQute.bndlib + 3.5.0 + + + com.squareup.okhttp3 + logging-interceptor + ${okhttp3.version} + + + com.sparkjava + spark-core + 2.6.0 + + + org.codehaus.groovy + groovy + 2.4.12 + + + org.apache.maven.resolver + maven-resolver-spi + ${mavenresolver.version} + + + + javax.xml.bind + jaxb-api + 2.3.0 + + + com.sun.xml.bind + jaxb-impl + 2.3.0 + + + com.sun.xml.bind + jaxb-core + 2.3.0 + + + com.sun.activation + javax.activation + 1.2.0 + + + + org.assertj + assertj-core + 3.8.0 + test + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + + + org.testng + testng + ${testng.version} + test + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + + ${project.basedir}../../src/main/kotlin + + + + + test-compile + test-compile + + + ${project.basedir}../../src/test/kotlin + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + compile + + + java-test-compile + test-compile + testCompile + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + + com.beust.kobalt.MainKt + + + + + + package + + shade + + + + + + + \ No newline at end of file diff --git a/modules/wrapper/build.gradle b/modules/wrapper/build.gradle index e3b313bc..c0b5dafe 100644 --- a/modules/wrapper/build.gradle +++ b/modules/wrapper/build.gradle @@ -1,4 +1,38 @@ -apply plugin: 'java' -apply plugin: 'application' +jar { + manifest { + attributes 'Main-Class': 'com.beust.kobalt.wrapper.Main' + } +} -mainClassName = 'com.beust.kobalt.wrapper.Main' +publishing { + publications { + maven(MavenPublication) { + from(components.java) + artifact sourcesJar + artifact javadocJar + + pom { + name = project.name + description = 'Wrapper for Kobalt' + url = 'https://beust.com/kobalt' + licenses { + license { + name = 'Apache-2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0' + } + } + developers { + developer { + name = 'Cedric Beust' + email = 'cedric@beust.com' + } + } + scm { + connection = 'scm:https://github.com/cbeust/kobalt.git' + developerConnection = 'scm:git@github.com:cbeust/kobalt.git' + url = 'https://github.com/cbeust/kobalt' + } + } + } + } +} diff --git a/modules/wrapper/pom.xml b/modules/wrapper/pom.xml new file mode 100644 index 00000000..a9dc8796 --- /dev/null +++ b/modules/wrapper/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + com.beust + kobalt-pom + 1.1.0 + ../.. + + + wrapper + jar + 1.1.0 + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 1.7 + 1.7 + + + + + \ No newline at end of file diff --git a/modules/wrapper/src/main/java/com/beust/kobalt/wrapper/Config.java b/modules/wrapper/src/main/java/com/beust/kobalt/wrapper/Config.java new file mode 100644 index 00000000..f6eb4b09 --- /dev/null +++ b/modules/wrapper/src/main/java/com/beust/kobalt/wrapper/Config.java @@ -0,0 +1,72 @@ +package com.beust.kobalt.wrapper; + + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +class Config { + + static Proxy getProxy() { + String configFilePath = getConfigFilePath(); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + try { + DocumentBuilder builder = factory.newDocumentBuilder(); + ByteArrayInputStream input = new ByteArrayInputStream(getConfigFileContent(configFilePath)); + Document doc = builder.parse(input); + NodeList proxies =doc.getElementsByTagName("proxies"); + for (int temp = 0; temp < proxies.getLength(); temp++) { + Node node = proxies.item(temp); + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) node; + String type = element.getElementsByTagName("type").item(0).getTextContent(); + if (type.toLowerCase().equals("http")) { + String host = element.getElementsByTagName("host").item(0).getTextContent(); + String portString = element.getElementsByTagName("port").item(0).getTextContent(); + try { + int port = Integer.parseInt(portString); + Main.log(2, String.format("Using HTTP proxy: %s:%s", host, port)); + return new Proxy(java.net.Proxy.Type.HTTP, new InetSocketAddress(host, port)); + } catch (NumberFormatException e) { + Main.log(1, String.format("Invalid proxy port number: %s in config file: %s", portString, configFilePath)); + } + } + } + } + } catch (Exception e) { + Main.log(2, String.format("%s while parsing config file: %s", e.getMessage(), configFilePath)); + return null; + } + Main.log(2, String.format("No HTTP proxy found in config file: %s", configFilePath)); + return null; + } + + private static String getConfigFilePath() { + String userHome = System.getProperty("user.home"); + String fileSeparator = System.getProperty("file.separator"); + String configDir = ".config"; + String appName = "kobalt"; + String configFileName = "settings.xml"; + return userHome + + fileSeparator + configDir + + fileSeparator + appName + + fileSeparator + configFileName; + } + + private static byte[] getConfigFileContent(String configFilePath) throws IOException { + Path path = Paths.get(configFilePath); + return Files.readAllBytes(path); + } + +} diff --git a/modules/wrapper/src/main/java/com/beust/kobalt/wrapper/Main.java b/modules/wrapper/src/main/java/com/beust/kobalt/wrapper/Main.java index 0735060a..f31bbc3f 100644 --- a/modules/wrapper/src/main/java/com/beust/kobalt/wrapper/Main.java +++ b/modules/wrapper/src/main/java/com/beust/kobalt/wrapper/Main.java @@ -2,7 +2,9 @@ package com.beust.kobalt.wrapper; import java.io.*; import java.net.HttpURLConnection; +import java.net.Proxy; import java.net.URL; +import java.nio.charset.Charset; import java.nio.file.*; import java.util.*; import java.util.zip.ZipEntry; @@ -12,29 +14,50 @@ import java.util.zip.ZipFile; //@com.beust.apt.processor.Version("1.3") public class Main { public static void main(String[] argv) throws IOException, InterruptedException { - new Main().installAndLaunchMain(argv); + int exitCode = new Main().installAndLaunchMain(argv); + System.exit(exitCode); } + private static final boolean DEV = false; + private static final int DEV_VERSION_INT = 662; + private static final String DEV_VERSION = "0." + DEV_VERSION_INT; + private static final String DEV_ZIP = "/Users/beust/kotlin/kobalt/kobaltBuild/libs/kobalt-" + DEV_VERSION + ".zip"; + private static final String KOBALT_PROPERTIES = "kobalt.properties"; private static final String KOBALTW = "kobaltw"; + private static final String KOBALTW_BAT = "kobaltw.bat"; private static final String KOBALT_WRAPPER_PROPERTIES = "kobalt-wrapper.properties"; private static final String PROPERTY_VERSION = "kobalt.version"; private static final String PROPERTY_DOWNLOAD_URL = "kobalt.downloadUrl"; private static final String FILE_NAME = "kobalt"; private static final String DISTRIBUTIONS_DIR = System.getProperty("user.home") + "/.kobalt/wrapper/dist"; + private static final File VERSION_TXT = new File(".kobalt", "wrapperVersion.txt"); private final Properties wrapperProperties = new Properties(); + private static int logQuietLevel = 0; private static int logLevel = 1; private boolean noOverwrite = false; - private void installAndLaunchMain(String[] argv) throws IOException, InterruptedException { + private String getVersion() throws IOException { + Properties properties = maybeCreateProperties(); + return properties.getProperty(PROPERTY_VERSION); + } + + private int installAndLaunchMain(String[] argv) throws IOException, InterruptedException { + String version = getVersion(); + List kobaltArgv = new ArrayList<>(); boolean noLaunch = false; + boolean exit = false; for (int i = 0; i < argv.length; i++) { boolean passToKobalt = true; switch(argv[i]) { + case "--version": + System.out.println("Kobalt " + version + ", Wrapper " + getWrapperVersion()); + exit = true; + break; case "--noOverwrite": noOverwrite = true; passToKobalt = false; @@ -52,10 +75,15 @@ public class Main { kobaltArgv.add(argv[i]); } } - Path kobaltJarFile = installDistribution(); - if (! noLaunch) { - launchMain(kobaltJarFile, kobaltArgv.toArray(new String[kobaltArgv.size()])); + int result = 0; + if (! exit) { + initWrapperFile(version); + Path kobaltJarFile = installDistribution(); + if (!noLaunch) { + result = launchMain(kobaltJarFile, kobaltArgv); + } } + return result; } private void readProperties(Properties properties, InputStream ins) throws IOException { @@ -90,7 +118,7 @@ public class Main { } private static String downloadUrl(String version) { - return "http://beust.com/kobalt/kobalt-" + version + ".zip"; + return "https://beust.com/kobalt/kobalt-" + version + ".zip"; } private void initWrapperFile(String version) throws IOException { @@ -105,7 +133,7 @@ public class Main { } private String getWrapperVersion() { - return wrapperProperties.getProperty(PROPERTY_VERSION); + return wrapperProperties.getProperty(PROPERTY_VERSION, "N/A"); } private String getWrapperDownloadUrl(String version) { @@ -120,33 +148,44 @@ public class Main { return System.getProperty("os.name").contains("Windows"); } - private Path installDistribution() throws IOException { - Properties properties = maybeCreateProperties(); + private static final String[] FILES = new String[] { KOBALTW, KOBALTW_BAT, "kobalt/wrapper/" + FILE_NAME + "-wrapper.jar" }; - String version = properties.getProperty(PROPERTY_VERSION); - initWrapperFile(version); - String wrapperVersion = getWrapperVersion(); + private Path installDistribution() throws IOException { + String wrapperVersion; + String version; + Path localZipFile; + if (DEV) { + version = DEV_VERSION; + wrapperVersion = DEV_VERSION; + localZipFile = Paths.get(DEV_ZIP); + } else { + version = getVersion(); + wrapperVersion = getWrapperVersion(); + String fileName = FILE_NAME + "-" + wrapperVersion + ".zip"; + Files.createDirectories(Paths.get(DISTRIBUTIONS_DIR)); + localZipFile = Paths.get(DISTRIBUTIONS_DIR, fileName); + } log(2, "Wrapper version: " + wrapperVersion); - String fileName = FILE_NAME + "-" + wrapperVersion + ".zip"; - Files.createDirectories(Paths.get(DISTRIBUTIONS_DIR)); - Path localZipFile = Paths.get(DISTRIBUTIONS_DIR, fileName); - String zipOutputDir = DISTRIBUTIONS_DIR + "/" + wrapperVersion; - Path kobaltJarFile = Paths.get(zipOutputDir, + String toZipOutputDir = DISTRIBUTIONS_DIR; + Path kobaltJarFile = Paths.get(toZipOutputDir, + "kobalt-" + wrapperVersion, getWrapperDir().getPath() + "/" + FILE_NAME + "-" + wrapperVersion + ".jar"); + boolean downloadedZipFile = false; if (! Files.exists(localZipFile) || ! Files.exists(kobaltJarFile)) { download(localZipFile.toFile(), wrapperVersion); + downloadedZipFile = true; } // // Extract all the zip files // - if (! noOverwrite) { + if (! noOverwrite && downloadedZipFile) { int retries = 0; while (retries < 2) { try { - extractZipFile(localZipFile, zipOutputDir); + extractZipFile(localZipFile, toZipOutputDir); break; } catch (ZipException e) { retries++; @@ -159,59 +198,135 @@ public class Main { } // - // Copy the wrapper files in the current kobalt/wrapper directory - // + // If the user didn't specify --noOverwrite, compare the installed wrapper to the one + // we're trying to install and if they are the same, no need to overwrite if (! noOverwrite) { - log(2, "Copying the wrapper files"); - for (String file : FILES) { - Path to = Paths.get(new File(".").getAbsolutePath(), file); - if (Files.exists(to)) { - log(1, to + " already exists, not overwriting it"); - continue; - } - boolean extractFile = true; - if (file.equals(KOBALTW)) { - // - // For kobaltw: try to generate it with the correct env shebang. If this fails, - // we'll extract the one from the zip file - // - File envFile = new File("/bin/env"); - if (!envFile.exists()) { - envFile = new File("/usr/bin/env"); - } - if (envFile.exists()) { - log(2, "Located " + envFile.getAbsolutePath() + ", generating " + file); - String content = "#!" + envFile.getAbsolutePath() + " bash\n" - + "java -jar $(dirname $0)/kobalt/wrapper/kobalt-wrapper.jar $*\n"; - Files.write(to, content.getBytes()); - extractFile = false; - } - } - - if (extractFile) { - Path from = Paths.get(zipOutputDir, file); - try { - if (isWindows() && to.toFile().exists()) { - log(1, "Windows detected, not overwriting " + to); + if (VERSION_TXT.exists()) { + try (FileReader fw = new FileReader(VERSION_TXT)) { + try (BufferedReader bw = new BufferedReader(fw)) { + String installedVersion = bw.readLine(); + if (!wrapperVersion.equals(installedVersion)) { + log(2, " Trying to install a different version " + + wrapperVersion + " != " + installedVersion + + ", overwriting the installed wrapper"); + installWrapperFiles(version, wrapperVersion); } else { - Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); + log(2, " Trying to install the same version " + wrapperVersion + " == " + installedVersion + + ", not overwriting the installed wrapper"); } - } catch (IOException ex) { - log(1, "Couldn't copy " + from + " to " + to + ": " + ex.getMessage()); } } + } else { + log(2, " Couldn't find $VERSION_TEXT, overwriting the installed wrapper"); + installWrapperFiles(version, wrapperVersion); } - if (!new File(KOBALTW).setExecutable(true)) { - if (!isWindows()) { - log(1, "Couldn't make " + KOBALTW + " executable"); - } + // + // Install the launcher(s) if not already found. + // + File kobaltw = new File(KOBALTW); + File kobaltwBat = new File(KOBALTW_BAT); + + if (!kobaltw.exists()) { + generateKobaltW(kobaltw.toPath()); + } + + if (!kobaltwBat.exists()) { + generateKobaltWBat(kobaltwBat.toPath()); } } return kobaltJarFile; } + private void installWrapperFiles(String version, String wrapperVersion) throws IOException { + String fromZipOutputDir = DISTRIBUTIONS_DIR + File.separator + "kobalt-" + wrapperVersion; + + for (String file : FILES) { + Path to = Paths.get(file); + to.toFile().getAbsoluteFile().getParentFile().mkdirs(); + + if (file.endsWith(KOBALTW)) { + generateKobaltW(Paths.get(KOBALTW)); + } else if (file.endsWith(KOBALTW_BAT)) { + generateKobaltWBat(Paths.get(KOBALTW_BAT)); + } else { + Path from = Paths.get(fromZipOutputDir, file); + try { + if (isWindows() && to.toFile().exists()) { + log(2, " Windows detected, not overwriting " + to); + } else { + log(2, " Copying " + from + " to " + to); + Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException ex) { + log(1, " Couldn't copy " + from + " to " + to + ": " + ex.getMessage()); + } + } + } + + // + // Save wrapperVersion.txt + // + log(2, " Writing " + VERSION_TXT); + File parentDir = VERSION_TXT.getParentFile(); + parentDir.mkdirs(); + if (parentDir.exists()) { + try (FileWriter fw = new FileWriter(VERSION_TXT)) { + try (BufferedWriter bw = new BufferedWriter(fw)) { + bw.write(wrapperVersion); + } + } + } else { + log(1, "Warning: could not create the directory " + parentDir.getAbsolutePath() + + ", can't write " + VERSION_TXT); + } + } + + private void generateKobaltW(Path filePath) throws IOException { + // + // For kobaltw: try to generate it with the correct env shebang. + // + String envPath; + if (isWindows()) { + envPath = "/usr/bin/env"; + } else { + File envFile = new File("/bin/env"); + if (!envFile.canExecute()) { + envFile = new File("/usr/bin/env"); + } + envPath = envFile.getAbsolutePath(); + } + + String content = "#!" + envPath + " sh\n" + + "java -jar \"`dirname \"$0\"`/kobalt/wrapper/kobalt-wrapper.jar\" $*\n"; + + log(2, " Generating " + KOBALTW + " with shebang."); + + Files.write(filePath, content.getBytes()); + + if (!new File(KOBALTW).setExecutable(true)) { + if (!isWindows()) { + log(1, "Couldn't make " + KOBALTW + " executable"); + } + } + } + + private void generateKobaltWBat(Path filePath) throws IOException { + if (isWindows() && filePath.toFile().exists()) { + log(2, " Windows detected, not overwriting " + filePath); + } else { + String content = "@echo off\r\n" + + "set DIRNAME=%~dp0\r\n" + + "if \"%DIRNAME%\" == \"\" set DIRNAME=.\r\n" + + "java -jar \"%DIRNAME%/kobalt/wrapper/kobalt-wrapper.jar\" %*\r\n"; + + log(2, " Generating " + KOBALTW_BAT + " for Windows."); + + Files.write(filePath, content.getBytes()); + } + } + /** * Extract the zip file in ~/.kobalt/wrapper/dist/$version */ @@ -230,17 +345,19 @@ public class Main { try { Files.createDirectories(entryPath.getParent()); Files.copy(zipFile.getInputStream(entry), entryPath, StandardCopyOption.REPLACE_EXISTING); + if (!isWindows() && entry.getName().endsWith(KOBALTW)) { + if (!entryPath.toFile().setExecutable(true)) { + log(1, "Couldn't make distribution " + KOBALTW + " executable"); + } + } } catch (FileSystemException ex) { log(2, "Couldn't copy to " + entryPath); - throw new IOException(ex); } } } } } - private static final String[] FILES = new String[] { KOBALTW, "kobalt/wrapper/" + FILE_NAME + "-wrapper.jar" }; - private void download(File file, String version) throws IOException { for (int attempt = 0; attempt < 3; ++attempt) { try { @@ -266,13 +383,18 @@ public class Main { log(2, "Downloading " + fileUrl); boolean done = false; + Proxy proxy = Config.getProxy(); HttpURLConnection httpConn = null; try { int responseCode = 0; URL url = null; while (!done) { url = new URL(fileUrl); - httpConn = (HttpURLConnection) url.openConnection(); + if (proxy != null) { + httpConn = (HttpURLConnection) url.openConnection(proxy); + } else { + httpConn = (HttpURLConnection) url.openConnection(); + } responseCode = httpConn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == HttpURLConnection.HTTP_MOVED_PERM) { @@ -318,6 +440,8 @@ public class Main { } else { error("No file to download. Server replied HTTP code: " + responseCode); } + } catch(IOException ex) { + log(1, "Warning: couldn't download " + fileUrl); } finally { if (httpConn != null) { httpConn.disconnect(); @@ -325,20 +449,28 @@ public class Main { } } + private static String PROPERTY_NO_ANIMATIONS = "com.beust.kobalt.noAnimations"; + private void copyToStreamWithProgress(InputStream inputStream, OutputStream outputStream, long contentLength, String url) throws IOException { int bytesRead; long bytesSoFar = 0; byte[] buffer = new byte[100_000]; + boolean hasTerminal = System.console() != null && System.getProperty(PROPERTY_NO_ANIMATIONS) == null; + if (! hasTerminal) { + log2(1, "\rDownloading " + url); + } while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); bytesSoFar += bytesRead; - if (bytesRead > 0) { - if (contentLength > 0) { - float percent = bytesSoFar * 100 / contentLength; - log2(1, "\rDownloading " + url + " " + percent + "%"); - } else { - log2(1, "."); + if (hasTerminal) { + if (bytesRead > 0) { + if (contentLength > 0) { + float percent = bytesSoFar * 100 / contentLength; + log2(1, "\rDownloading " + url + " " + percent + "%"); + } else { + log2(1, "."); + } } } } @@ -361,7 +493,7 @@ public class Main { } private static void p(int level, String s, boolean newLine) { - if (level <= logLevel) { + if (level != logQuietLevel && level <= logLevel) { if (newLine) System.out.println(s); else System.out.print(s); } @@ -371,19 +503,28 @@ public class Main { System.out.println("[Wrapper error] *** " + s); } - private void launchMain(Path kobaltJarFile, String[] argv) throws IOException, InterruptedException { + private int launchMain(Path kobaltJarFile, List argv) throws IOException, InterruptedException { List args = new ArrayList<>(); args.add("java"); + args.add("-Dfile.encoding=" + Charset.defaultCharset().name()); + // jvm parameters must go before -jar + Iterator i = argv.iterator(); + while (i.hasNext()) { + String arg = i.next(); + if (arg.matches("-D(.+?)=(.*)")) { + args.add(arg); + i.remove(); + } + } args.add("-jar"); args.add(kobaltJarFile.toFile().getAbsolutePath()); - Collections.addAll(args, argv); + Collections.addAll(args, argv.toArray(new String[argv.size()])); ProcessBuilder pb = new ProcessBuilder(args); pb.inheritIO(); log(2, "Launching " + args); Process process = pb.start(); - process.waitFor(); - + return process.waitFor(); } -} \ No newline at end of file +} diff --git a/modules/wrapper/wrapper.iml b/modules/wrapper/wrapper.iml deleted file mode 100644 index d3218a2d..00000000 --- a/modules/wrapper/wrapper.iml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..3a87c7ac --- /dev/null +++ b/pom.xml @@ -0,0 +1,34 @@ + + 4.0.0 + + com.beust + kobalt-pom + pom + 1.1.0 + + + modules/kobalt-plugin-api + modules/wrapper + modules/kobalt + + + + + testng + https://dl.bintray.com/cbeust/maven + + + + + 1.2.71 + 1.13.0 + 3.9.1 + 1.1.0 + 1.1.0 + 5.1.0 + 6.12 + 1.7.3 + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 91ce3de2..c36e45fd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,5 @@ -include ':modules/wrapper' +rootProject.name = 'kobalt-pom' +include(':kobalt-plugin-api', ':wrapper', ':kobalt') +project(':kobalt-plugin-api').projectDir = file('modules/kobalt-plugin-api') +project(':wrapper').projectDir = file('modules/wrapper') +project(':kobalt').projectDir = file('modules/kobalt') diff --git a/src/main/kotlin/com/beust/kobalt/Args.kt b/src/main/kotlin/com/beust/kobalt/Args.kt deleted file mode 100644 index 26491a07..00000000 --- a/src/main/kotlin/com/beust/kobalt/Args.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.beust.kobalt - -import com.beust.jcommander.Parameter - -class Args { - @Parameter - var targets: List = arrayListOf() - - @Parameter(names = arrayOf("-bf", "--buildFile"), description = "The build file") - var buildFile: String? = null - - @Parameter(names = arrayOf("--checkVersions"), description = "Check if there are any newer versions of the " + - "dependencies") - var checkVersions = false - - @Parameter(names = arrayOf("--client")) - var client: Boolean = false - - @Parameter(names = arrayOf("--dev"), description = "Turn of dev mode, resulting in a more verbose log output") - var dev: Boolean = false - - @Parameter(names = arrayOf("--download"), description = "Force a download from the downloadUrl in the wrapper") - var download: Boolean = false - - @Parameter(names = arrayOf("--dryRun"), description = "Display all the tasks that will get run without " + - "actually running them") - var dryRun: Boolean = false - - @Parameter(names = arrayOf("--help", "--usage"), description = "Display the help") - var usage: Boolean = false - - @Parameter(names = arrayOf("-i", "--init"), description = "Create a build file based on the current project") - var init: Boolean = false - - @Parameter(names = arrayOf("--log"), description = "Define the log level (1-3)") - var log: Int = 1 - - companion object { - const val DEFAULT_SERVER_PORT = 1234 - } - - @Parameter(names = arrayOf("--port"), description = "Port, if --server was specified") - var port: Int = DEFAULT_SERVER_PORT - - @Parameter(names = arrayOf("--profiles"), description = "Comma-separate list of profiles to run") - var profiles: String? = null - - @Parameter(names = arrayOf("--resolve"), description = "Resolve the given dependency and display its tree") - var dependency: String? = null - - @Parameter(names = arrayOf("--server"), description = "Run in server mode") - var serverMode: Boolean = false - - @Parameter(names = arrayOf("--tasks"), description = "Display the tasks available for this build") - var tasks: Boolean = false - - @Parameter(names = arrayOf("--update"), description = "Update to the latest version of Kobalt") - var update: Boolean = false -} - diff --git a/src/main/kotlin/com/beust/kobalt/AsciiArt.kt b/src/main/kotlin/com/beust/kobalt/AsciiArt.kt deleted file mode 100644 index 00b75a80..00000000 --- a/src/main/kotlin/com/beust/kobalt/AsciiArt.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.beust.kobalt - -import com.beust.kobalt.misc.log -import java.util.* - - -class AsciiArt { - companion object { - private val BANNERS = arrayOf( - " __ __ __ __ __ \n" + - " / //_/ ____ / /_ ____ _ / / / /_\n" + - " / ,< / __ \\ / __ \\ / __ `/ / / / __/\n" + - " / /| | / /_/ / / /_/ // /_/ / / / / /_ \n" + - " /_/ |_| \\____/ /_.___/ \\__,_/ /_/ \\__/ ", - - " _ __ _ _ _ \n" + - " | |/ / ___ | |__ __ _ | | | |_ \n" + - " | ' / / _ \\ | '_ \\ / _` | | | | __|\n" + - " | . \\ | (_) | | |_) | | (_| | | | | |_ \n" + - " |_|\\_\\ \\___/ |_.__/ \\__,_| |_| \\__| " - ) - - val banner : String get() = BANNERS.get(Random().nextInt(BANNERS.size)) - - fun box(s: String) : List = box(listOf(s)) - - fun box(strings: List) : List { - val ul = "\u2554" - val ur = "\u2557" - val h = "\u2550" - val v = "\u2551" - val bl = "\u255a" - val br = "\u255d" - - fun r(n: Int, w: String) : String { - with(StringBuffer()) { - repeat(n, { append(w) }) - return toString() - } - } - - val maxString: String = strings.maxBy { it.length } ?: "" - val max = maxString.length - val result = arrayListOf(ul + r(max + 2, h) + ur) - result.addAll(strings.map { "$v ${center(it, max - 2)} $v" }) - result.add(bl + r(max + 2, h) + br) - return result - } - - private fun fill(n: Int) = StringBuffer().apply { repeat(n, { append(" ")})}.toString() - - val defaultLog : (s: String) -> Unit = { log(1, " $it") } - - fun logBox(strings: List, print: (String) -> Unit = defaultLog) { - box(strings).forEach { - print(it) - } - } - - fun logBox(s: String, print: (String) -> Unit = defaultLog) { - logBox(listOf(s), print) - } - - fun center(s: String, width: Int) : String { - val diff = width - s.length - val spaces = diff / 2 + 1 - return fill(spaces) + s + fill(spaces + if (diff % 2 == 1) 1 else 0) - } - } -} - diff --git a/src/main/kotlin/com/beust/kobalt/BuildScript.kt b/src/main/kotlin/com/beust/kobalt/BuildScript.kt deleted file mode 100644 index b40718de..00000000 --- a/src/main/kotlin/com/beust/kobalt/BuildScript.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.beust.kobalt - -import com.beust.kobalt.api.IClasspathDependency -import com.beust.kobalt.api.Kobalt -import com.beust.kobalt.api.annotation.Directive -import com.beust.kobalt.maven.DepFactory -import com.beust.kobalt.maven.dependency.FileDependency -import com.beust.kobalt.misc.KobaltExecutors -import java.io.File - -@Directive -fun homeDir(vararg dirs: String) : String = SystemProperties.homeDir + - File.separator + dirs.toArrayList().joinToString(File.separator) - -@Directive -fun file(file: String) : String = FileDependency.PREFIX_FILE + file - -@Directive -fun plugins(vararg dependency : IClasspathDependency) { - Plugins.dynamicPlugins.addAll(dependency) -} - -@Directive -fun plugins(vararg dependencies : String) { - val executor = Kobalt.INJECTOR.getInstance(KobaltExecutors::class.java).miscExecutor - val factory = Kobalt.INJECTOR.getInstance(DepFactory::class.java) - dependencies.forEach { - Plugins.dynamicPlugins.add(factory.create(it, executor)) - } -} - -@Directive -fun repos(vararg repos : String) { - repos.forEach { Kobalt.addRepo(it) } -} - -@Directive -fun glob(g: String) : IFileSpec.Glob = IFileSpec.Glob(g) diff --git a/src/main/kotlin/com/beust/kobalt/FileSpec.kt b/src/main/kotlin/com/beust/kobalt/FileSpec.kt deleted file mode 100644 index 28e4ea00..00000000 --- a/src/main/kotlin/com/beust/kobalt/FileSpec.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.beust.kobalt - -import com.beust.kobalt.misc.log -import java.io.File -import java.nio.file.* -import java.nio.file.attribute.BasicFileAttributes - -sealed class IFileSpec { - abstract fun toFiles(directory: String): List - - class FileSpec(val spec: String) : IFileSpec() { - override public fun toFiles(directory: String) = arrayListOf(File(spec)) - - override public fun toString() = spec - } - - class Glob(val spec: String) : IFileSpec() { - override public fun toFiles(directory: String): List { - val result = arrayListOf() - - val matcher = FileSystems.getDefault().getPathMatcher("glob:${spec}") - Files.walkFileTree(Paths.get(directory), object : SimpleFileVisitor() { - override public fun visitFile(path: Path, attrs: BasicFileAttributes): FileVisitResult { - val rel = Paths.get(directory).relativize(path) - if (matcher.matches(rel)) { - log(3, "Adding ${rel.toFile()}") - result.add(rel.toFile()) - } - return FileVisitResult.CONTINUE - } - }) - return result - } - - override public fun toString() = spec - } - -} diff --git a/src/main/kotlin/com/beust/kobalt/Main.kt b/src/main/kotlin/com/beust/kobalt/Main.kt index 8b5183aa..19b85404 100644 --- a/src/main/kotlin/com/beust/kobalt/Main.kt +++ b/src/main/kotlin/com/beust/kobalt/Main.kt @@ -1,250 +1,136 @@ package com.beust.kobalt import com.beust.jcommander.JCommander -import com.beust.kobalt.api.Kobalt -import com.beust.kobalt.api.Project -import com.beust.kobalt.internal.PluginInfo -import com.beust.kobalt.internal.TaskManager -import com.beust.kobalt.internal.build.BuildFile -import com.beust.kobalt.internal.build.BuildFileCompiler -import com.beust.kobalt.internal.remote.KobaltClient -import com.beust.kobalt.internal.remote.KobaltServer -import com.beust.kobalt.maven.DepFactory -import com.beust.kobalt.maven.Http -import com.beust.kobalt.maven.LocalRepo import com.beust.kobalt.api.IClasspathDependency +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.app.MainModule +import com.beust.kobalt.app.UpdateKobalt +import com.beust.kobalt.app.remote.KobaltClient +import com.beust.kobalt.internal.KobaltSettings +import com.beust.kobalt.internal.PluginInfo +import com.beust.kobalt.maven.DependencyManager +import com.beust.kobalt.maven.Http +import com.beust.kobalt.maven.dependency.FileDependency import com.beust.kobalt.misc.* -import com.google.inject.Guice import java.io.File -import java.nio.file.Paths -import java.util.* -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException +import java.net.URLClassLoader import javax.inject.Inject -public fun main(argv: Array) { - val result = mainNoExit(argv) +fun main(argv: Array) { + val result = Main.mainNoExit(argv) if (result != 0) { System.exit(result) } } -private fun parseArgs(argv: Array): Main.RunInfo { - val args = Args() - val result = JCommander(args) - result.parse(*argv) - KobaltLogger.LOG_LEVEL = args.log - return Main.RunInfo(result, args) -} - - -public fun mainNoExit(argv: Array): Int { - val (jc, args) = parseArgs(argv) - Kobalt.INJECTOR = Guice.createInjector(MainModule(args)) - return Kobalt.INJECTOR.getInstance(Main::class.java).run(jc, args, argv) -} - -private class Main @Inject constructor( - val buildFileCompilerFactory: BuildFileCompiler.IFactory, +class Main @Inject constructor( val plugins: Plugins, - val taskManager: TaskManager, val http: Http, val files: KFiles, val executors: KobaltExecutors, - val localRepo: LocalRepo, - val depFactory: DepFactory, - val checkVersions: CheckVersions, - val github: GithubApi, + val dependencyManager: DependencyManager, + val github: GithubApi2, val updateKobalt: UpdateKobalt, val client: KobaltClient, - val server: KobaltServer, val pluginInfo: PluginInfo, - val projectGenerator: ProjectGenerator, - val resolveDependency: ResolveDependency) { + val options: Options) { + + companion object { + fun mainNoExit(argv: Array): Int { + val (jc, args) = parseArgs(argv) + if (args.usage) { + jc.usage() + return 0 + } + if (args.version) { + println("Kobalt ${Kobalt.version}") + return 0 + } + Kobalt.init(MainModule(args, KobaltSettings.readSettingsXml())) + val result = launchMain(Kobalt.INJECTOR.getInstance(Main::class.java), jc, args, argv) + return result + } + + private fun parseArgs(argv: Array): Main.RunInfo { + val args = Args() + val result = JCommander(args) + result.parse(*argv) + KobaltLogger.setLogLevel(args) + return Main.RunInfo(result, args) + } + + /** + * Entry point for tests, which can instantiate their main object with their own module and injector. + */ + fun launchMain(main: Main, jc: JCommander, args: Args, argv: Array) : Int { + return main.run { + val runResult = run(jc, args, argv) + pluginInfo.cleanUp() + executors.shutdown() + runResult + } + } + } data class RunInfo(val jc: JCommander, val args: Args) - private fun addReposFromContributors(project: Project?) = - pluginInfo.repoContributors.forEach { - it.reposFor(project).forEach { - Kobalt.addRepo(it.toString()) - } - } + private fun installCommandLinePlugins(args: Args): ClassLoader { + var pluginClassLoader = javaClass.classLoader + val dependencies = arrayListOf() + args.pluginIds?.let { + // We want this call to go to the network if no version was specified, so set localFirst to false + dependencies.addAll(it.split(',').map { dependencyManager.create(it) }) + } + args.pluginJarFiles?.let { + dependencies.addAll(it.split(',').map { FileDependency(it) }) + } + if (dependencies.size > 0) { + val urls = dependencies.map { it.jarFile.get().toURI().toURL() } + pluginClassLoader = URLClassLoader(urls.toTypedArray()) + plugins.installPlugins(dependencies, pluginClassLoader) + } - public fun run(jc: JCommander, args: Args, argv: Array): Int { -// github.uploadRelease("kobalt", "0.101", File("/Users/beust/t/a.zip")) + return pluginClassLoader + } + + fun run(jc: JCommander, args: Args, argv: Array): Int { // - // Add all the repos from repo contributors (at least those that return values without a Project) + // Install plug-ins requested from the command line // - addReposFromContributors(null) - - // - // Add all the plugins read in kobalt-plugin.xml to the Plugins singleton, so that code - // in the build file that calls Plugins.findPlugin() can find them (code in the - // build file do not have access to the KobaltContext). - // - pluginInfo.plugins.forEach { Plugins.addPluginInstance(it) } + installCommandLinePlugins(args) if (args.client) { client.run() return 0 } - var result = 0 + var result = 1 + val latestVersionFuture = github.latestKobaltVersion - val seconds = benchmark("build", { - try { - result = runWithArgs(jc, args, argv) - } catch(ex: KobaltException) { - error("", ex.cause ?: ex) - result = 1 - } finally { - executors.shutdown() - } - }) - log(1, if (result != 0) "BUILD FAILED: $result" else "BUILD SUCCESSFUL ($seconds seconds)") - - // Check for new version try { - val latestVersionString = latestVersionFuture.get(1, TimeUnit.SECONDS) - val latestVersion = Versions.toLongVersion(latestVersionString) - val current = Versions.toLongVersion(Kobalt.version) - if (latestVersion > current) { - listOf("", "New Kobalt version available: $latestVersionString", - "To update, run ./kobaltw --update", "").forEach { - log(1, "**** $it") - } - } - } catch(ex: TimeoutException) { - log(2, "Didn't get the new version in time, skipping it") + result = runWithArgs(jc, args, argv) + } catch(ex: Throwable) { + error("", ex.cause ?: ex) } - return result - } - - // public fun runTest() { - // val file = File("src\\main\\resources\\META-INF\\plugin.ml") - // } - - private fun runWithArgs(jc: JCommander, args: Args, argv: Array): Int { -// val file = File("/Users/beust/.kobalt/repository/com/google/guava/guava/19.0-rc2/guava-19.0-rc2.pom") -// val md5 = Md5.toMd5(file) -// val md52 = MessageDigest.getInstance("MD5").digest(file.readBytes()).toHexString() - var result = 0 - val p = if (args.buildFile != null) File(args.buildFile) else findBuildFile() - args.buildFile = p.absolutePath - val buildFile = BuildFile(Paths.get(p.absolutePath), p.name) if (!args.update) { - println(AsciiArt.banner + Kobalt.version + "\n") - } - - if (args.init) { - // - // --init: create a new build project and install the wrapper - // Make sure the wrapper won't call us back with --noLaunch - // - com.beust.kobalt.wrapper.Main.main(arrayOf("--noLaunch") + argv) - projectGenerator.run(args) - } else if (args.usage) { - jc.usage() - } else if (args.serverMode) { - server.run() - } else { - if (!buildFile.exists()) { - error(buildFile.path.toFile().path + " does not exist") - } else { - val allProjects = buildFileCompilerFactory.create(listOf(buildFile), pluginInfo) - .compileBuildFiles(args) - - // - // Now that we have projects, add all the repos from repo contributors that need a Project - // - allProjects.forEach { addReposFromContributors(it) } - - // - // Run all their dependencies through the IDependencyInterceptors - // - runClasspathInterceptors(allProjects, pluginInfo) - - log(2, "Final list of repos:\n " + Kobalt.repos.joinToString("\n ")) - - if (args.dependency != null) { - // --resolve - resolveDependency.run(args.dependency as String) - } else if (args.tasks) { - // - // List of tasks - // - val sb = StringBuffer("List of tasks\n") - Plugins.plugins.forEach { plugin -> - if (plugin.tasks.size > 0) { - sb.append("\n ===== ${plugin.name} =====\n") - plugin.tasks.distinctBy { - it.name - }.forEach { task -> - sb.append(" ${task.name}\t\t${task.doc}\n") - } - } - } - println(sb.toString()) - } else if (args.checkVersions) { - checkVersions.run(allProjects) - } else if (args.download) { - updateKobalt.downloadKobalt() - } else if (args.update) { - updateKobalt.updateKobalt() - } else { - // - // Launch the build - // - val thisResult = taskManager.runTargets(args.targets, allProjects) - if (result == 0) { - result = thisResult - } - } - } + updateKobalt.checkForNewVersion(latestVersionFuture) } return result } - private fun runClasspathInterceptors(allProjects: List, pluginInfo: PluginInfo) { - allProjects.forEach { - runClasspathInterceptors(it.compileDependencies) - runClasspathInterceptors(it.compileProvidedDependencies) - runClasspathInterceptors(it.compileRuntimeDependencies) - runClasspathInterceptors(it.testProvidedDependencies) - runClasspathInterceptors(it.testDependencies) + private fun runWithArgs(jc: JCommander, args: Args, argv: Array): Int { + val p = if (args.buildFile != null) File(args.buildFile) else File(".") + args.buildFile = p.absolutePath + + if (!args.update) { + kobaltLog(1, AsciiArt.banner + Kobalt.version + "\n") } + + return options.run(jc, args, argv) } - private fun runClasspathInterceptors(dependencies: ArrayList) = with(dependencies) { - val deps = interceptDependencies(pluginInfo, this) - clear() - addAll(deps) - } - private fun interceptDependencies(pluginInfo: PluginInfo, dependencies: ArrayList) - : ArrayList { - val result = arrayListOf() - pluginInfo.classpathInterceptors.forEach { - result.addAll(it.intercept(dependencies)) - } - return result - } - - private fun findBuildFile(): File { - val files = arrayListOf("Build.kt", "build.kobalt", KFiles.src("build.kobalt"), - KFiles.src("Build.kt")) - try { - return files.map { - File(SystemProperties.currentDir, it) - }.first { - it.exists() - } - } catch(ex: NoSuchElementException) { - return File("Build.kt") - } - } } diff --git a/src/main/kotlin/com/beust/kobalt/Options.kt b/src/main/kotlin/com/beust/kobalt/Options.kt new file mode 100644 index 00000000..ef4cbfd4 --- /dev/null +++ b/src/main/kotlin/com/beust/kobalt/Options.kt @@ -0,0 +1,210 @@ +package com.beust.kobalt + +import com.beust.jcommander.JCommander +import com.beust.kobalt.api.ITask +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.api.KobaltContext +import com.beust.kobalt.api.Project +import com.beust.kobalt.app.ProjectFinder +import com.beust.kobalt.app.ProjectGenerator +import com.beust.kobalt.app.Templates +import com.beust.kobalt.app.UpdateKobalt +import com.beust.kobalt.app.remote.KobaltServer +import com.beust.kobalt.internal.PluginInfo +import com.beust.kobalt.internal.TaskManager +import com.beust.kobalt.internal.build.BuildSources +import com.beust.kobalt.internal.build.SingleFileBuildSources +import com.beust.kobalt.misc.CheckVersions +import com.beust.kobalt.misc.kobaltLog +import com.beust.kobalt.wrapper.Main +import com.google.common.collect.HashMultimap +import com.google.inject.Inject +import java.io.File + +/** + * Some options require a build file, others shouldn't have one and some don't care. This + * class captures these requirements. + */ +open class Option(open val enabled: () -> Boolean, open val action: () -> Unit, + open val requireBuildFile: Boolean = true) +class OptionalBuildOption(override val enabled: () -> Boolean, override val action: () -> Unit) + : Option(enabled, action, false) + +class Options @Inject constructor( + val plugins: Plugins, + val checkVersions: CheckVersions, + val projectGenerator: ProjectGenerator, + val pluginInfo: PluginInfo, + val serverFactory: KobaltServer.IFactory, + val updateKobalt: UpdateKobalt, + val projectFinder: ProjectFinder, + val taskManager: TaskManager, + val resolveDependency: ResolveDependency + ) { + + fun run(jc: JCommander, args: Args, argv: Array): Int { + val p = if (args.buildFile != null) File(args.buildFile) else File(".") +// val buildFile = BuildFile(Paths.get(p.absolutePath), p.name) + val buildSources = if (p.isDirectory) BuildSources(p.absoluteFile) else SingleFileBuildSources(p) + val pluginClassLoader = javaClass.classLoader + + // + // Attempt to parse the build file in order to correctly set up repos, plug-ins, etc... + // If the build file can't be parsed, don't give up just yet since some options don't need + // a correct build file to work. + // + var buildError: Throwable? = null + val allProjects = + try { + projectFinder.initForBuildFile(buildSources, args).projects + } catch(ex: Exception) { + buildError = ex + listOf() + } + + fun runIfSuccessfulBuild(buildError: Throwable?, action: () -> Unit) { + buildError?.let { throw it } + action() + } + + // Modify `args` with options found in buildScript { kobaltOptions(...) }, if any + addOptionsFromBuild(args, Kobalt.optionsFromBuild) + + val options = listOf