diff --git a/.gitignore b/.gitignore index 7e740ce2..a680191b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,15 @@ .gradle annotations .idea/* -!.idea/modules.xml -!.idea/libraries -!.idea/misc.xml -buildScript +*.iml +nonBuildScript kobaltBuild local.properties classes libs .kobalt/ -./build/ out .DS_Store +lib/kotlin-* +build +.history diff --git a/.idea/libraries/KotlinJavaRuntime.xml b/.idea/libraries/KotlinJavaRuntime.xml deleted file mode 100644 index 207fc74d..00000000 --- a/.idea/libraries/KotlinJavaRuntime.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/kobalt__Compile_.xml b/.idea/libraries/kobalt__Compile_.xml deleted file mode 100644 index 874c6a9b..00000000 --- a/.idea/libraries/kobalt__Compile_.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/kobalt__Test_.xml b/.idea/libraries/kobalt__Test_.xml deleted file mode 100644 index a93b84bc..00000000 --- a/.idea/libraries/kobalt__Test_.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/kobalt_jar.xml b/.idea/libraries/kobalt_jar.xml deleted file mode 100644 index 097d174d..00000000 --- a/.idea/libraries/kobalt_jar.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/kobalt_plugin_api__Compile_.xml b/.idea/libraries/kobalt_plugin_api__Compile_.xml deleted file mode 100644 index f28f8426..00000000 --- a/.idea/libraries/kobalt_plugin_api__Compile_.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/kobalt_plugin_api__Test_.xml b/.idea/libraries/kobalt_plugin_api__Test_.xml deleted file mode 100644 index 18b8b5ed..00000000 --- a/.idea/libraries/kobalt_plugin_api__Test_.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/kobalt_wrapper__Compile_.xml b/.idea/libraries/kobalt_wrapper__Compile_.xml deleted file mode 100644 index 3c4baa9e..00000000 --- a/.idea/libraries/kobalt_wrapper__Compile_.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/kobalt_wrapper__Test_.xml b/.idea/libraries/kobalt_wrapper__Test_.xml deleted file mode 100644 index 79d6011e..00000000 --- a/.idea/libraries/kobalt_wrapper__Test_.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 4079311b..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index a41ffd7c..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 43e61725..00000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -cache: - directories: - - $HOME/.m2 - - $HOME/.kobalt - -language: java - -jdk: - - oraclejdk8 - -install: true - -script: ./kobaltw assemble test --log 2 diff --git a/README.md b/README.md index e8db3ef6..d5d7cbe0 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ -# Kobalt [![Build Status](https://travis-ci.org/cbeust/kobalt.svg?branch=master)](https://travis-ci.org/cbeust/kobalt) +# 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/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 new file mode 100644 index 00000000..3f0053cc --- /dev/null +++ b/build.gradle @@ -0,0 +1,58 @@ +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() + maven { + url = 'https://dl.bintray.com/cbeust/maven' + } + + maven { + url = 'https://repo.maven.apache.org/maven2' + } + } + + 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 index c3b9470e..333738df 100755 --- a/dist/kobaltw +++ b/dist/kobaltw @@ -1,2 +1,11 @@ -#!/usr/bin/env bash -java -jar $(dirname $0)/../kobalt/wrapper/kobalt-wrapper.jar $* +#!/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/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..5c2d1cf0 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..838e6bc8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +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 diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..b0d6d0ab --- /dev/null +++ b/gradlew @@ -0,0 +1,188 @@ +#!/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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +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 + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +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 + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +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` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# 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 new file mode 100644 index 00000000..9991c503 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@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 +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +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 + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kobalt.iml b/kobalt.iml deleted file mode 100644 index aa615c4b..00000000 --- a/kobalt.iml +++ /dev/null @@ -1,387 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/kobalt/Build.kt.iml b/kobalt/Build.kt.iml deleted file mode 100644 index d3683da5..00000000 --- a/kobalt/Build.kt.iml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/kobalt/src/Build.kt b/kobalt/src/Build.kt index 4f4310de..0d09844a 100644 --- a/kobalt/src/Build.kt +++ b/kobalt/src/Build.kt @@ -1,35 +1,57 @@ -import com.beust.kobalt.TaskResult -import com.beust.kobalt.api.License +import com.beust.kobalt.* import com.beust.kobalt.api.Project -import com.beust.kobalt.api.Scm import com.beust.kobalt.api.annotation.Task -import com.beust.kobalt.homeDir import com.beust.kobalt.plugin.application.application 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.project -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 bs = buildScript { + repos("https://dl.bintray.com/cbeust/maven") +} object Versions { - val okhttp = "3.2.0" - val okio = "1.6.0" - val retrofit = "2.0.0" - val gson = "2.6.2" - val aether = "1.1.0" - val sonatypeAether = "1.13.1" - val maven = "3.3.9" - val jersey = "2.22.2" - val jetty = "8.1.19.v20160209" // "9.3.9.M1" + 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" @@ -42,6 +64,7 @@ val wrapper = project { } assemble { + jar { } jar { name = projectName + ".jar" manifest { @@ -53,6 +76,13 @@ val wrapper = project { application { mainClass = "com.beust.kobalt.wrapper.Main" } + + bintray { + publish = true + sign = true + } + + pom = createPom(name, "Wrapper for Kobalt") } val kobaltPluginApi = project { @@ -62,37 +92,44 @@ val kobaltPluginApi = project { version = readVersion() directory = "modules/kobalt-plugin-api" description = "A build system in Kotlin" - url = "http://beust.com/kobalt" - licenses = listOf(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" + + pom = createPom(name, "A build system in Kotlin") dependencies { - compile("org.jetbrains.kotlinx:kotlinx.dom:0.0.10", - - "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", + "com.google.guava:guava:27.0.1-jre", "org.apache.maven:maven-model:${Versions.maven}", - "io.reactivex:rxjava:1.0.16", - "com.google.code.gson:gson:${Versions.gson}", + "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:1.48", + "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", - "org.slf4j:slf4j-nop:1.6.0", - "org.eclipse.aether:aether-spi:${Versions.aether}", - "org.eclipse.aether:aether-impl:${Versions.aether}", - "org.eclipse.aether:aether-connector-basic:${Versions.aether}", - "org.eclipse.aether:aether-transport-file:${Versions.aether}", - "org.eclipse.aether:aether-transport-http:${Versions.aether}", - "org.sonatype.aether:aether-api:${Versions.sonatypeAether}", - "org.sonatype.aether:aether-connector-wagon:1.13.1", - "org.apache.maven:maven-aether-provider:${Versions.maven}" + // Java 9 + "javax.xml.bind:jaxb-api:2.3.0" ) + exclude(*aether("impl", "spi", "util", "api")) } @@ -105,12 +142,8 @@ val kobaltPluginApi = project { } } -// install { -// libDir = "lib-test" -// } - kotlinCompiler { - args("-nowarn") + args("nowarn") } bintray { @@ -126,26 +159,35 @@ val kobaltApp = project(kobaltPluginApi, wrapper) { dependencies { // Used by the plugins - compile("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.0.0") + compile("org.jetbrains.kotlin:kotlin-compiler-embeddable:${Versions.kotlin}") // Used by the main app - compile("com.github.spullara.mustache.java:compiler:0.9.1", + 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:4.0", - "com.google.inject.extensions:guice-assistedinject:4.0", - "com.beust:jcommander:1.48", + "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.1", + "com.google.code.findbugs:jsr305:3.0.2", "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}", - "org.codehaus.plexus:plexus-utils:3.0.22", - "biz.aQute.bnd:bndlib:2.4.0", +// "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.2.0", + "com.squareup.okhttp3:logging-interceptor:3.9.0", - "com.sparkjava:spark-core:2.3" + "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}", @@ -159,8 +201,11 @@ val kobaltApp = project(kobaltPluginApi, wrapper) { } dependenciesTest { - compile("org.testng:testng:6.9.10", - "org.assertj:assertj-core:3.4.1") + compile("org.jetbrains.kotlin:kotlin-test:${Versions.kotlin}", + "org.testng:testng:${Versions.testng}", + "org.assertj:assertj-core:3.8.0", + *mavenResolver("util") + ) } assemble { @@ -172,17 +217,27 @@ val kobaltApp = project(kobaltPluginApi, wrapper) { } zip { val dir = "kobalt-$version" - include(from("dist"), to("$dir/bin"), "kobaltw") - include(from("dist"), to("$dir/bin"), "kobaltw.bat") - include(from("$buildDirectory/libs"), to("$dir/kobalt/wrapper"), - "$projectName-$version.jar") - include(from("modules/wrapper/$buildDirectory/libs"), to("$dir/kobalt/wrapper"), - "$projectName-wrapper.jar") + 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") + args("nowarn") } bintray { @@ -196,6 +251,32 @@ val kobaltApp = project(kobaltPluginApi, wrapper) { 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 { @@ -208,16 +289,38 @@ fun readVersion() : String { } } -@Task(name = "copyVersionForWrapper", reverseDependsOn = arrayOf("assemble"), 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 6ca6ca57..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 4c0c3afd..0ca8045f 100644 --- a/kobalt/wrapper/kobalt-wrapper.properties +++ b/kobalt/wrapper/kobalt-wrapper.properties @@ -1 +1 @@ -kobalt.version=0.774 \ 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 52e2d636..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/lib/kotlin-reflect.jar b/lib/kotlin-reflect.jar deleted file mode 100644 index 5cff48a8..00000000 Binary files a/lib/kotlin-reflect.jar and /dev/null differ diff --git a/lib/kotlin-runtime-sources.jar b/lib/kotlin-runtime-sources.jar deleted file mode 100644 index 7be4c42c..00000000 Binary files a/lib/kotlin-runtime-sources.jar and /dev/null differ diff --git a/lib/kotlin-runtime.jar b/lib/kotlin-runtime.jar deleted file mode 100644 index efb45632..00000000 Binary files a/lib/kotlin-runtime.jar and /dev/null differ 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/kobalt-plugin-api.iml b/modules/kobalt-plugin-api/kobalt-plugin-api.iml deleted file mode 100644 index 81d29a4b..00000000 --- a/modules/kobalt-plugin-api/kobalt-plugin-api.iml +++ /dev/null @@ -1,456 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file 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 index 7501cec5..372f1ba1 100644 --- 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 @@ -7,7 +7,7 @@ class Args { var targets: List = arrayListOf() @Parameter(names = arrayOf("-bf", "--buildFile"), description = "The build file") - var buildFile: String? = null + var buildFile: String? = "kobalt/src/Build.kt" @Parameter(names = arrayOf("--checkVersions"), description = "Check if there are any newer versions of the " + "dependencies") @@ -22,6 +22,10 @@ class Args { @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 @@ -42,12 +46,24 @@ class Args { @Parameter(names = arrayOf("--listTemplates"), description = "List the available templates") var listTemplates: Boolean = false - @Parameter(names = arrayOf("--log"), description = "Define the log level (1-3)") - var log: Int = 1 + @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 @@ -60,13 +76,26 @@ class Args { @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 comma-separated dependencies and display their dependency tree") - var dependencies: String? = null + 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 @@ -75,5 +104,8 @@ class Args { @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 index de16f9d7..e138fabc 100644 --- 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 @@ -1,23 +1,32 @@ package com.beust.kobalt -import com.beust.kobalt.misc.log 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" + - " | |/ / ___ | |__ __ _ | | | |_ \n" + - " | ' / / _ \\ | '_ \\ / _` | | | | __|\n" + - " | . \\ | (_) | | |_) | | (_| | | | | |_ \n" + - " |_|\\_\\ \\___/ |_.__/ \\__,_| |_| \\__| " + " | |/ / ___ | |__ __ _ | | | |_ \n" + + " | ' / / _ \\ | '_ \\ / _` | | | | __|\n" + + " | . \\ | (_) | | |_) | | (_| | | | | |_ \n" + + " |_|\\_\\ \\___/ |_.__/ \\__,_| |_| \\__| " ) val banner : String get() = BANNERS[Random().nextInt(BANNERS.size)] @@ -26,47 +35,59 @@ class AsciiArt { 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() - 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() - } + // 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(ul + r(max + 2, h) + ur) + val result = arrayListOf(upperBox(max)) result.addAll(strings.map { "$v ${center(it, max - 2)} $v" }) - result.add(bl + r(max + 2, h) + br) + result.add(lowerBox(max, bl, 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(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, print: (String) -> Unit = defaultLog) { - logBox(listOf(s), print) - } + 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 @@ -84,14 +105,60 @@ class AsciiArt { const val CYAN = "\u001B[36m" const val WHITE = "\u001B[37m" - private fun wrap(s: String, color: String) = color + s + RESET - private fun blue(s: String) = wrap(s, BLUE) - private fun red(s: String) = wrap(s, RED) - private fun yellow(s: String) = wrap(s, YELLOW) + 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: String) = s - fun errorColor(s: String) = red(s) - fun warnColor(s: String) = red(s) + 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/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 index e1f4405f..4c35b9ed 100644 --- 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 @@ -3,42 +3,89 @@ 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 localMavenRepo() = homeDir(".m2" + File.separator + "repository/") - @Directive fun file(file: String) : String = FileDependency.PREFIX_FILE + file -@Directive fun plugins(vararg dependency : IClasspathDependency) { dependency.forEach { Plugins.addDynamicPlugin(it) } } -@Directive 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(var host: String = "", var port: Int = 0, val type: String = "") { +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 username: String? = null, var password: String? = null) { +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()) } @@ -52,18 +99,47 @@ data class HostConfig(var url: String = "", var username: String? = null, var pa } } -@Directive 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().apply { init() } +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 index 7a95109f..8eb73c84 100644 --- 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 @@ -3,15 +3,19 @@ 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/", - "http://repo1.maven.org/maven2/", - "https://jcenter.bintray.com/", - "http://repository.jetbrains.com/all/" + 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/" 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 index a655325a..93d9434c 100644 --- 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 @@ -4,9 +4,11 @@ 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 -public fun project(vararg projects: Project, init: Project.() -> Unit): Project { +fun project(vararg projects: Project, init: Project.() -> Unit): Project { return Project("").apply { init() (Kobalt.findPlugin(JvmCompilerPlugin.PLUGIN_NAME) as JvmCompilerPlugin) @@ -14,3 +16,24 @@ public fun project(vararg projects: Project, init: Project.() -> Unit): Project } } +@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/FileSpec.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/FileSpec.kt index 043e2bac..1eb409f4 100644 --- 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 @@ -1,6 +1,6 @@ package com.beust.kobalt -import com.beust.kobalt.misc.log +import com.beust.kobalt.misc.kobaltLog import java.io.File import java.nio.file.* import java.nio.file.attribute.BasicFileAttributes @@ -28,15 +28,15 @@ sealed class IFileSpec { private fun isIncluded(includeMatchers: Glob, excludes: List, rel: Path) : Boolean { excludes.forEach { if (it.matches(rel)) { - log(3, "Excluding ${rel.toFile()}") + kobaltLog(3, " Excluding ${rel.toFile()}") return false } } if (includeMatchers.matches(rel)) { - log(3, "Including ${rel.toFile().path}") + kobaltLog(3, " Including ${rel.toFile().path}") return true } - log(2, "Excluding ${rel.toFile()} (not matching any include pattern") + kobaltLog(2, " Excluding ${rel.toFile()} (not matching any include pattern") return false } @@ -56,7 +56,7 @@ sealed class IFileSpec { val path = p.normalize() val rel = orgRootDir.relativize(path) if (isIncluded(includes, excludes, path)) { - log(3, " including file " + rel.toFile() + " from rootDir $rootDir") + kobaltLog(3, " including file " + rel.toFile() + " from rootDir $rootDir") result.add(rel.toFile()) } return FileVisitResult.CONTINUE 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/JarGenerator.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/JarGenerator.kt index 432039ea..19bb52c6 100644 --- 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 @@ -2,21 +2,23 @@ package com.beust.kobalt import com.beust.kobalt.api.KobaltContext import com.beust.kobalt.api.Project -import com.beust.kobalt.api.ProjectDescription import com.beust.kobalt.archive.Archives -import com.beust.kobalt.archive.Jar -import com.beust.kobalt.internal.JvmCompilerPlugin +import com.beust.kobalt.archive.MetaArchive +import com.beust.kobalt.archive.Zip import com.beust.kobalt.maven.DependencyManager -import com.beust.kobalt.misc.* +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.OutputStream +import java.io.FileInputStream import java.nio.file.Paths -import java.util.jar.JarOutputStream +import java.util.jar.Manifest -class JarGenerator @Inject constructor(val dependencyManager: DependencyManager) { +class JarGenerator @Inject constructor(val dependencyManager: DependencyManager) : ArchiveGenerator { companion object { - fun findIncludedFiles(directory: String, files: List, excludes: List) + fun findIncludedFiles(directory: String, files: List, excludes: List, + throwOnError: Boolean = true) : List { val result = arrayListOf() files.forEach { includedFile -> @@ -26,7 +28,7 @@ class JarGenerator @Inject constructor(val dependencyManager: DependencyManager) if (File(directory, fromPath).exists()) { spec.toFiles(directory, fromPath).forEach { file -> val fullFile = File(KFiles.joinDir(directory, fromPath, file.path)) - if (! fullFile.exists()) { + if (! fullFile.exists() && throwOnError) { throw AssertionError("File should exist: $fullFile") } @@ -34,16 +36,16 @@ class JarGenerator @Inject constructor(val dependencyManager: DependencyManager) val normalized = Paths.get(file.path).normalize().toFile().path includedSpecs.add(IFileSpec.FileSpec(normalized)) } else { - log(2, "Not adding ${file.path} to jar file because it's excluded") + kobaltLog(2, "Not adding ${file.path} to jar file because it's excluded") } } } else { - log(2, "Directory $fromPath doesn't exist, not including it in the jar") + kobaltLog(2, " Directory $fromPath doesn't exist, not including it in the jar") } } if (includedSpecs.size > 0) { - log(3, "Including specs $includedSpecs") + kobaltLog(3, "Including specs $includedSpecs") result.add(IncludedFile(From(includedFile.from), To(includedFile.to), includedSpecs)) } } @@ -51,7 +53,9 @@ class JarGenerator @Inject constructor(val dependencyManager: DependencyManager) } } - fun findIncludedFiles(project: Project, context: KobaltContext, jar: Jar) : List { + override val suffix = ".jar" + + override fun findIncludedFiles(project: Project, context: KobaltContext, zip: Zip) : List { // // Add all the applicable files for the current project // @@ -59,7 +63,7 @@ class JarGenerator @Inject constructor(val dependencyManager: DependencyManager) val result = arrayListOf() val classesDir = KFiles.makeDir(buildDir.path, "classes") - if (jar.includedFiles.isEmpty()) { + 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 @@ -69,7 +73,7 @@ class JarGenerator @Inject constructor(val dependencyManager: DependencyManager) // 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), jar.excludes) + ! KFiles.Companion.isExcluded(KFiles.joinDir(project.directory, it.path), zip.excludes) } val fileSpecs = arrayListOf() filesNotExcluded.forEach { @@ -85,30 +89,30 @@ class JarGenerator @Inject constructor(val dependencyManager: DependencyManager) // // The user specified an include, just use it verbatim // - val includedFiles = findIncludedFiles(project.directory, jar.includedFiles, jar.excludes) + 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 (jar.fatJar) { - log(2, "Creating fat jar") - + if (zip.fatJar) { val seen = hashSetOf() @Suppress("UNCHECKED_CAST") - val dependentProjects = project.projectProperties.get(JvmCompilerPlugin.DEPENDENT_PROJECTS) - as List - val allDependencies = project.compileDependencies + project.compileRuntimeDependencies - val transitiveDependencies = dependencyManager.calculateDependencies(project, context, dependentProjects, - allDependencies) + 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, jar.excludes)) { - result.add(IncludedFile(specs = arrayListOf(IFileSpec.FileSpec(file.path)), + if (! KFiles.Companion.isExcluded(file, zip.excludes)) { + result.add(IncludedFile(specs = arrayListOf(IFileSpec.FileSpec(file.absolutePath)), expandJarFiles = true)) } } @@ -118,20 +122,41 @@ class JarGenerator @Inject constructor(val dependencyManager: DependencyManager) return result } - fun generateJar(project: Project, context: KobaltContext, jar: Jar) : File { - val allFiles = findIncludedFiles(project, context, jar) - + 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 = java.util.jar.Manifest()//FileInputStream(mf)) - jar.attributes.forEach { attribute -> - manifest.mainAttributes.putValue(attribute.first, attribute.second) - } - val jarFactory = { os: OutputStream -> JarOutputStream(os, 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 + } - return Archives.generateArchive(project, context, jar.name, ".jar", allFiles, - true /* expandJarFiles */, jarFactory) + 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/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/JavaInfo.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/JavaInfo.kt index 53c4e010..3c957454 100644 --- a/modules/kobalt-plugin-api/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/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Jvm.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Jvm.kt index 7dda6015..14c55efd 100644 --- a/modules/kobalt-plugin-api/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,7 +110,7 @@ public open class Jvm constructor( val pathExecutable = os.findInPath(command) if (pathExecutable != null) { - log(2, "Unable to find the $command executable using home: " + + kobaltLog(2, "Unable to find the $command executable using home: " + "$javaHome but found it on the PATH: $pathExecutable.") return pathExecutable } 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 index c31837b0..0102dd8b 100644 --- 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 @@ -3,15 +3,17 @@ 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.maven.LocalRepo import com.beust.kobalt.misc.JarUtils import com.beust.kobalt.misc.KFiles import com.beust.kobalt.misc.KobaltExecutors -import com.beust.kobalt.misc.log +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 @@ -24,9 +26,9 @@ import javax.inject.Singleton class Plugins @Inject constructor (val taskManagerProvider : Provider, val files: KFiles, val depManager: DependencyManager, - val localRepo: LocalRepo, + val settings: KobaltSettings, val executors: KobaltExecutors, - val pluginInfo: PluginInfo, + val incrementalManagerFactory: IncrementalManager.IFactory, val taskManager: TaskManager) { companion object { @@ -62,7 +64,7 @@ class Plugins @Inject constructor (val taskManagerProvider : Provider plugin.apply(project, context) } @@ -78,7 +80,26 @@ class Plugins @Inject constructor (val taskManagerProvider : Provider + 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 @@ -149,6 +170,9 @@ class Plugins @Inject constructor (val taskManagerProvider : Provider() +// @Inject +// lateinit var pluginInfo: PluginInfo + fun installPlugins(dependencies: List, scriptClassLoader: ClassLoader) { val executor = executors.newExecutor("Plugins", 5) dependencies.forEach { @@ -160,9 +184,19 @@ class Plugins @Inject constructor (val taskManagerProvider : Provider) = ids.forEach { displayDependenciesFor(it) } + 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 = - if (mavenId.hasVersion) aether.resolve(id) - else aether.latestArtifact(mavenId.groupId, mavenId.artifactId) + 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) } @@ -42,10 +66,10 @@ class ResolveDependency @Inject constructor( val seen = hashSetOf(dep.id) root.addChildren(findChildren(root, seen)) - AsciiArt.logBox(listOf(dep.id, url, dep.jarFile.get()).map { " $it" }, {s -> println(s) }) + kobaltLog(1, AsciiArt.logBox(listOf(dep.id, url, dep.jarFile.get()).map { " $it" })) display(root.children) - println("") + kobaltLog(1, "") } private fun display(nodes: List>) { @@ -57,10 +81,12 @@ class ResolveDependency @Inject constructor( else leftMiddle val indent = level * increment for(i in 0..indent - 2) { - if (i % increment == 0) print(vertical) - else print(" ") + if (!KobaltLogger.isQuiet) { + if (i == 0 || ((i + 1) % increment == 0)) print(vertical) + else print(" ") + } } - println(left + " " + dep.id) + kobaltLog(1, left + " " + dep.id + (if (dep.optional) " (optional)" else "")) display(node.children) } } @@ -73,13 +99,18 @@ class ResolveDependency @Inject constructor( if (! seen.contains(it.id)) { val dep = Dep(it, root.value.level + 1) val node = Node(dep) - log(2, "Found dependency ${dep.dep.id} level: ${dep.level}") + kobaltLog(2, "Found dependency ${dep.dep.id} level: ${dep.level}") result.add(node) seen.add(it.id) - node.addChildren(findChildren(node, seen)) + 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 + } } } - log(2, "Children for ${root.value.dep.id}: ${result.size}") + 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 index b7877264..d5507497 100644 --- 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 @@ -1,9 +1,17 @@ package com.beust.kobalt -public class SystemProperties { +class SystemProperties { companion object { - val javaBase = System.getProperty("java.home") ?: - (System.getenv("JAVA_HOME") ?: throw IllegalArgumentException("JAVA_HOME not defined")) + 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") 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 index 4d6a45f3..241bc045 100644 --- 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 @@ -1,3 +1,8 @@ package com.beust.kobalt -open public class TaskResult(val success: Boolean = true, val errorMessage: String? = null) +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/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 index ecc29809..ad026380 100644 --- 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 @@ -3,43 +3,13 @@ package com.beust.kobalt import com.beust.kobalt.api.Project import com.beust.kobalt.api.annotation.Directive -class TestConfig(val project: Project) { - val testArgs = arrayListOf() - val jvmArgs = arrayListOf() - val testIncludes = arrayListOf("**/*Test.class") - val testExcludes = arrayListOf() - - var name: String = "" - - fun args(vararg arg: String) { - testArgs.addAll(arg) - } - - fun jvmArgs(vararg arg: String) { - jvmArgs.addAll(arg) - } - - fun includes(vararg arg: String) { - testIncludes.apply { - clear() - addAll(arg) - } - } - - fun excludes(vararg arg: String) { - testExcludes.apply { - clear() - addAll(arg) - } - } -} - @Directive -fun Project.test(init: TestConfig.() -> Unit) = let { project -> +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 index d9752f66..13120fa0 100644 --- 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 @@ -2,9 +2,9 @@ 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 com.beust.kobalt.misc.log import java.io.File import java.util.* @@ -29,12 +29,12 @@ class Variant(val initialProductFlavor: ProductFlavorConfig? = null, /** * for {internal, release}, return [internalRelease, internal, release] */ - fun allDirectories(project: Project): List { - val result = arrayListOf() - result.add(toCamelcaseDir()) - if (productFlavor != null) result.add(productFlavor.name) - if (buildType != null) result.add(buildType.name) - return result + fun allDirectories(): List { + return arrayListOf().apply { + add(toCamelcaseDir()) + add(productFlavor.name) + add(buildType.name) + } } fun sourceDirectories(project: Project, context: KobaltContext, sourceSet: SourceSet) : List { @@ -70,28 +70,27 @@ class Variant(val initialProductFlavor: ProductFlavorConfig? = null, val result = arrayListOf() val sourceDirectories = sourceSet.correctSourceSet(project) .filter { File(project.directory, it).exists() } - .map { File(it) } + .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)) - log(3, "Adding source for build type ${it.name}: ${dir.path}") + 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)) - log(3, "Adding source for product flavor ${it.name}: ${dir.path}") + kobaltLog.log(project.name, 3, "Adding source for product flavor ${it.name}: ${dir.path}") result.add(dir) } - result.addAll(allDirectories(project).map { - File(KFiles.joinDir("src", it, suffix)) - }.filter { - it.exists() - }) + 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) @@ -114,8 +113,8 @@ class Variant(val initialProductFlavor: ProductFlavorConfig? = null, if (isDefault) { archiveName ?: project.name + "-" + project.version + suffix } else { - val base = if (archiveName != null) archiveName.substring(0, archiveName.length - suffix.length) - else project.name + "-" + project.version + 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 @@ -125,18 +124,16 @@ class Variant(val initialProductFlavor: ProductFlavorConfig? = null, return result } - val shortArchiveName = if (isDefault) "" else "-" + productFlavor.name + "-" + buildType.name - var generatedSourceDirectory: File? = null private fun findBuildTypeBuildConfig(project: Project, variant: Variant?) : BuildConfig? { val buildTypeName = variant?.buildType?.name - return project.buildTypes[buildTypeName]?.buildConfig ?: null + return project.buildTypes[buildTypeName]?.buildConfig } private fun findProductFlavorBuildConfig(project: Project, variant: Variant?) : BuildConfig? { val buildTypeName = variant?.productFlavor?.name - return project.productFlavors[buildTypeName]?.buildConfig ?: null + return project.productFlavors[buildTypeName]?.buildConfig } /** @@ -154,7 +151,7 @@ class Variant(val initialProductFlavor: ProductFlavorConfig? = null, /** * 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 generate any additional field (in that order to + * 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? { @@ -163,9 +160,10 @@ class Variant(val initialProductFlavor: ProductFlavorConfig? = null, if (buildConfigs.size > 0) { val pkg = project.packageName ?: project.group ?: throw KobaltException( - "packageName needs to be defined on the project in order to generate BuildConfig") + "packageName needs to be defined on the project in order to generateAndSave BuildConfig") - val contributor = ActorUtils.selectAffinityActor(context.pluginInfo.buildConfigContributors, project) + 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")) @@ -175,10 +173,10 @@ class Variant(val initialProductFlavor: ProductFlavorConfig? = null, val outputGeneratedSourceDirectory = File(result, pkg.replace('.', File.separatorChar)) val outputDir = File(outputGeneratedSourceDirectory, "BuildConfig." + contributor.buildConfigSuffix) KFiles.saveFile(outputDir, code) - log(2, "Generated ${outputDir.path}") + context.logger.log(project.name, 2, "Generated ${outputDir.path}") return result } else { - throw KobaltException("Couldn't find a contributor to generate BuildConfig") + throw KobaltException("Couldn't find a contributor to generateAndSave BuildConfig") } } else { return null @@ -187,9 +185,10 @@ class Variant(val initialProductFlavor: ProductFlavorConfig? = null, override fun toString() = toTask("") + companion object { val DEFAULT_PRODUCT_FLAVOR = ProductFlavorConfig("") - val DEFAULT_BUILD_TYPE = BuildTypeConfig(null, "") + val DEFAULT_BUILD_TYPE = BuildTypeConfig("") fun allVariants(project: Project): List { val result = arrayListOf() @@ -222,7 +221,7 @@ class Variant(val initialProductFlavor: ProductFlavorConfig? = null, } fun toCamelcaseDir() : String { - fun lci(s : String) = if (s.length == 0 || s.length == 1) s else s[0].toLowerCase() + s.substring(1) + 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() } 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/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/CompilerActionInfo.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/CompilerActionInfo.kt index 480e5a38..e323e474 100644 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/CompilerActionInfo.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/CompilerActionInfo.kt @@ -10,6 +10,7 @@ data class CompilerActionInfo(val directory: String?, 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/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/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 index 35553f74..ef9d3b4d 100644 --- 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 @@ -5,7 +5,7 @@ import com.beust.kobalt.Variant /** * Plug-ins that can generate a BuildConfig file. */ -interface IBuildConfigContributor : ISimpleAffinity { +interface IBuildConfigContributor : IProjectAffinity { fun generateBuildConfig(project: Project, context: KobaltContext, packageName: String, variant: Variant, buildConfigs: List) : String diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildDirectoryInterceptor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IBuildDirectoryInterceptor.kt index 5a317c0f..afb0f1ef 100644 --- a/modules/kobalt-plugin-api/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 : IInterceptor { +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/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathDependency.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathDependency.kt index 80d9e998..527e6f13 100644 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathDependency.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IClasspathDependency.kt @@ -22,15 +22,20 @@ interface IClasspathDependency { /** @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) */ 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/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 index 84f6694d..b6ce8fd2 100644 --- 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 @@ -2,7 +2,12 @@ package com.beust.kobalt.api import com.beust.kobalt.TaskResult -interface ICompiler : Comparable { +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"). */ @@ -27,7 +32,7 @@ interface ICompiler : Comparable { */ val priority: Int get() = DEFAULT_PRIORITY - override fun compareTo(other: ICompiler) = priority.compareTo(other.priority) + override fun compareTo(other: ICompilerDescription) = priority.compareTo(other.priority) /** * Can this compiler be passed directories or does it need individual source files? @@ -36,5 +41,28 @@ interface ICompiler : Comparable { } interface ICompilerContributor : IProjectAffinity, IContributor { - fun compilersFor(project: Project, context: KobaltContext): List + 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 index 5962e9fb..2d8febe4 100644 --- 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 @@ -3,13 +3,27 @@ package com.beust.kobalt.api /** * Plugins that add compiler flags. */ -interface ICompilerFlagContributor : IContributor { - fun flagsFor(project: Project, context: KobaltContext, currentFlags: List, - suffixesBeingCompiled: List): List - val flagPriority: Int - get() = DEFAULT_FLAG_PRIORITY - +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/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 index aec3b3db..3a66f980 100644 --- 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 @@ -1,5 +1,12 @@ 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. */ @@ -7,12 +14,12 @@ interface IDependencyManager { /** * Parse the id and return the correct IClasspathDependency */ - fun create(id: String, project: Project? = null): IClasspathDependency + fun create(id: String, optional: Boolean = false, projectDirectory: String? = null): IClasspathDependency /** * Create an IClasspathDependency from a Maven id. */ - fun createMaven(id: String): IClasspathDependency + fun createMaven(id: String, optional: Boolean = false): IClasspathDependency /** * Create an IClasspathDependency from a path. @@ -22,7 +29,7 @@ interface IDependencyManager { /** * @return the source dependencies for this project, including the contributors. */ - fun dependencies(project: Project, context: KobaltContext): List + fun dependencies(project: Project, context: KobaltContext, scopes: List): List /** * @return the test dependencies for this project, including the contributors. @@ -34,6 +41,48 @@ interface IDependencyManager { * allDependencies is typically either compileDependencies or testDependencies */ fun calculateDependencies(project: Project?, context: KobaltContext, - dependentProjects: List = emptyList(), - vararg allDependencies: List): List + 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/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/IPluginActor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IPluginActor.kt index 5cf11f87..96d54218 100644 --- 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 @@ -10,3 +10,6 @@ interface IPluginActor { interface IContributor : IPluginActor interface IInterceptor : IPluginActor + +interface IListener : IPluginActor + diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IRunnerContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IRunnerContributor.kt index 5ab88cb0..f8c28b52 100644 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IRunnerContributor.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/IRunnerContributor.kt @@ -1,10 +1,11 @@ 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, IProjectAffinity { /** 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 index 2711248b..b9c7cdfe 100644 --- 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 @@ -8,5 +8,5 @@ interface ISimpleAffinity : IAffinity { * @return an integer indicating the affinity of your actor. The actor that returns * the highest affinity gets selected. */ - fun affinity(arg: T) : Int + fun affinity(project: T) : Int } \ No newline at end of file diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ISourceDirectoriesInterceptor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ISourceDirectoriesInterceptor.kt index aa39712b..33ec6e7c 100644 --- a/modules/kobalt-plugin-api/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 ISourceDirectoryIncerceptor : IInterceptor { +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/ITaskContributor.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/ITaskContributor.kt index 48f5173c..c606d54f 100644 --- 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 @@ -8,7 +8,7 @@ import com.beust.kobalt.internal.TaskResult2 * to implement this interface. */ interface ITaskContributor : IContributor { - fun tasksFor(context: KobaltContext) : List + fun tasksFor(project: Project, context: KobaltContext) : List } class DynamicTask(override val plugin: IPlugin, override val name: String, override val doc: String, @@ -23,8 +23,10 @@ class DynamicTask(override val plugin: IPlugin, override val name: String, overr override fun call(): TaskResult2 { val taskResult = closure.invoke(project) - return TaskResult2(taskResult.success, taskResult.errorMessage, this) + 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/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/JarTemplate.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/JarTemplate.kt deleted file mode 100644 index 1966b747..00000000 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/JarTemplate.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.beust.kobalt.api - -import com.beust.kobalt.Args -import com.beust.kobalt.misc.KFiles -import com.beust.kobalt.misc.log -import java.io.File -import java.io.FileOutputStream -import java.util.jar.JarInputStream - -/** - * Base class for templates that simply decompress a jar file to generate their project. - */ -abstract class JarTemplate(val jarName: String) : ITemplate { - companion object { - fun extractFile(ins: JarInputStream, destDir: File) { - 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 - } - - log(2, "Extracting: $entry to ${f.absolutePath}") - FileOutputStream(f).use { fos -> - KFiles.copy(ins, fos) - } - entry = ins.nextEntry - } - } - } - - override fun generateTemplate(args: Args, classLoader: ClassLoader) { - log(2, "Generating template with class loader $classLoader") - val destDir = File(".") - val ins = JarInputStream(classLoader.getResource(jarName).openConnection().inputStream) - extractFile(ins, destDir) - } - -} 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 index 29b9a6b2..7d37a0b8 100644 --- 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 @@ -4,6 +4,8 @@ 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 @@ -30,18 +32,16 @@ class Kobalt { var context: KobaltContext? = null /** - * @return the repos calculated from the following places: - * - Either repos specified in settings.xml or from Constants.DEFAULT_REPOS - * - Repos from the build file + * @return the repos calculated from various places where repos can be specified. */ val repos : Set get() { - val settingsRepos = Kobalt.context?.settings?.defaultRepos ?: emptyList() + 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) - .map { HostConfig(it) }) + ) // Repo from in the settings Kobalt.context?.settings?.kobaltCompilerRepo?.let { @@ -56,6 +56,9 @@ class Kobalt { // Repos from the build file result.addAll(reposFromBuildFiles) + result.forEach { + KobaltMavenResolver.initAuthentication(it) + } return result.toHashSet() } @@ -65,6 +68,13 @@ class Kobalt { 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 @@ -112,5 +122,20 @@ class Kobalt { 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 index 292bb255..b7e5ace8 100644 --- 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 @@ -1,26 +1,74 @@ 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) { - var variant: Variant = Variant() + lateinit var variant: Variant val profiles = arrayListOf() init { - args.profiles?.split(",")?.filterNotNull()?.forEach { + 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() @@ -32,6 +80,10 @@ class KobaltContext(val args: Args) { 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 { @@ -53,4 +105,12 @@ class InternalContext { * 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/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 index bf4a732b..e54e30ec 100644 --- 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 @@ -2,13 +2,17 @@ package com.beust.kobalt.api import com.beust.kobalt.TestConfig import com.beust.kobalt.api.annotation.Directive -import com.beust.kobalt.internal.JvmCompilerPlugin 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 = "", @@ -18,22 +22,27 @@ open class Project( @Directive open var group: String? = null, @Directive open var artifactId: String? = null, @Directive open var packaging: String? = null, - @Directive open var dependencies: Dependencies? = null, @Directive open var description : String = "", - @Directive open var scm : Scm? = null, @Directive open var url: String? = null, - @Directive open var licenses: List = arrayListOf(), - @Directive open var packageName: String? = group) : IBuildConfig { + @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) { - val dependsOn = arrayListOf() - var isDirty = false /** * @return true if any of the projects we depend on is dirty. */ - fun dependsOnDirtyProjects(project: Project) = project.projectExtra.dependsOn.any { it.projectExtra.isDirty } + fun dependsOnDirtyProjects(project: Project) = project.allProjectDependedOn().any { it.projectExtra.isDirty } } /** @@ -44,7 +53,7 @@ open class Project( val testConfigs = arrayListOf() - // If one is specified by default, we only generate a BuildConfig, find a way to fix that + // 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() @@ -52,10 +61,6 @@ open class Project( override fun equals(other: Any?) = name == (other as Project).name override fun hashCode() = name.hashCode() - /** Can be used by plug-ins */ - val dependentProjects : List - get() = projectProperties.get(JvmCompilerPlugin.DEPENDENT_PROJECTS) as List - 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") @@ -83,23 +88,11 @@ open class Project( // Dependencies // - @Directive - fun dependencies(init: Dependencies.() -> Unit) : Dependencies { - dependencies = Dependencies(this, compileDependencies, compileProvidedDependencies, - compileRuntimeDependencies, excludedDependencies) - dependencies!!.init() - return dependencies!! - } - - val compileDependencies : ArrayList = arrayListOf() - val compileProvidedDependencies : ArrayList = arrayListOf() - val compileRuntimeDependencies : ArrayList = arrayListOf() - val excludedDependencies : ArrayList = arrayListOf() - @Directive fun dependenciesTest(init: Dependencies.() -> Unit) : Dependencies { - dependencies = Dependencies(this, testDependencies, testProvidedDependencies, compileRuntimeDependencies, - excludedDependencies) + dependencies = Dependencies(this, testDependencies, arrayListOf(), + testProvidedDependencies, compileOnlyDependencies, compileRuntimeDependencies, + excludedDependencies, nativeDependencies) dependencies!!.init() return dependencies!! } @@ -107,6 +100,9 @@ open class Project( 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 @@ -132,6 +128,20 @@ open class Project( }) 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) { @@ -143,9 +153,12 @@ class Sources(val project: Project, val sources: HashSet) { class Dependencies(val project: Project, val dependencies: ArrayList, + val optionalDependencies: ArrayList, val providedDependencies: ArrayList, + val compileOnlyDependencies: ArrayList, val runtimeDependencies: ArrayList, - val excludedDependencies: ArrayList) { + val excludedDependencies: ArrayList, + val nativeDependencies: ArrayList) { /** * Add the dependencies to the given ArrayList and return a list of future jar files corresponding to @@ -154,35 +167,107 @@ class Dependencies(val project: Project, * future tasks receive a get(), the repos will be correct. */ private fun addToDependencies(project: Project, dependencies: ArrayList, - dep: Array): List> - = with(dep.map { DependencyManager.create(it, project)}) { + 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 provided(vararg dep: String) = addToDependencies(project, providedDependencies, dep) + 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) -} - -class Scm(val connection: String, val developerConnection: String, val url: String) - -class License(val name: String, val url: String) { - fun toMavenLicense() : org.apache.maven.model.License { - val result = org.apache.maven.model.License() - result.name = name - result.url = url - return result - } + @Directive + fun native(vararg dep: String) = addToDependencies(project, nativeDependencies, dep) } class BuildConfig { @@ -208,39 +293,22 @@ interface IBuildConfig { } } -class ProductFlavorConfig(val name: String) : IBuildConfig { - var applicationId: String? = null - override var buildConfig : BuildConfig? = BuildConfig() -} - -@Directive -fun Project.productFlavor(name: String, init: ProductFlavorConfig.() -> Unit) = ProductFlavorConfig(name).apply { - init() - addProductFlavor(name, this) - } - -class BuildTypeConfig(val project: Project?, val name: String) : IBuildConfig { - var minifyEnabled = false - var applicationIdSuffix: String? = null - var proguardFile: String? = null - -// fun getDefaultProguardFile(name: String) : String { -// val androidPlugin = Plugins.findPlugin(AndroidPlugin.PLUGIN_NAME) as AndroidPlugin -// return Proguard(androidPlugin.androidHome(project)).getDefaultProguardFile(name) -// } - - override var buildConfig : BuildConfig? = BuildConfig() -} - -@Directive -fun Project.buildType(name: String, init: BuildTypeConfig.() -> Unit) = BuildTypeConfig(this, name).apply { - init() - addBuildType(name, this) - } - 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/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 index c1586211..c45a820d 100644 --- 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 @@ -2,8 +2,6 @@ package com.beust.kobalt.api import com.beust.kobalt.misc.toString -data public class Task(val pluginName: String, val taskName: String) { - override public fun toString() : String { - return toString("Task", pluginName, taskName) - } -} \ No newline at end of file +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 index 2cae53fa..8c68be94 100644 --- 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 @@ -3,6 +3,7 @@ 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 @@ -23,7 +24,7 @@ class TaskContributor @Inject constructor(val incrementalManagerFactory: Increme * depends on variants of that task. */ fun addVariantTasks(plugin: IPlugin, project: Project, context: KobaltContext, taskName: String, - group: String, + group: String = AnnotationDefault.GROUP, dependsOn: List = emptyList(), reverseDependsOn : List = emptyList(), runBefore : List = emptyList(), @@ -43,8 +44,27 @@ class TaskContributor @Inject constructor(val incrementalManagerFactory: Increme } } + 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, + group: String = AnnotationDefault.GROUP, dependsOn: List = emptyList(), reverseDependsOn : List = emptyList(), runBefore : List = emptyList(), @@ -52,6 +72,7 @@ class TaskContributor @Inject constructor(val incrementalManagerFactory: Increme 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) }, @@ -61,5 +82,5 @@ class TaskContributor @Inject constructor(val incrementalManagerFactory: Increme } } - override fun tasksFor(context: KobaltContext) : List = dynamicTasks + 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 index baa87322..f4269d47 100644 --- 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 @@ -6,6 +6,10 @@ package com.beust.kobalt.api.annotation */ annotation class Directive +object AnnotationDefault { + const val GROUP = "other" +} + @Retention(AnnotationRetention.RUNTIME) annotation class Task( /* This task's name */ @@ -15,7 +19,7 @@ annotation class Task( val description: String = "", /** Used to show the task in the correct group in the IDE */ - val group: String = "other", + val group: String = AnnotationDefault.GROUP, /** Dependency: tasks this task depends on */ val dependsOn: Array = arrayOf(), @@ -42,7 +46,7 @@ annotation class IncrementalTask( val description: String = "", /** Used to show the task in the correct group in the IDE */ - val group: String = "other", + val group: String = AnnotationDefault.GROUP, /** Dependency: tasks this task depends on */ val dependsOn: Array = arrayOf(), 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 index ca808318..5334e09f 100644 --- 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 @@ -1,25 +1,24 @@ package com.beust.kobalt.archive -import com.beust.kobalt.Features +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.IncludedFile import com.beust.kobalt.misc.JarUtils import com.beust.kobalt.misc.KFiles -import com.beust.kobalt.misc.log +import com.beust.kobalt.misc.kobaltLog import java.io.File -import java.io.FileOutputStream -import java.io.OutputStream import java.util.* -import java.util.zip.ZipOutputStream 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" - private val DEFAULT_STREAM_FACTORY = { os : OutputStream -> ZipOutputStream(os) } + fun defaultArchiveName(project: Project) = project.name + + if (project.version.isNullOrBlank()) "" else "-${project.version}" fun generateArchive(project: Project, context: KobaltContext, @@ -27,23 +26,28 @@ class Archives { suffix: String, includedFiles: List, expandJarFiles : Boolean = false, - outputStreamFactory: (OutputStream) -> ZipOutputStream = DEFAULT_STREAM_FACTORY) : File { + 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) - log(3, "Creating $result") + context.logger.log(project.name, 3, "Creating $result") if (! Features.USE_TIMESTAMPS || isOutdated(project.directory, includedFiles, result)) { - val outStream = outputStreamFactory(FileOutputStream(result)) - JarUtils.addFiles(project.directory, includedFiles, outStream, expandJarFiles) - log(2, text = "Added ${includedFiles.size} files to $result") - outStream.flush() - outStream.close() - log(1, " Created $result") - } else { - log(3, " $result is up to date") - } + 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 + } - project.projectProperties.put(JAR_NAME, result.absolutePath) + } else { + context.logger.log(project.name, 3, " $result is up to date") + } return result } @@ -55,13 +59,21 @@ class Archives { includedFiles.forEach { root -> val allFiles = root.allFromFiles(directory) allFiles.forEach { relFile -> - val file = File(KFiles.joinDir(directory, root.from, relFile.path)) + 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) { - log(3, " TS - Outdated $file and $output " + 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 } } } 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 index fbbf23a0..d5086cbd 100644 --- 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 @@ -1,11 +1,14 @@ 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 var name: String? = null, var fatJar: Boolean = false) : Zip(name), AttributeHolder { +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) @@ -15,7 +18,7 @@ open class Jar(override var name: String? = null, var fatJar: Boolean = false) : // Need to specify the version or attributes will just be dropped @Directive - val attributes = arrayListOf(Pair("Manifest-Version", "1.0")) + 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/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 index c98ebb70..978f21bf 100644 --- 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 @@ -1,10 +1,12 @@ package com.beust.kobalt.archive +import com.beust.kobalt.api.Project import com.beust.kobalt.glob -class War(override var name: String? = null) : Jar(name), AttributeHolder { +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("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 index 93178c03..41957218 100644 --- 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 @@ -1,21 +1,13 @@ package com.beust.kobalt.archive -import com.beust.kobalt.Glob -import com.beust.kobalt.IFileSpec +import com.beust.kobalt.* +import com.beust.kobalt.api.Project import com.beust.kobalt.api.annotation.Directive -import com.beust.kobalt.misc.From -import com.beust.kobalt.misc.IncludedFile -import com.beust.kobalt.misc.To -open class Zip(open var name: String? = null) { +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 from(s: String) = From(s) - - @Directive - fun to(s: String) = To(s) - @Directive fun exclude(vararg files: String) { files.forEach { excludes.add(Glob(it)) } @@ -27,26 +19,9 @@ open class Zip(open var name: String? = null) { } @Directive - fun include(vararg files: String) { - includedFiles.add(IncludedFile(files.map { IFileSpec.FileSpec(it) })) + open val attributes = arrayListOf(Pair("Manifest-Version", "1.0")) + + override fun addAttribute(k: String, v: String) { + attributes.add(Pair(k, v)) } - - @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))) - } - - /** - * 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() - } - - 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 index 42aac9e2..e9b315a5 100644 --- 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 @@ -1,7 +1,6 @@ package com.beust.kobalt.internal import com.beust.kobalt.api.IProjectAffinity -import com.beust.kobalt.api.ISimpleAffinity import com.beust.kobalt.api.KobaltContext import com.beust.kobalt.api.Project @@ -11,19 +10,13 @@ class ActorUtils { * Return the plug-in actor with the highest affinity. */ fun selectAffinityActor(project: Project, context: KobaltContext, actors: List) - = actors.maxBy { it.affinity(project, context) } + = 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) } - - /** - * Return the plug-in actor with the highest affinity. - */ - fun , A> selectAffinityActor(actors: List, arg: A) = actors.maxBy { it.affinity(arg) } + .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 index 44d2fe81..e89fc9aa 100644 --- 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 @@ -1,9 +1,7 @@ package com.beust.kobalt.internal -import com.beust.kobalt.api.BasePlugin -import com.beust.kobalt.api.ConfigActor -import com.beust.kobalt.api.ICompilerFlagContributor -import com.beust.kobalt.api.IConfigActor +import com.beust.kobalt.api.* +import com.beust.kobalt.misc.KFiles /** * Base class for JVM language plug-ins. @@ -14,7 +12,7 @@ abstract class BaseJvmPlugin(open val configActor: ConfigActor) : ICompilerFlagContributor { companion object { // Run before other flag contributors - val FLAG_CONTRIBUTOR_PRIORITY = ICompilerFlagContributor.DEFAULT_FLAG_PRIORITY - 10 + val FLAG_CONTRIBUTOR_PRIORITY = FlagContributor.DEFAULT_FLAG_PRIORITY - 10 } protected fun maybeCompilerArgs(sourceSuffixes: List, suffixesBeingCompiled: List, @@ -23,4 +21,17 @@ abstract class BaseJvmPlugin(open val configActor: ConfigActor) : 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/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/DocUrl.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/DocUrl.kt index 8acab0ab..93010294 100644 --- a/modules/kobalt-plugin-api/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 index b02b8b22..a3e26afd 100644 --- 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 @@ -1,46 +1,101 @@ package com.beust.kobalt.internal -import com.beust.kobalt.KobaltException -import com.beust.kobalt.TaskResult -import com.beust.kobalt.misc.NamedThreadFactory -import com.beust.kobalt.misc.error -import com.beust.kobalt.misc.log +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, errorMessage: String?, val value: T) : TaskResult(success, errorMessage) { +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 Node(val value: T) { - override fun hashCode() = value!!.hashCode() - override fun equals(other: Any?) : Boolean { - val result = if (other is Node<*>) other.value == value else false - return result - } - override fun toString() = value.toString() -} - class DynamicGraph { - val VERBOSE = 2 + val VERBOSE = 3 val values : Collection get() = nodes.map { it.value } - val nodes = hashSetOf>() - private val dependedUpon = HashMultimap.create, Node>() - private val dependingOn = HashMultimap.create, Node>() + 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(Node(t)) + nodes.add(PrivateNode(t)) } fun removeNode(t: T) = synchronized(nodes) { - log(VERBOSE, " Removing $t") - Node(t).let { node -> + kobaltLog(VERBOSE, " Removing node $t") + PrivateNode(t).let { node -> nodes.remove(node) dependingOn.removeAll(node) val set = dependedUpon.keySet() - val toReplace = arrayListOf, Collection>>>() + val toReplace = arrayListOf, Collection>>>() set.forEach { du -> val l = ArrayList(dependedUpon[du]) l.remove(node) @@ -56,10 +111,10 @@ class DynamicGraph { * Make "from" depend on "to" ("from" is no longer free). */ fun addEdge(from: T, to: T) { - val fromNode = Node(from) + val fromNode = PrivateNode(from) nodes.add(fromNode) - val toNode = Node(to) - nodes.add(Node(to)) + val toNode = PrivateNode(to) + nodes.add(PrivateNode(to)) dependingOn.put(toNode, fromNode) dependedUpon.put(fromNode, toNode) } @@ -75,6 +130,7 @@ class DynamicGraph { } } val result = nodes.map { it.value }.filter { !nonFree.contains(it) }.toHashSet() + kobaltLog(VERBOSE, " Free nodes: $result") return result } } @@ -82,7 +138,7 @@ class DynamicGraph { fun dump() : String { val result = StringBuffer() result.append("************ Graph dump ***************\n") - val free = arrayListOf>() + val free = arrayListOf>() nodes.forEach { node -> val d = dependedUpon.get(node) if (d == null || d.isEmpty()) { @@ -107,6 +163,8 @@ interface IWorker : Callable> { */ // val tasks : List + val name: String + /** * @return the priority of this task. */ @@ -125,11 +183,18 @@ interface IThreadWorkerFactory { fun createWorkers(nodes: Collection) : List> } -class DynamicGraphExecutor(val graph : DynamicGraph, val factory: IThreadWorkerFactory) { - val executor = Executors.newFixedThreadPool(5, NamedThreadFactory("DynamicGraphExecutor")) +class DynamicGraphExecutor(val graph : DynamicGraph, val factory: IThreadWorkerFactory, + val threadCount: Int = 1) { + val executor : ExecutorService + = Executors.newFixedThreadPool(threadCount, NamedThreadFactory("DynamicGraphExecutor")) val completion = ExecutorCompletionService>(executor) - fun run() : Int { + 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 { @@ -137,14 +202,31 @@ class DynamicGraphExecutor(val graph : DynamicGraph, val factory: IThreadW } } - private fun run2() : Int { + private fun run2() : TaskResult { var running = 0 - var gotError = false val nodesRun = hashSetOf() - var newFreeNodes = HashSet(graph.freeNodes) - while (! gotError && (running > 0 || newFreeNodes.size > 0)) { + 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) + 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 @@ -154,17 +236,19 @@ class DynamicGraphExecutor(val graph : DynamicGraph, val factory: IThreadW running-- if (taskResult.success) { nodesRun.add(taskResult.value) - log(2, "Task succeeded: $taskResult") + kobaltLog(3, "Task succeeded: $taskResult") graph.removeNode(taskResult.value) newFreeNodes.clear() newFreeNodes.addAll(graph.freeNodes.minus(nodesRun)) } else { - log(2, "Task failed: $taskResult") + kobaltLog(3, "Task failed: $taskResult") newFreeNodes.clear() - gotError = true + if (failedResult == null) { + failedResult = taskResult + } } } catch(ex: TimeoutException) { - log(2, "Time out") + kobaltLog(3, "Time out") } catch(ex: Exception) { val ite = ex.cause if (ite is InvocationTargetException) { @@ -172,15 +256,125 @@ class DynamicGraphExecutor(val graph : DynamicGraph, val factory: IThreadW throw (ex.cause as InvocationTargetException).targetException } else { error("Error: ${ite.cause?.message}", ite.cause) - gotError = true + failedResult = TaskResult(success = false, errorMessage = ite.cause?.message) } } else { error("Error: ${ex.message}", ex) - gotError = true + failedResult = TaskResult(success = false, errorMessage = ex.message) } } } - return if (gotError) 1 else 0 + 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()) } } @@ -199,11 +393,12 @@ fun main(argv: Array) { return nodes.map { object: IWorker { override fun call(): TaskResult2? { - log(1, " Running worker $it") - return TaskResult2(true, null, it) + kobaltLog(1, " Running worker $it") + return TaskResult2(true, value = it) } override val priority: Int get() = 0 + override val name: String = "workerName" } } } 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 index 90266dab..995dba53 100644 --- 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 @@ -3,8 +3,8 @@ package com.beust.kobalt.internal import com.beust.kobalt.* import com.beust.kobalt.api.* import com.beust.kobalt.misc.KFiles -import com.beust.kobalt.misc.log import com.google.common.annotations.VisibleForTesting +import com.google.inject.Inject import java.io.File import java.util.* @@ -15,49 +15,113 @@ import java.util.* abstract class GenericTestRunner: ITestRunnerContributor { abstract val dependencyName : String abstract val mainClass: String - abstract fun args(project: Project, classpath: List, testConfig: TestConfig) : List + 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(runTests(project, context, classpath, configName)) + classpath: List) : TaskResult { + val tr = runTests(project, context, classpath, configName) + return TaskResult(tr.success, testResult = tr) + } - override fun affinity(project: Project, context: KobaltContext) = - if (project.testDependencies.any { it.id.contains(dependencyName)}) IAffinity.DEFAULT_POSITIVE_AFFINITY + 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, testConfig: TestConfig): List { - val path = KFiles.joinDir(project.buildDirectory, KFiles.TEST_CLASSES_DIR).apply { + 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 result = IFileSpec.GlobSpec(toClassPaths(testConfig.testIncludes)) - .toFiles(project.directory, path, testConfig.testExcludes.map { - Glob(it) - }).map { - it.toString().replace("/", ".").replace("\\", ".").replace(".class", "") - } + 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 +// } - log(2, "Found ${result.size} test classes") - return 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 */ - fun runTests(project: Project, context: KobaltContext, classpath: List, - configName: String) : Boolean { + 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, classpath, testConfig) + val args = args(project, context, classpath, testConfig) if (args.size > 0) { - val java = JavaInfo.create(File(SystemProperties.javaBase)).javaExecutable + val java = jvm.javaExecutable val jvmArgs = calculateAllJvmArgs(project, context, testConfig, classpath, Kobalt.INJECTOR.getInstance (PluginInfo::class.java)) val allArgs = arrayListOf().apply { @@ -70,24 +134,28 @@ abstract class GenericTestRunner: ITestRunnerContributor { val pb = ProcessBuilder(allArgs) pb.directory(File(project.directory)) pb.inheritIO() - log(2, "Running tests with classpath size ${classpath.size}") - log(2, "Launching " + allArgs.joinToString(" ")) + 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() - val errorCode = process.waitFor() - if (errorCode == 0) { - log(1, "All tests passed") - } else { - log(1, "Test failures") - } + errorCode = process.waitFor() result = result || errorCode == 0 } else { - log(2, "Couldn't find any test classes") + context.logger.log(project.name, 1, " No tests to run") result = true } } else { throw KobaltException("Couldn't find a test configuration named \"$configName\"") } - return result + + 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) } /* @@ -96,11 +164,13 @@ abstract class GenericTestRunner: ITestRunnerContributor { @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(classpath.map { it.jarFile.get().absolutePath }.joinToString(File.pathSeparator)) + add(fullClasspath.joinToString(File.pathSeparator)) } // JVM flags from the contributors @@ -109,7 +179,7 @@ abstract class GenericTestRunner: ITestRunnerContributor { } // JVM flags from the interceptors (these overwrite flags instead of just adding to the list) - var result = ArrayList(jvmFlags + jvmFlagsFromContributors) + val result = ArrayList(jvmFlags + jvmFlagsFromContributors) pluginInfo.testJvmFlagInterceptors.forEach { val newFlags = it.testJvmFlagsFor(project, context, result) result.clear() @@ -117,7 +187,8 @@ abstract class GenericTestRunner: ITestRunnerContributor { } if (result.any()) { - log(2, "Final JVM test flags after running the contributors and interceptors: $result") + 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 index f74c9e61..067cf0c1 100644 --- 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 @@ -7,7 +7,7 @@ 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.log +import com.beust.kobalt.misc.kobaltLog import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.inject.Inject @@ -19,16 +19,17 @@ import java.nio.file.Files import java.nio.file.Paths import java.util.* -data class TaskInfo(val taskName: String, var inputChecksum: String? = null, var outputChecksum: String? = null) - -class BuildInfo(var tasks: List) - /** * 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 } @@ -54,6 +55,7 @@ class IncrementalManager @Inject constructor(val args: Args, @Assisted val fileN 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())) } @@ -61,31 +63,40 @@ class IncrementalManager @Inject constructor(val args: Args, @Assisted val fileN = taskInfos.getOrPut(taskName, { -> TaskInfo(taskName) }) fun saveInputChecksum(taskName: String, inputChecksum: String) { - with(taskInfos()) { - taskInfoFor(this, taskName).inputChecksum = inputChecksum - save(this) + 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) { - with(taskInfos()) { - taskInfoFor(this, taskName).outputChecksum = outputChecksum - save(this) + 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 = Variant()): (Project) -> TaskResult { + variant: Variant): (Project) -> TaskResult { return { project: Project -> Kobalt.context?.variant = variant val iti = method(project) @@ -93,7 +104,8 @@ class IncrementalManager @Inject constructor(val args: Args, @Assisted val fileN var upToDate = false var taskOutputChecksum : String? = null - if (args.noIncremental || (Kobalt.context?.internalContext?.buildFileOutOfDate as Boolean)) { + 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 // @@ -124,7 +136,8 @@ class IncrementalManager @Inject constructor(val args: Args, @Assisted val fileN if (outputChecksum == taskOutputChecksum) { upToDate = true } else { - logIncremental(LEVEL, "Incremental task $taskName output is out of date, running it") + logIncremental(LEVEL, "Incremental task $taskName output is out of date" + + " (different output checksums), running it") } } } else { @@ -132,7 +145,7 @@ class IncrementalManager @Inject constructor(val args: Args, @Assisted val fileN logIncremental(LEVEL, "Project ${project.name} depends on dirty project, running $taskName") } else { logIncremental(LEVEL, "Incremental task $taskName input is out of date, running it" - + " old: $inputChecksum new: ${iti.inputChecksum()}") + + " (different input checksums old: $inputChecksum new: ${iti.inputChecksum()})") } project.projectExtra.isDirty = true } @@ -169,5 +182,5 @@ class IncrementalManager @Inject constructor(val args: Args, @Assisted val fileN } val LEVEL = 2 - private fun logIncremental(level: Int, s: String) = log(level, " INC - $s") + 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 index 491b2139..c5b36997 100644 --- 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 @@ -2,15 +2,31 @@ 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) } + } - override fun args(project: Project, classpath: List, testConfig: TestConfig) - = findTestClasses(project, testConfig) } diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JvmCompiler.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JvmCompiler.kt index ba330537..e7773737 100644 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JvmCompiler.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/JvmCompiler.kt @@ -15,47 +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 currentFlags = arrayListOf().apply { addAll(info.compilerArgs) } - val contributorFlags : List = if (project != null) { - val contributors = context.pluginInfo.compilerFlagContributors - contributors.sortBy { it.flagPriority } - context.pluginInfo.compilerFlagContributors.forEach { - currentFlags.addAll(it.flagsFor(project, context, currentFlags, info.suffixesBeingCompiled)) - } - currentFlags - } 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(project?.name, 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(projectName: String?, 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 index db4519c0..5e2a9354 100644 --- 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 @@ -9,11 +9,11 @@ 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.LocalRepo 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.log +import com.beust.kobalt.misc.error import com.beust.kobalt.misc.warn import java.io.File import java.util.* @@ -21,24 +21,21 @@ import javax.inject.Inject import javax.inject.Singleton /** - * This plug-in takes care of compilation: it declares a bunch of tasks ("compile", "compileTest") and + * 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 localRepo: LocalRepo, open val files: KFiles, open val dependencyManager: DependencyManager, open val executors: KobaltExecutors, - open val taskContributor : TaskContributor) + open val taskContributor : TaskContributor, + val compilerUtils: CompilerUtils) : BasePlugin(), ISourceDirectoryContributor, IProjectContributor, ITaskContributor by taskContributor { companion object { val PLUGIN_NAME = "JvmCompiler" - @ExportedProjectProperty(doc = "Projects this project depends on", type = "List") - const val DEPENDENT_PROJECTS = "dependentProjects" - @ExportedProjectProperty(doc = "Compiler args", type = "List") const val COMPILER_ARGS = "compilerArgs" @@ -58,13 +55,6 @@ open class JvmCompilerPlugin @Inject constructor( override fun accept(project: Project) = true - /** - * Log with a project. - */ - protected fun lp(project: Project, s: String) { - log(2, "${project.name}: $s") - } - override fun apply(project: Project, context: KobaltContext) { super.apply(project, context) // cleanUpActors() @@ -76,7 +66,7 @@ open class JvmCompilerPlugin @Inject constructor( // users don't have to specify a test{} // if (project.testConfigs.isEmpty()) { - project.testConfigs.add(TestConfig(project)) + project.testConfigs.add(TestConfig(project, isDefault = true)) } project.testConfigs.forEach { config -> val taskName = if (config.name.isEmpty()) TASK_TEST else TASK_TEST + config.name @@ -89,20 +79,28 @@ open class JvmCompilerPlugin @Inject constructor( } private fun taskTest(project: Project, configName: String): TaskResult { - lp(project, "Running tests: $configName") + context.logger.log(project.name, 2, "Running tests: $configName") - val runContributor = ActorUtils.selectAffinityActor(project, context, + val testContributor = ActorUtils.selectAffinityActor(project, context, context.pluginInfo.testRunnerContributors) - if (runContributor != null && runContributor.affinity(project, context) > 0) { - return runContributor.run(project, context, configName, dependencyManager.testDependencies(project, - context)) + 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 { - log(1, "Couldn't find a test runner for project ${project.name}, did you specify a dependenciesTest{}?") + 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) + @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()) { @@ -112,41 +110,9 @@ open class JvmCompilerPlugin @Inject constructor( return TaskResult() } - /** - * Copy the resources from a source directory to the build one - */ - protected fun copyResources(project: Project, sourceSet: SourceSet) { - var outputDir = sourceSet.outputDir - - val variantSourceDirs = context.variant.resourceDirectories(project, sourceSet) - if (variantSourceDirs.size > 0) { - lp(project, "Copying $sourceSet resources") - val absOutputDir = File(KFiles.joinDir(project.directory, project.buildDirectory, outputDir)) - variantSourceDirs.map { File(project.directory, it.path) }.filter { - it.exists() - }.forEach { - log(2, "Copying from $it to $absOutputDir") - KFiles.copyRecursively(it, absOutputDir, deleteFirst = false) - } - } else { - lp(project, "No resources to copy for $sourceSet") - } - } - - protected fun compilerArgsFor(project: Project): List { - val result = project.projectProperties.get(COMPILER_ARGS) - if (result != null) { - @Suppress("UNCHECKED_CAST") - return result as List - } else { - return emptyList() - } - } - @IncrementalTask(name = TASK_COMPILE_TEST, description = "Compile the tests", group = GROUP_BUILD, dependsOn = arrayOf(TASK_COMPILE)) fun taskCompileTest(project: Project): IncrementalTaskInfo { - sourceTestDirectories.addAll(context.variant.sourceDirectories(project, context, SourceSet.of(isTest = true))) return IncrementalTaskInfo( inputChecksum = { Md5.toMd5Directories(context.testSourceDirectories(project).map { File(project.directory, it.path)}) @@ -159,19 +125,12 @@ open class JvmCompilerPlugin @Inject constructor( ) } + 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 { - // Generate the BuildConfig before invoking sourceDirectories() since that call - // might add the buildConfig source directories - val sourceDirectory = context.variant.maybeGenerateBuildConfig(project, context) - if (sourceDirectory != null) { - sourceDirectories.add(sourceDirectory) - } - - // Set up the source files now that we have the variant - sourceDirectories.addAll(context.variant.sourceDirectories(project, context, SourceSet.of(isTest = false))) - return IncrementalTaskInfo( inputChecksum = { Md5.toMd5Directories(context.sourceDirectories(project).map { File(project.directory, it.path) }) @@ -192,44 +151,65 @@ open class JvmCompilerPlugin @Inject constructor( val results = arrayListOf() val compilerContributors = context.pluginInfo.compilerContributors - ActorUtils.selectAffinityActors(project, context, - 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() - allCompilers.forEach { compiler -> - val contributedSourceDirs = - if (isTest) { - context.testSourceDirectories(project) - } else { - context.sourceDirectories(project) + + /** + * 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) } - val sourceFiles = KFiles.findSourceFiles(project.directory, - contributedSourceDirs.map { it.path }, compiler.sourceSuffixes) - if (sourceFiles.size > 0) { - // TODO: createCompilerActionInfo recalculates the source files, only compute them - // once and pass them - val info = createCompilerActionInfo(project, context, compiler, isTest, sourceDirectories, - sourceSuffixes = compiler.sourceSuffixes) - val thisResult = compiler.compile(project, context, info) - results.add(thisResult) - if (!thisResult.success && failedResult == null) { - failedResult = thisResult - } - } else { - log(2, "Compiler $compiler not running on ${project.name} since no source files were found") } } + return if (failedResult != null) failedResult!! else if (results.size > 0) results[0] else TaskResult(true) } } - val allProjects = arrayListOf() + private val allProjects = arrayListOf() // IProjectContributor override fun projects() = allProjects @@ -239,27 +219,27 @@ open class JvmCompilerPlugin @Inject constructor( } fun addDependentProjects(project: Project, dependents: List) { - project.projectExtra.dependsOn.addAll(dependents) + project.dependsOn.addAll(dependents) with(ProjectDescription(project, dependents)) { allProjects.add(this) } - - project.projectProperties.put(DEPENDENT_PROJECTS, allProjects) } - @Task(name = "doc", description = "Generate the documentation for the project", group = GROUP_DOCUMENTATION) + @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, createCompilerActionInfo(project, context, - compiler, - isTest = false, sourceDirectories = sourceDirectories, - sourceSuffixes = compiler.sourceSuffixes)) + result = docGenerator.generateDoc(project, context, + compilerUtils.createCompilerActionInfo(project, context, compiler, + isTest = false, sourceDirectories = sourceDirectories(project, context, false), + sourceSuffixes = compiler.sourceSuffixes, buildDirectory = buildDirectory)) } } return result!! @@ -269,110 +249,10 @@ open class JvmCompilerPlugin @Inject constructor( } } - /** - * 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. - */ - private fun isDependencyExcluded(id: IClasspathDependency, excluded: List) - = excluded.any { id.id.startsWith(it.id) } - - /** - * 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. - */ - protected fun createCompilerActionInfo(project: Project, context: KobaltContext, compiler: ICompiler, - isTest: Boolean, sourceDirectories: List, sourceSuffixes: List): CompilerActionInfo { - copyResources(project, SourceSet.of(isTest)) - - val fullClasspath = if (isTest) dependencyManager.testDependencies(project, context) - else dependencyManager.dependencies(project, context) - - // Remove all the excluded dependencies from the classpath - val classpath = fullClasspath.filter { - ! isDependencyExcluded(it, project.excludedDependencies) - } - - val buildDirectory = if (isTest) File(project.buildDirectory, KFiles.TEST_CLASSES_DIR) - else File(project.classesDir(context)) - buildDirectory.mkdirs() - - - 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) - }.distinct() - - // Now that we have all the source directories, find all the source files in them - 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() - if (sourceSuffixes.any { it.contains("kt")}) { - project.sourceDirectories.forEach { - val javaDir = KFiles.joinDir(project.directory, it) - if (File(javaDir).exists()) { - if (it.contains("java")) { - extraSourceFiles.add(javaDir) - // Add all the source directories contributed as potential Java directories too - // (except our own) - context.pluginInfo.sourceDirContributors.filter { it != this }.forEach { - extraSourceFiles.addAll(it.sourceDirectoriesFor(project, context).map { it.path }) - } - - } - } - } - } - - val allSources = (sourceFiles + extraSourceFiles).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 */) - val result = context.pluginInfo.compilerInterceptors.fold(initialActionInfo, { ai, interceptor -> - interceptor.intercept(project, context, ai) - }) - return result - } - - val sourceDirectories = arrayListOf() - val sourceTestDirectories = arrayListOf() - // ISourceDirectoryContributor override fun sourceDirectoriesFor(project: Project, context: KobaltContext) = if (accept(project)) { - sourceDirectories.toList() + sourceDirectories(project, context, isTest = false) } else { arrayListOf() } 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 index b062dc62..d8ca3555 100644 --- 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 @@ -2,7 +2,7 @@ package com.beust.kobalt.internal import com.beust.kobalt.KobaltException import com.beust.kobalt.api.* -import com.beust.kobalt.misc.log +import com.beust.kobalt.misc.kobaltLog import java.io.ByteArrayInputStream import java.io.InputStream import javax.xml.bind.JAXBContext @@ -50,6 +50,9 @@ class ClassNameXml { var className: List = arrayListOf() } +/** + * The fields in this interface have tests. + */ interface IPluginInfo { val testJvmFlagContributors : List val testJvmFlagInterceptors : List @@ -69,13 +72,13 @@ class PluginInfo(val xml: KobaltPluginXml, val pluginClassLoader: ClassLoader?, val plugins = arrayListOf() val projectContributors = arrayListOf() val classpathContributors = arrayListOf() - val initContributors = arrayListOf() + val templateContributors = arrayListOf() val repoContributors = arrayListOf() val compilerFlagContributors = arrayListOf() val compilerInterceptors = arrayListOf() - val sourceDirectoriesInterceptors = arrayListOf() - val buildDirectoryInterceptors = arrayListOf() - val runnerContributors = arrayListOf() + val sourceDirectoriesInterceptors = arrayListOf() + val buildDirectoryInterceptors = arrayListOf() +// val runnerContributors = arrayListOf() val testRunnerContributors = arrayListOf() val classpathInterceptors = arrayListOf() val compilerContributors = arrayListOf() @@ -86,10 +89,16 @@ class PluginInfo(val xml: KobaltPluginXml, val pluginClassLoader: ClassLoader?, 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 @@ -115,7 +124,7 @@ class PluginInfo(val xml: KobaltPluginXml, val pluginClassLoader: ClassLoader?, 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) - log(2, "URL for core kobalt-plugin.xml: $url") + kobaltLog(2, "URL for core kobalt-plugin.xml: $url") if (url != null) { return readPluginXml(url.openConnection().inputStream) } else { @@ -131,7 +140,7 @@ class PluginInfo(val xml: KobaltPluginXml, val pluginClassLoader: ClassLoader?, val jaxbContext = JAXBContext.newInstance(KobaltPluginXml::class.java) val kobaltPlugin: KobaltPluginXml = jaxbContext.createUnmarshaller().unmarshal(ins) as KobaltPluginXml - log(2, "Parsed plugin XML file, found: " + kobaltPlugin.name) + kobaltLog(2, "Parsed plugin XML file, found: " + kobaltPlugin.name) val result = try { PluginInfo(kobaltPlugin, pluginClassLoader, classLoader) @@ -153,10 +162,21 @@ class PluginInfo(val xml: KobaltPluginXml, val pluginClassLoader: ClassLoader?, GuiceFactory() } - fun forName(className: String) = - if (pluginClassLoader != null) pluginClassLoader.loadClass(className) - else if (classLoader != null) classLoader.loadClass(className) - else Class.forName(className) + 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 @@ -166,20 +186,20 @@ class PluginInfo(val xml: KobaltPluginXml, val pluginClassLoader: ClassLoader?, 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 IBuildDirectoryIncerceptor) buildDirectoryInterceptors.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) initContributors.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 IRunnerContributor) runnerContributors.add(this) if (this is ISourceDirectoryContributor) sourceDirContributors.add(this) - if (this is ISourceDirectoryIncerceptor) sourceDirectoriesInterceptors.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) @@ -187,27 +207,33 @@ class PluginInfo(val xml: KobaltPluginXml, val pluginClassLoader: ClassLoader?, 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, initContributors, + listOf(projectContributors, classpathContributors, templateContributors, repoContributors, compilerFlagContributors, compilerInterceptors, sourceDirectoriesInterceptors, buildDirectoryInterceptors, - runnerContributors, testRunnerContributors, classpathInterceptors, + /* runnerContributors, */ testRunnerContributors, classpathInterceptors, compilerContributors, docContributors, sourceDirContributors, testSourceDirContributors, buildConfigFieldContributors, - taskContributors, assemblyContributors, - incrementalAssemblyContributors, testJvmFlagInterceptors + taskContributors, incrementalTaskContributors, assemblyContributors, + incrementalAssemblyContributors, testJvmFlagInterceptors, + jvmFlagContributors, localMavenRepoPathInterceptors, buildListeners, + buildReportContributors, docFlagContributors ).forEach { - it.forEach { - it.cleanUpActors() - } + it.forEach(IPluginActor::cleanUpActors) } } @@ -215,18 +241,18 @@ class PluginInfo(val xml: KobaltPluginXml, val pluginClassLoader: ClassLoader?, * Add the content of @param[pluginInfo] to this pluginInfo. */ fun addPluginInfo(pluginInfo: PluginInfo) { - log(2, "Found new plug-in, adding it to 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) - initContributors.addAll(pluginInfo.initContributors) + 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) +// runnerContributors.addAll(pluginInfo.runnerContributors) testRunnerContributors.addAll(pluginInfo.testRunnerContributors) classpathInterceptors.addAll(pluginInfo.classpathInterceptors) compilerContributors.addAll(pluginInfo.compilerContributors) @@ -234,6 +260,7 @@ class PluginInfo(val xml: KobaltPluginXml, val pluginClassLoader: ClassLoader?, 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) @@ -241,6 +268,11 @@ class PluginInfo(val xml: KobaltPluginXml, val pluginClassLoader: ClassLoader?, 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 index 7bc02011..2eb5374c 100644 --- 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 @@ -1,9 +1,11 @@ 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.log +import com.beust.kobalt.misc.kobaltLog import com.google.inject.Inject import com.google.inject.Singleton import java.io.File @@ -15,22 +17,39 @@ import javax.xml.bind.annotation.XmlRootElement /** * The root element of kobalt-settings.xml */ -@XmlRootElement(name = "kobalt-settings") +@XmlRootElement(name = "kobaltSettings") class KobaltSettingsXml { - @XmlElement(name = "local-repo") @JvmField - var localRepo: String = homeDir(KFiles.KOBALT_DOT_DIR, "repository") + @XmlElement(name = "localCache") @JvmField + var localCache: String = homeDir(KFiles.KOBALT_DOT_DIR, "cache") - @XmlElement(name = "default-repos") @JvmField + @XmlElement(name = "localMavenRepo") @JvmField + var localMavenRepo: String = homeDir(KFiles.KOBALT_DOT_DIR, "localMavenRepo") + + @XmlElement(name = "defaultRepos") @JvmField var defaultRepos: DefaultReposXml? = null - @XmlElement(name = "proxy") @JvmField - var proxy: ProxyXml? = null + @XmlElement(name = "proxies") @JvmField + var proxies: ProxiesXml? = null - @XmlElement(name = "kobalt-compiler-version") @JvmField - var kobaltCompilerVersion: String = "1.0.0" + @XmlElement(name = "kobaltCompilerVersion") @JvmField + var kobaltCompilerVersion: String = Constants.KOTLIN_COMPILER_VERSION - @XmlElement(name = "kobalt-compiler-repo") @JvmField + @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 { @@ -42,6 +61,9 @@ class ProxyXml { @XmlElement @JvmField var type: String = "" + + @XmlElement @JvmField + var nonProxyHosts: String = "" } class DefaultReposXml { @@ -49,19 +71,37 @@ class DefaultReposXml { 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 local repo. + * Location of the cache repository. */ - var localRepo = KFiles.makeDir(xmlFile.localRepo) // var for testing + 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 proxyConfig = with(xmlFile.proxy) { + 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) { @@ -69,12 +109,40 @@ class KobaltSettings @Inject constructor(val xmlFile: KobaltSettingsXml) { } if (this != null) { - ProxyConfig(host, toIntOr(port, 0), type) - } else null + map { proxyXml-> + ProxyConfig(proxyXml.host, toIntOr(proxyXml.port, 0), proxyXml.type, proxyXml.nonProxyHosts) + } + } else { + null + } } - var kobaltCompilerVersion = xmlFile.kobaltCompilerVersion - var kobaltCompilerRepo = xmlFile.kobaltCompilerRepo + 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") @@ -90,7 +158,7 @@ class KobaltSettings @Inject constructor(val xmlFile: KobaltSettingsXml) { return result } } else { - log(2, "Couldn't find ${KobaltSettings.SETTINGS_FILE_PATH}, using default settings") + 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/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 index dccf9290..df865cec 100644 --- 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 @@ -12,7 +12,7 @@ import com.beust.kobalt.api.Project interface IBuildConfig { /** * If at least one build config was found either on the project or the variant, this function - * will be used to generate the BuildConfig file with the correct language. + * 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 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/SpekRunner.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/SpekRunner.kt index 2129a315..0c1ddad6 100644 --- 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 @@ -6,5 +6,6 @@ package com.beust.kobalt.internal */ 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 index bd117f8b..99aa7624 100644 --- 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 @@ -4,14 +4,10 @@ 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.Strings -import com.beust.kobalt.misc.benchmarkMillis -import com.beust.kobalt.misc.kobaltError -import com.beust.kobalt.misc.log -import com.google.common.annotations.VisibleForTesting +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.Multimap import com.google.common.collect.TreeMultimap import java.lang.reflect.Method import java.util.* @@ -20,7 +16,8 @@ import javax.inject.Singleton @Singleton class TaskManager @Inject constructor(val args: Args, - val incrementalManagerFactory: IncrementalManager.IFactory) { + val incrementalManagerFactory: IncrementalManager.IFactory, + val kobaltLog: ParallelLogger) { private val dependsOn = TreeMultimap.create() private val reverseDependsOn = TreeMultimap.create() private val runBefore = TreeMultimap.create() @@ -45,7 +42,7 @@ class TaskManager @Inject constructor(val args: Args, /** * Ordering: task2 runs after task 1. */ - fun runAfter(task1: String, task2: String) = runAfter.put(task2, task1) + fun runAfter(task1: String, task2: String) = runAfter.put(task1, task2) /** * Wrapper task: task2 runs after task 1. @@ -56,14 +53,16 @@ class TaskManager @Inject constructor(val args: Args, constructor(project: String, task: String) : this(project + ":" + task) val project: String? - get() = if (id.contains(":")) id.split(":")[0] else null + get() = if (id.contains(':')) id.split(':')[0] else null val taskName: String - get() = if (id.contains(":")) id.split(":")[1] else id + get() = if (id.contains(':')) id.split(':')[1] else id fun matches(projectName: String) = project == null || project == projectName + + override fun toString() = id } - class RunTargetResult(val exitCode: Int, val messages: List) + class RunTargetResult(val taskResult: TaskResult, val timings: List) /** * @return the list of tasks available for the given project. @@ -78,348 +77,100 @@ class TaskManager @Inject constructor(val args: Args, }.forEach { put(it.name, it) } - dynamicTasks.filter { - it.plugin.accept(project) - }.forEach { - put(it.name, it) - } } } - fun runTargets(taskNames: List, projects: List): RunTargetResult { - var result = 0 - val failedProjects = hashSetOf() - val messages = Collections.synchronizedList(arrayListOf()) - projects.forEach { project -> - AsciiArt.logBox("Building ${project.name}") +// @Inject +// lateinit var pluginInfo: PluginInfo - // Does the current project depend on any failed projects? - val fp = project.projectExtra.dependsOn.filter { - failedProjects.contains(it.name) - }.map { - it.name - } - - if (fp.size > 0) { - log(2, "Marking project ${project.name} as skipped") - failedProjects.add(project.name) - kobaltError("Not building project ${project.name} since it depends on failed " - + Strings.pluralize(fp.size, "project") - + " " + fp.joinToString(",")) - } else { - // 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) - - log(3, "Tasks:") - tasksByNames.keys().forEach { - log(3, " $it: " + tasksByNames.get(it)) - } - - val graph = createGraph2(project.name, taskNames, tasksByNames, - dependsOn, reverseDependsOn, runBefore, runAfter, alwaysRunAfter, - { task: ITask -> task.name }, - { task: ITask -> task.plugin.accept(project) }) - - // - // Now that we have a full graph, run it - // - log(2, "About to run graph:\n ${graph.dump()} ") - - val factory = object : IThreadWorkerFactory { - override fun createWorkers(nodes: Collection) - = nodes.map { TaskWorker(listOf(it), args.dryRun, messages) } - } - - val executor = DynamicGraphExecutor(graph, factory) - val thisResult = executor.run() - if (thisResult != 0) { - log(2, "Marking project ${project.name} as failed") - failedProjects.add(project.name) - } - if (result == 0) { - result = thisResult - } - } - } - return RunTargetResult(result, messages) - } - - val LOG_LEVEL = 3 - - /** - * 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 createGraph2(projectName: String, taskNames: List, nodeMap: Multimap, - dependsOn: Multimap, - reverseDependsOn: Multimap, - runBefore: Multimap, - runAfter: Multimap, - alwaysRunAfter: Multimap, - toName: (T) -> String, - accept: (T) -> Boolean): - DynamicGraph { - - 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) - } - } - } - - // - // Turn the task names into the more useful TaskInfo and do some sanity checking on the way - // - val taskInfos = taskNames.map { TaskInfo(it) }.filter { - if (!nodeMap.keys().contains(it.taskName)) { + 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") - } - 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) + val pluginInfo = Kobalt.INJECTOR.getInstance(PluginInfo::class.java) + var taskInfos = calculateDependentTaskNames(passedTaskNames, allProjects) - while (toProcess.size > 0) { + // Remove non existing tasks (e.g. dynamic task defined for a single project) + taskInfos = taskInfos.filter { hasTask(it) } - /** - * 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 -> - val tn = toName(t) - log(LOG_LEVEL, " Adding edge ($text) $f -> $t") - result.addEdge(f, t) - newToProcess.add(t) - } - } + 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) } - - /** - * Whenever a task is added to the graph, we also add its alwaysRunAfter tasks. - */ - fun processAlways(always: Multimap, node: T) { - log(LOG_LEVEL, " Processing always for $node") - always[toName(node)]?.let { to: Collection -> - to.forEach { t -> - nodeMap[t].forEach { from -> - log(LOG_LEVEL, " Adding always edge $from -> $node") - result.addEdge(from, node) - } - } - log(LOG_LEVEL, " ... done processing always for $node") - } - } - - log(LOG_LEVEL, " 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 - log(LOG_LEVEL, " ***** 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") - } - - // - // runBefore and runAfter (task ordering) are only considered for explicit tasks (tasks that were - // explicitly requested by the user) - // - runBefore[taskName].forEach { from -> - if (taskNames.contains(from)) { - addEdge(result, from, taskName, newToProcess, "runBefore") - } - } - runAfter[taskName].forEach { to -> - if (taskNames.contains(to)) { - addEdge(result, taskName, to, newToProcess, "runAfter") - } - } - seen.add(taskName) - } - - newToProcess.forEach { processAlways(always, it) } - - toProcess.clear() - toProcess.addAll(newToProcess.filter { ! seen.contains(toName(it))}.map { TaskInfo(toName(it)) }) - newToProcess.clear() - } - return result + return projectRunner.runProjects(taskInfos, projectsToRun) } -// private fun reverseMultimap(mm: Multimap) : Multimap { -// val result = TreeMultimap.create() -// mm.keySet().forEach { key -> -// mm[key].forEach { value -> -// result.put(value, key) -// } -// } -// return result -// } + /** + * 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) /** - * Create a dynamic graph representing all the tasks that need to be run. + * 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). */ -// @VisibleForTesting -// fun createGraph(projectName: String, taskNames: List, nodeMap: Multimap, -// dependsOn: Multimap, -// reverseDependsOn: Multimap, -// runBefore: Multimap, -// runAfter: Multimap, -// toName: (T) -> String, -// accept: (T) -> Boolean): -// DynamicGraph { -// -// val result = DynamicGraph() -// taskNames.forEach { fullTaskName -> -// val ti = TaskInfo(fullTaskName) -// if (!nodeMap.keys().contains(ti.taskName)) { -// throw KobaltException("Unknown task: $fullTaskName") -// } -// -// if (ti.matches(projectName)) { -// val tiTaskName = ti.taskName -// nodeMap[tiTaskName].forEach { task -> -// if (task != null && accept(task)) { -// val toProcess = arrayListOf(task) -// val newToProcess = hashSetOf() -// -// fun maybeAddEdge(task: T, mm: Multimap, -// seen: Set, -// isDependency: Boolean, -// reverseEdges: Boolean = false): Boolean { -// var added = false -// val taskName = toName(task) -// mm[taskName]?.forEach { toName -> -// val addEdge = isDependency || (!isDependency && taskNames.contains(toName)) -// if (addEdge) { -// nodeMap[toName].forEach { to -> -// if (reverseEdges) { -// log(3, " Adding reverse edge \"$to\" -> \"$task\" it=\"$toName\"") -// added = true -// result.addEdge(to, task) -// } else { -// log(3, " Adding edge \"$task\" -> \"$to\"") -// added = true -// result.addEdge(task, to) -// } -// if (!seen.contains(toName(to))) { -// log(3, " New node to process: \"$to\"") -// newToProcess.add(to) -// } else { -// log(3, " Already seen: $to") -// } -// } -// } -// } -// return added -// } -// -// // These two maps indicate reversed dependencies so we want to have -// // a reverse map for them so we consider all the cases. For example, -// // if we are looking at task "a", we want to find all the relationships -// // that depend on "a" and also all the relationships that "a" depends on -// val invertedReverseDependsOn = reverseMultimap(reverseDependsOn) -// val invertedRunBefore = reverseMultimap(runBefore) -// val invertedDependsOn = reverseMultimap(dependsOn) -// val invertedRunAfter = reverseMultimap(runAfter) -// -// val seen = hashSetOf() -// while (toProcess.size > 0) { -// log(3, " New batch of nodes to process: $toProcess") -// toProcess.filter { !seen.contains(toName(it)) }.forEach { current -> -// result.addNode(current) -// -// if (! maybeAddEdge(current, invertedDependsOn, seen, true, true)) { -// maybeAddEdge(current, dependsOn, seen, true, false) -// } -// if (! maybeAddEdge(current, invertedRunAfter, seen, false, true)) { -// maybeAddEdge(current, runAfter, seen, false, false) -// } -// if (! maybeAddEdge(current, reverseDependsOn, seen, true, true)) { -// maybeAddEdge(current, invertedReverseDependsOn, seen, true, false) -// } -// if (! maybeAddEdge(current, runBefore, seen, false, true)) { -// maybeAddEdge(current, invertedRunBefore, seen, false, false) -// } -// -// seen.add(toName(current)) -// -// newToProcess.addAll(dependsOn[toName(current)].flatMap { nodeMap[it] }) -// } -// toProcess.clear() -// toProcess.addAll(newToProcess) -// newToProcess.clear() -// } -// } -// } -// } else { -// log(3, "Task $fullTaskName does not match the current project $projectName, skipping it") -// } -// } -// return result -// } + fun calculateDependentTaskNames(taskNames: List, projects: List): List { + return taskNames.flatMap { calculateDependentTaskNames(it, projects) } + } - /** - * Find the free tasks of the graph. - */ -// private fun calculateFreeTasks(tasksByNames: Multimap, runBefore: TreeMultimap, -// reverseAfter: HashMap) -// : Collection { -// val freeTaskMap = hashMapOf() -// tasksByNames.keys().forEach { -// if (! runBefore.containsKey(it) && ! reverseAfter.containsKey(it)) { -// tasksByNames[it].forEach { t -> -// freeTaskMap.put(it, t) -// } -// } -// } -// -// return freeTaskMap.values -// } + 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 @@ -445,6 +196,7 @@ class TaskManager @Inject constructor(val args: Args, = 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 }) @@ -457,7 +209,7 @@ class TaskManager @Inject constructor(val args: Args, 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() @@ -476,16 +228,14 @@ class TaskManager @Inject constructor(val args: Args, */ fun computePluginTasks(projects: List) { installAnnotationTasks(projects) - installDynamicTasks(projects) + installDynamicTasks() } - private fun installDynamicTasks(projects: List) { + private fun installDynamicTasks() { dynamicTasks.forEach { task -> - projects.filter { task.plugin.accept(it) }.forEach { project -> - addTask(task.plugin, project, task.name, task.doc, task.group, - task.dependsOn, task.reverseDependsOn, task.runBefore, task.runAfter, task.alwaysRunAfter, - task.closure) - } + addTask(task.plugin, task.project, task.name, task.doc, task.group, + task.dependsOn, task.reverseDependsOn, task.runBefore, task.runAfter, task.alwaysRunAfter, + task.closure) } } @@ -494,7 +244,7 @@ class TaskManager @Inject constructor(val args: Args, val method = staticTask.method val methodName = method.declaringClass.toString() + "." + method.name - log(3, " Found task:${staticTask.name} method: $methodName") + kobaltLog(3, " Found task:${staticTask.name} method: $methodName") val plugin = staticTask.plugin projects.filter { plugin.accept(it) }.forEach { project -> @@ -522,7 +272,8 @@ class TaskManager @Inject constructor(val args: Args, object : BasePluginTask(plugin, name, description, group, project) { override fun call(): TaskResult2 { val taskResult = task(project) - return TaskResult2(taskResult.success, taskResult.errorMessage, this) + return TaskResult2(taskResult.success, errorMessage = taskResult.errorMessage, value = this, + testResult = taskResult.testResult) } }) dependsOn.forEach { dependsOn(it, name) } @@ -532,6 +283,12 @@ class TaskManager @Inject constructor(val args: Args, 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. */ @@ -546,30 +303,32 @@ class TaskManager @Inject constructor(val args: Args, ///// } -class TaskWorker(val tasks: List, val dryRun: Boolean, val messages: MutableList) - : IWorker { +class TaskWorker(val tasks: List, val dryRun: Boolean, val pluginInfo: PluginInfo) : IWorker { override fun call() : TaskResult2 { if (tasks.size > 0) { tasks[0].let { - log(1, AsciiArt.taskColor(AsciiArt.horizontalSingleLine + " ${it.project.name}:${it.name}")) + 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 - val time = benchmarkMillis { - val tr = if (dryRun) TaskResult() else it.call() - success = success and tr.success - if (tr.errorMessage != null) errorMessages.add(tr.errorMessage) + 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) } - messages.add("$name: $time ms") } - return TaskResult2(success, errorMessages.joinToString("\n"), tasks[0]) + 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 index 958bfc99..f4ee96f8 100644 --- 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 @@ -1,25 +1,53 @@ 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.misc.KFiles -import com.beust.kobalt.misc.warn +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" - fun defaultOutput(project: Project) = KFiles.joinDir(project.buildDirectory, "test-output") + 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, classpath: List, testConfig: TestConfig) - = arrayListOf().apply { - var addOutput = true - testConfig.testArgs.forEach { arg -> - if (arg == "-d") addOutput = false + 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) { @@ -28,26 +56,213 @@ class TestNgRunner : GenericTestRunner() { if (testngXml.exists()) { add(testngXml.absolutePath) } else { - val testClasses = findTestClasses(project, testConfig) - if (testClasses.size > 0) { - if (addOutput) { - add("-d") - add(defaultOutput(project)) - } + val testClasses = findTestClasses(project, context, testConfig) + if (testClasses.isNotEmpty()) { addAll(testConfig.testArgs) add("-testclass") add(testClasses.joinToString(",")) } else { - warn("Couldn't find any test classes for ${project.name}") + 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 { - if (addOutput) { - add("-d") - add(defaultOutput(project)) - } 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 index f23b1716..a2eb91e9 100644 --- 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 @@ -1,11 +1,8 @@ package com.beust.kobalt.internal.build -import com.beust.kobalt.misc.KFiles -import com.beust.kobalt.misc.log import java.io.File import java.nio.file.Files import java.nio.file.Path -import java.nio.file.attribute.BasicFileAttributes /** * 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. @@ -13,24 +10,5 @@ import java.nio.file.attribute.BasicFileAttributes class BuildFile(val path: Path, val name: String, val realPath: Path = path) { fun exists() : Boolean = Files.exists(path) - val lastModified : Long - get() = Files.readAttributes(realPath, BasicFileAttributes::class.java).lastModifiedTime().toMillis() - val directory : File get() = path.toFile().parentFile - - /** - * @return the .kobalt directory where this build file will be compiled. - */ - val dotKobaltDir: File get() = File(directory.parentFile.parentFile, KFiles.KOBALT_DOT_DIR).apply { - mkdirs() - } - - /** - * @return the absolute directory of this project's location, assuming the build file is in - * $project/kobalt/src/Build.kt. - */ - val absoluteDir : File? get() { - log(1, "Current path: $path") - return path.parent?.parent?.parent?.toFile() - } } 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/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/GetDependenciesCommand.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/remote/GetDependenciesCommand.kt deleted file mode 100644 index 7422c493..00000000 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/remote/GetDependenciesCommand.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.beust.kobalt.internal.remote - -import com.beust.kobalt.Constants -import java.io.PrintWriter -import java.net.Socket - -fun main(argv: Array) { - Socket("localhost", 1234).use { socket -> - (PrintWriter(socket.outputStream, true)).use { out -> - out.println("""{ "name" : "getDependencies", "buildFile": - "/Users/beust/kotlin/kobalt/kobalt/src/${Constants.BUILD_FILE_NAME}"}""") - } - } -} \ No newline at end of file diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/kotlin/ParentLastClassLoader.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/kotlin/ParentLastClassLoader.kt index 10269d52..6d5fa9d2 100644 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/kotlin/ParentLastClassLoader.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/kotlin/ParentLastClassLoader.kt @@ -9,7 +9,7 @@ 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 @@ -22,7 +22,7 @@ public class ParentLastClassLoader(val classpath: List) * 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/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/CompletedFuture.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/CompletedFuture.kt index f64c32b5..1e24cd8e 100644 --- a/modules/kobalt-plugin-api/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 index 6085ad61..ff2d9fd9 100644 --- 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 @@ -1,45 +1,48 @@ package com.beust.kobalt.maven +import com.beust.kobalt.KobaltException import com.beust.kobalt.api.* -import com.beust.kobalt.maven.aether.ConsoleRepositoryListener -import com.beust.kobalt.maven.aether.KobaltAether +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.beust.kobalt.misc.log -import com.beust.kobalt.misc.warn 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 aether: KobaltAether) - : IDependencyManager { +class DependencyManager @Inject constructor(val executors: KobaltExecutors, + val resolver: KobaltMavenResolver) : IDependencyManager { companion object { - fun create(id: String, project: Project? = null) = - Kobalt.INJECTOR.getInstance(DependencyManager::class.java).create(id, project) + 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, project: Project?) : IClasspathDependency { + override fun create(id: String, optional: Boolean, projectDirectory: String?) : IClasspathDependency { if (id.startsWith(FileDependency.PREFIX_FILE)) { - val path = if (project?.directory != null) { + 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(project!!.directory), Kobalt.context?.internalContext?.absoluteDir).map { + val result = listOf(File(projectDirectory), Kobalt.context?.internalContext?.absoluteDir).map { File(it, idPath) - }.first { + }.firstOrNull { it.exists() } - result + result ?: throw KobaltException("Couldn't find $id") + } else { File(idPath) } @@ -50,14 +53,14 @@ class DependencyManager @Inject constructor(val executors: KobaltExecutors, val } 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) + return createMaven(MavenId.create(id).toId, optional) } } /** * Create an IClasspathDependency from a Maven id. */ - override fun createMaven(id: String) : IClasspathDependency = aether.create(id) + override fun createMaven(id: String, optional: Boolean) : IClasspathDependency = resolver.create(id, optional) /** * Create an IClasspathDependency from a path. @@ -67,12 +70,14 @@ class DependencyManager @Inject constructor(val executors: KobaltExecutors, val /** * @return the source dependencies for this project, including the contributors. */ - override fun dependencies(project: Project, context: KobaltContext) = dependencies(project, context, false) + 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) = dependencies(project, context, true) + override fun testDependencies(project: Project, context: KobaltContext) + = privateDependencies(project, context, listOf(Scope.COMPILE, Scope.TEST)) /** * Transitive dependencies for the compilation of this project. @@ -82,20 +87,82 @@ class DependencyManager @Inject constructor(val executors: KobaltExecutors, val // project.compileDependencies + project.compileRuntimeDependencies) /** - * @return the classpath for this project, including the IClasspathContributors. - * allDependencies is typically either compileDependencies or testDependencies + * @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, - dependentProjects: List, - vararg allDependencies: List): List { - var result = arrayListOf() + 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)) + result.addAll(transitiveClosure(dependencies, dependencyFilter, project?.name)) } result.addAll(runClasspathContributors(project, context)) - result.addAll(dependentProjectDependencies(dependentProjects, project, context)) + result.addAll(dependentProjectDependencies(project, context, dependencyFilter, scopes)) - return result + /** + * 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) : @@ -111,39 +178,19 @@ class DependencyManager @Inject constructor(val executors: KobaltExecutors, val * 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, indent : String = " "): - List { - var executor = executors.newExecutor("JvmCompiler}", 10) - - var result = hashSetOf() - - dependencies.forEach { projectDependency -> - log(ConsoleRepositoryListener.LOG_LEVEL, "$indent Resolving $projectDependency") - result.add(projectDependency) - projectDependency.id.let { - result.add(create(it)) - val downloaded = transitiveClosure(projectDependency.directDependencies(), indent + " ") - - result.addAll(downloaded) + 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) - - val nonexistent = reordered.filter{ ! it.jarFile.get().exists() } - if (nonexistent.any()) { - warn("Nonexistent dependencies: $nonexistent") - } - - val result2 = reordered.filter { - // Only keep existent files (nonexistent files are probably optional dependencies or parent poms - // that point to other poms but don't have a jar file themselves) - it.jarFile.get().exists() - } - - executor.shutdown() - - return result2 + return reordered } /** @@ -170,44 +217,87 @@ class DependencyManager @Inject constructor(val executors: KobaltExecutors, val * If this project depends on other projects, we need to include their jar file and also * their own dependencies */ - private fun dependentProjectDependencies(projectDescriptions: List, - project: Project?, context: KobaltContext) : List { - val result = arrayListOf() - projectDescriptions.filter { - it.project.name == project?.name - }.forEach { pd -> - pd.dependsOn.forEach { p -> - result.add(FileDependency(KFiles.joinDir(p.directory, p.classesDir(context)))) - val otherDependencies = calculateDependencies(p, context, projectDescriptions, - p.compileDependencies) + 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) } - } - return result + if (isTest) { + project.testsDependOn.forEach { p -> + val otherDependencies = calculateDependencies(p, context, dependencyFilter, scopes) + result.addAll(otherDependencies) + } + } + return result + } } - private fun dependencies(project: Project, context: KobaltContext, isTest: Boolean) + private fun privateDependencies(project: Project, context: KobaltContext, passedScopes: List) : List { - val result = arrayListOf() - val projects = listOf(ProjectDescription(project, project.projectExtra.dependsOn)) + val isTest = passedScopes.contains(Scope.TEST) + val transitive = hashSetOf() with(project) { - val deps = arrayListOf(compileDependencies, compileProvidedDependencies) - if (isTest) { - deps.add(testDependencies) - deps.add(testProvidedDependencies) - } - deps.forEach { - result.addAll(calculateDependencies(project, context, projects, it)) + 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 result2 = listOf(FileDependency(KFiles.makeOutputDir(project).absolutePath), - FileDependency(KFiles.makeOutputTestDir(project).absolutePath)) + - reorderDependencies(result) - return result2 + 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/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Gpg.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Gpg.kt index 7078abb9..60a4335c 100644 --- a/modules/kobalt-plugin-api/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? { @@ -42,12 +44,27 @@ public class Gpg { 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/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Http.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Http.kt index 72feefc3..df286a47 100644 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Http.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Http.kt @@ -3,6 +3,7 @@ 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.google.inject.Inject import okhttp3.* @@ -21,7 +22,7 @@ class Http @Inject constructor(val settings:KobaltSettings) { } fun get(user: String?, password: String?, url: String) : Response { - val client = OkHttpClient.Builder().proxy(settings.proxyConfig?.toProxy()).build() + 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)) @@ -41,7 +42,7 @@ class Http @Inject constructor(val settings:KobaltSettings) { 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) } } @@ -74,8 +75,8 @@ class Http @Inject constructor(val settings:KobaltSettings) { requestBuilder.put(CountingFileRequestBody(file.file, file.mimeType, progressCallback))) .build() - log(2, "Uploading $file to $url") - val response = OkHttpClient.Builder().proxy(settings.proxyConfig?.toProxy()).build().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/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Kurl.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Kurl.kt index 6b9c0a62..909c18c5 100644 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Kurl.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Kurl.kt @@ -1,9 +1,8 @@ package com.beust.kobalt.maven import com.beust.kobalt.HostConfig -import com.beust.kobalt.KobaltException +import com.beust.kobalt.maven.aether.KobaltMavenResolver import com.beust.kobalt.maven.dependency.FileDependency -import com.beust.kobalt.misc.LocalProperties import java.io.* import java.net.HttpURLConnection import java.net.URL @@ -21,27 +20,7 @@ class Kurl(val hostInfo: HostConfig) { } init { - // 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 == "$KEY.$host.$VALUE_USER") { - hostInfo.username = properties.getProperty(key) - } else if (key == "$KEY.$host.$VALUE_PASSWORD") { - hostInfo.password = properties.getProperty(key) - } - } - fun error(s1: String, s2: String) { - throw KobaltException("Found \"$s1\" but not \"$s2\" in local.properties for $KEY.$host", - docUrl = "http://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") - } + KobaltMavenResolver.initAuthentication(hostInfo) } override fun toString() = hostInfo.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 index baebac31..53cb1c89 100644 --- 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 @@ -1,6 +1,6 @@ package com.beust.kobalt.maven -open public class LocalDep(override val mavenId: MavenId, open val localRepo: LocalRepo) +open class LocalDep(override val mavenId: MavenId, open val localRepo: LocalRepo) : SimpleDep(mavenId) { fun toAbsoluteJarFilePath(v: String) = localRepo.toFullPath(toJarFile(v)) 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 index 5077d5b9..35f8c50a 100644 --- 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 @@ -1,54 +1,16 @@ package com.beust.kobalt.maven import com.beust.kobalt.internal.KobaltSettings -import com.beust.kobalt.misc.KFiles -import com.beust.kobalt.misc.Versions import com.google.inject.Inject import java.io.File -import java.util.* import javax.inject.Singleton @Singleton open class LocalRepo @Inject constructor(val kobaltSettings: KobaltSettings) { val localRepo: File - get() = kobaltSettings.localRepo + get() = kobaltSettings.localCache - fun existsPom(d: LocalDep, v: String) : Boolean { - return File(d.toAbsolutePomFile(v)).exists() - } - - fun existsJar(d: LocalDep, v: String) : Boolean { - return File(d.toAbsoluteJarFilePath(v)).exists() - } - - /** - * If the dependency is local, return the correct version for it - */ - fun findLocalVersion(groupId: String, artifactId: String, packaging: String? = null) : String? { - // No version: look at all the directories under group/artifactId, pick the latest and see - // if it contains a maven and jar file - val dir = toFullPath(KFiles.joinDir(groupId.replace(".", File.separator), artifactId)) - val files = File(dir).listFiles() - - if (files != null) { - val directories = files.filter { it.isDirectory } - if (directories.size > 0) { - Collections.sort(directories, { f1, f2 -> - val v1 = Versions.toLongVersion(f1.name) - val v2 = Versions.toLongVersion(f2.name) - v2.compareTo(v1) // we want the most recent at position 0 - }) - val result = directories[0].name - val newDep = LocalDep(MavenId.create(groupId, artifactId, packaging, null, result), this) - if (existsPom(newDep, result) && existsJar(newDep, result)) { - return result - } - } - } - return null - } - - fun toFullPath(path: String) = File(localRepo, path).absolutePath + 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 index 1ab12db7..13fff154 100644 --- 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 @@ -17,8 +17,12 @@ class MavenId private constructor(val groupId: String, val artifactId: String, v val classifier: String?, val version: String?) { companion object { - fun isMavenId(id: String) = with(id.split(":")) { - size >= 3 && size <= 5 + fun isMavenId(id: String) = if (id.startsWith("file://")) { + false + } else { + with(id.split(':')) { + size >= 3 && size <= 5 + } } fun isRangedVersion(s: String): Boolean { @@ -32,14 +36,14 @@ class MavenId private constructor(val groupId: String, val artifactId: String, v MavenId(groupId, artifactId, extension, classifier, version) } - fun toKobaltId(id: String) = if (id.endsWith(":")) id + "(0,]" else id + 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 = toKobaltId(originalId) + val id = toMavenId(originalId) var originalMavenId = createNoInterceptors(id) var interceptedMavenId = originalMavenId val interceptors = Kobalt.context?.pluginInfo?.mavenIdInterceptors @@ -54,11 +58,12 @@ class MavenId private constructor(val groupId: String, val artifactId: String, v 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" + 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" } 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 index 339efaa1..efa34944 100644 --- 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 @@ -1,12 +1,12 @@ package com.beust.kobalt.maven import com.beust.kobalt.misc.KFiles -import com.beust.kobalt.misc.log +import com.beust.kobalt.misc.kobaltLog import java.io.File import java.security.MessageDigest import javax.xml.bind.DatatypeConverter -public class Md5 { +class Md5 { companion object { // private fun md5(file: File) : String { // if (file.isDirectory) { @@ -18,26 +18,33 @@ public class Md5 { // return DatatypeConverter.printHexBinary(md5.digest()).toLowerCase() // } - fun toMd5Directories(directories: List) : String? { - val ds = directories.filter { it.exists() } - if (ds.size > 0) { + /** + * 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 - directories.filter { it.exists() }.forEach { file -> + filesOrDirectories.filter(File::exists).forEach { file -> if (file.isFile) { - val bytes = file.readBytes() + 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")}) - log(2, " Calculating checksum of ${files.size} files in $file") + kobaltLog(3, " Calculating checksum of ${files.size} files in $file") files.map { File(file, it) }.filter { it.isFile }.forEach { fileCount++ - val bytes = it.readBytes() + val bytes = toBytes(it) md5.update(bytes, 0, bytes.size) } } diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Pom.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Pom.kt index 48f6965e..a6263e22 100644 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Pom.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/Pom.kt @@ -3,11 +3,9 @@ package com.beust.kobalt.maven 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 class Pom @javax.inject.Inject constructor(@Assisted val id: String, @@ -66,8 +64,8 @@ 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) @@ -75,11 +73,12 @@ 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) } @@ -111,5 +110,18 @@ class Pom @javax.inject.Inject constructor(@Assisted val id: String, } } + 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 index 71d7d6af..879f6d51 100644 --- 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 @@ -1,13 +1,12 @@ 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.XmlAnyElement -import javax.xml.bind.annotation.XmlElement -import javax.xml.bind.annotation.XmlRootElement +import javax.xml.bind.annotation.* import javax.xml.parsers.SAXParserFactory import javax.xml.transform.sax.SAXSource @@ -21,9 +20,6 @@ class PomProject { var description: String? = null var url: String? = null var packaging: String? = null - var licenses : Licenses? = null - var developers: Developers? = null - var scm: Scm? = null var properties: Properties? = null var parent: Parent? = null @XmlElement(name = "dependencies") @JvmField @@ -46,12 +42,6 @@ class PomProject { } } -//fun main(argv: Array) { -// val p = Pom2(File("/Users/beust/t/pom.xml")) -// val pom = p.pom -// println("Dependencies: " + pom.dependencies[0]) -//} - class Either(val exception: E?, val value: V?) class Pom2(val pomProject: PomProject) { @@ -156,7 +146,7 @@ class Dependency { private fun expandVariable(s: String, pom: Pom2) : String { val variable = extractVariable(s) if (variable != null) { - println("Expanding variable $variable") + kobaltLog(2, "Expanding variable $variable") val value = pom.pomProject.propertyValue(variable) return s } else { @@ -165,12 +155,6 @@ class Dependency { } } -class Scm { - var connection: String? = null - var developerConnection: String? = null - var url: String? = null -} - class Parent { var groupId: String? = null var artifactId: 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 index 2ccc9250..79c5e4b8 100644 --- 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 @@ -1,66 +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.log +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.Scm 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 -public class PomGenerator @Inject constructor(@Assisted val project: Project) { +class PomGenerator @Inject constructor(@Assisted val project: Project) { interface IFactory { fun create(project: Project) : PomGenerator } - fun generate() { - requireNotNull(project.version, { "version mandatory on project ${project.name}" }) - requireNotNull(project.group, { "group mandatory on project ${project.name}" }) - requireNotNull(project.artifactId, { "artifactId mandatory on project ${project.name}" }) - - val m = Model().apply { - name = project.name - artifactId = project.artifactId - groupId = project.group - version = project.version - description = project.description - licenses = project.licenses.map { it.toMavenLicense() } - url = project.url - scm = Scm().apply { - project.scm?.let { - url = it.url - connection = it.connection - developerConnection = it.developerConnection - } - } - } - with(Developer()) { - name = SystemProperties.username - m.addDeveloper(this) - } - - val dependencies = arrayListOf() - m.dependencies = dependencies - project.compileDependencies.forEach { dep -> - dependencies.add(dep.toMavenDependencies()) - } - - val s = StringWriter() - MavenXpp3Writer().write(s, m) + /** + * 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 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(s.toString(), Charset.defaultCharset()) - log(1, " Created $outputFile") + 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/aether/Aether.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Aether.kt deleted file mode 100644 index 5120c7ad..00000000 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/maven/aether/Aether.kt +++ /dev/null @@ -1,235 +0,0 @@ -package com.beust.kobalt.maven.aether - -import com.beust.kobalt.KobaltException -import com.beust.kobalt.api.IClasspathDependency -import com.beust.kobalt.api.Kobalt -import com.beust.kobalt.homeDir -import com.beust.kobalt.internal.KobaltSettings -import com.beust.kobalt.internal.KobaltSettingsXml -import com.beust.kobalt.maven.CompletedFuture -import com.beust.kobalt.maven.MavenId -import com.beust.kobalt.misc.KobaltLogger -import com.beust.kobalt.misc.Versions -import com.beust.kobalt.misc.log -import com.beust.kobalt.misc.warn -import com.google.inject.Inject -import com.google.inject.Singleton -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.Dependency -import org.eclipse.aether.graph.DependencyFilter -import org.eclipse.aether.graph.DependencyNode -import org.eclipse.aether.metadata.DefaultMetadata -import org.eclipse.aether.repository.RemoteRepository -import org.eclipse.aether.resolution.* -import org.eclipse.aether.util.artifact.JavaScopes -import org.eclipse.aether.util.filter.AndDependencyFilter -import org.eclipse.aether.util.filter.DependencyFilterUtils -import java.io.File -import java.util.concurrent.Future - -class DependencyResult(val dependency: IClasspathDependency, val repoUrl: String) - -class KobaltAether @Inject constructor (val settings: KobaltSettings, val aether: Aether) { - val localRepo: File get() = settings.localRepo - - /** - * Create an IClasspathDependency from a Kobalt id. - */ - fun create(id: String) = AetherDependency(DefaultArtifact(id)) - - /** - * @return the latest artifact for the given group and artifactId. - */ - fun latestArtifact(group: String, artifactId: String, extension: String = "jar") : DependencyResult - = aether.latestArtifact(group, artifactId, extension).let { - DependencyResult(AetherDependency(it.artifact), it.repository.toString()) - } - - fun resolve(id: String): DependencyResult { - log(ConsoleRepositoryListener.LOG_LEVEL, "Resolving $id") - val results = aether.resolve(DefaultArtifact(MavenId.toKobaltId(id))) - if (results != null && results.size > 0) { - return DependencyResult(AetherDependency(results[0].artifact), results[0].repository.toString()) - } else { - throw KobaltException("Couldn't resolve $id") - } - } - -} - -class ExcludeOptionalDependencyFilter: DependencyFilter { - override fun accept(node: DependencyNode?, p1: MutableList?): Boolean { -// val result = node != null && ! node.dependency.isOptional - val accept1 = node == null || node.artifact.artifactId != "srczip" - val accept2 = node != null && ! node.dependency.isOptional - val result = accept1 && accept2 - return result - } -} - -@Singleton -class Aether(val localRepo: File, val settings: KobaltSettings) { - private val system = Booter.newRepositorySystem() - private val session = Booter.newRepositorySystemSession(system, localRepo) - private val classpathFilter = AndDependencyFilter( - ExcludeOptionalDependencyFilter(), - DependencyFilterUtils.classpathFilter(JavaScopes.COMPILE)) - private val kobaltRepositories : List - get() = Kobalt.repos.map { - RemoteRepository.Builder("maven", "default", it.url) - .setProxy(settings.proxyConfig?.toAetherProxy()) -// .setSnapshotPolicy(RepositoryPolicy(false, null, null)) - .build() - } - - private fun collectRequest(artifact: Artifact) : CollectRequest { - with(CollectRequest()) { - root = Dependency(artifact, JavaScopes.COMPILE) - repositories = kobaltRepositories - - return this - } - } - - fun latestArtifact(group: String, artifactId: String, extension: String = "jar") : ArtifactResult { - val artifact = DefaultArtifact(group, artifactId, extension, "(0,]") - val resolved = resolveVersion(artifact) - if (resolved != null) { - val newArtifact = DefaultArtifact(artifact.groupId, artifact.artifactId, artifact.extension, - resolved.highestVersion.toString()) - val artifactResult = resolve(newArtifact) - if (artifactResult != null && artifactResult.size > 0) { - return artifactResult[0] - } else { - throw KobaltException("Couldn't find latest artifact for $group:$artifactId") - } - } else { - throw KobaltException("Couldn't find latest artifact for $group:$artifactId") - } - } - - fun resolveVersion(artifact: Artifact): VersionRangeResult? { - val metadata = DefaultMetadata(artifact.groupId, artifact.artifactId, "maven-metadata.xml", - org.eclipse.aether.metadata.Metadata.Nature.RELEASE) - - val r = system.resolveMetadata(session, kobaltRepositories.map { - MetadataRequest(metadata, it, null).apply { - isFavorLocalRepository = false - } - }) - - -// kobaltRepositories.forEach { -// val request = MetadataRequest(metadata, it, null).apply { -// isFavorLocalRepository = false -// } -// val r = system.resolveMetadata(session, listOf(request)) -// println("Repo: $it " + r) -// } - val request = VersionRangeRequest(artifact, kobaltRepositories, null) - val result = system.resolveVersionRange(session, request) - return result - } - - fun resolve(artifact: Artifact): List? { - try { - val dependencyRequest = DependencyRequest(collectRequest(artifact), classpathFilter) - - val result = system.resolveDependencies(session, dependencyRequest).artifactResults - return result - } catch(ex: DependencyResolutionException) { - warn("Couldn't resolve $artifact") - return emptyList() - } - } - - fun transitiveDependencies(artifact: Artifact) = directDependencies(artifact) - - fun directDependencies(artifact: Artifact): CollectResult? - = system.collectDependencies(session, collectRequest(artifact)) -} - -class AetherDependency(val artifact: Artifact): IClasspathDependency, Comparable { - val aether: Aether get() = Kobalt.INJECTOR.getInstance(Aether::class.java) - - constructor(node: DependencyNode) : this(node.artifact) {} - - 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() = if (artifact.file != null) { - CompletedFuture(artifact.file) - } else { - val td = aether.transitiveDependencies(artifact) - if (td?.root?.artifact?.file != null) { - CompletedFuture(td!!.root.artifact.file) - } else { - val resolved = aether.resolve(artifact) - if (resolved != null && resolved.size > 0) { - CompletedFuture(resolved[0].artifact.file) - } else { - CompletedFuture(File("DONOTEXIST")) // will be filtered out - } - } - } - - override fun toMavenDependencies() = let { md -> - org.apache.maven.model.Dependency().apply { - artifact.let { md -> - groupId = md.groupId - artifactId = md.artifactId - version = md.version - } - } - } - - override fun directDependencies() : List { - val result = arrayListOf() - val deps = aether.directDependencies(artifact) - if (deps != null) { - if (! deps.root.dependency.optional) { - deps.root.children.forEach { - if (! it.dependency.isOptional) { - result.add(AetherDependency(it.artifact)) - } else { - log(ConsoleRepositoryListener.LOG_LEVEL, "Skipping optional dependency " + deps.root.artifact) - } - } - } else { - log(ConsoleRepositoryListener.LOG_LEVEL, "Skipping optional dependency " + deps.root.artifact) - } - } else { - warn("Couldn't resolve $artifact") - } - return result - } - - override val shortId = artifact.groupId + ":" + artifact.artifactId + ":" + artifact.classifier - - override fun compareTo(other: AetherDependency): Int { - return Versions.toLongVersion(artifact.version).compareTo(Versions.toLongVersion( - other.artifact.version)) - } - - override fun toString() = id -} - -fun main(argv: Array) { - KobaltLogger.LOG_LEVEL = 1 - val id = "org.testng:testng:6.9.11" - val aether = KobaltAether(KobaltSettings(KobaltSettingsXml()), Aether(File(homeDir(".aether")),KobaltSettings(KobaltSettingsXml()))) - val r = aether.resolve(id) - val r2 = aether.resolve(id) - val d = org.eclipse.aether.artifact.DefaultArtifact("org.testng:testng:6.9") - - println("Artifact: " + d) -} 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 index ac6fee3e..02917929 100644 --- 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 @@ -1,5 +1,8 @@ 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 @@ -14,14 +17,31 @@ object Booter { // return org.eclipse.aether.examples.plexus.PlexusRepositorySystemFactory.newRepositorySystem(); } - fun newRepositorySystemSession(system: RepositorySystem, repo: File): DefaultRepositorySystemSession { - val session = MavenRepositorySystemUtils.newSession() +// 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() + session.repositoryListener = ConsoleRepositoryListener(eventBus = eventBus) // uncomment to generate dirty trees // session.setDependencyGraphTransformer( null ); 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 index dc55b290..fdbdbac7 100644 --- 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 @@ -1,6 +1,8 @@ package com.beust.kobalt.maven.aether -import com.beust.kobalt.misc.log +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 @@ -8,86 +10,83 @@ import java.io.PrintStream /** * A simplistic repository listener that logs events to the console. */ -class ConsoleRepositoryListener @JvmOverloads constructor(out: PrintStream? = null) : AbstractRepositoryListener() { +class ConsoleRepositoryListener @JvmOverloads constructor(out: PrintStream? = null, val eventBus: EventBus) + : AbstractRepositoryListener() { companion object { val LOG_LEVEL = 4 } - private val out: PrintStream - - init { - this.out = out ?: System.out - } - override fun artifactDeployed(event: RepositoryEvent?) { - log(LOG_LEVEL, "Deployed " + event!!.artifact + " to " + event.repository) + kobaltLog(LOG_LEVEL, "Deployed " + event!!.artifact + " to " + event.repository) } override fun artifactDeploying(event: RepositoryEvent?) { - log(LOG_LEVEL, "Deploying " + event!!.artifact + " to " + event.repository) + kobaltLog(LOG_LEVEL, "Deploying " + event!!.artifact + " to " + event.repository) } override fun artifactDescriptorInvalid(event: RepositoryEvent?) { - log(LOG_LEVEL, "Invalid artifact descriptor for " + event!!.artifact + ": " + kobaltLog(LOG_LEVEL, "Invalid artifact descriptor for " + event!!.artifact + ": " + event.exception.message) } override fun artifactDescriptorMissing(event: RepositoryEvent?) { - log(LOG_LEVEL, "Missing artifact descriptor for " + event!!.artifact) + kobaltLog(LOG_LEVEL, "Missing artifact descriptor for " + event!!.artifact) } override fun artifactInstalled(event: RepositoryEvent?) { - log(LOG_LEVEL, "Installed " + event!!.artifact + " to " + event.file) + kobaltLog(LOG_LEVEL, "Installed " + event!!.artifact + " to " + event.file) } override fun artifactInstalling(event: RepositoryEvent?) { - log(LOG_LEVEL, "Installing " + event!!.artifact + " to " + event.file) + kobaltLog(LOG_LEVEL, "Installing " + event!!.artifact + " to " + event.file) } override fun artifactResolved(event: RepositoryEvent?) { - log(LOG_LEVEL, "Resolved artifact " + event!!.artifact + " from " + event.repository) + kobaltLog(LOG_LEVEL, "Resolved artifact " + event!!.artifact + " from " + event.repository) } override fun artifactDownloading(event: RepositoryEvent?) { - log(LOG_LEVEL, "Downloading artifact " + event!!.artifact + " from " + event.repository) + kobaltLog(LOG_LEVEL, "Downloading artifact " + event!!.artifact + " from " + event.repository) } override fun artifactDownloaded(event: RepositoryEvent?) { - if (event?.file != null) { - log(1, "Downloaded artifact " + event!!.artifact + " from " + event.repository) + 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?) { - log(LOG_LEVEL, "Resolving artifact " + event!!.artifact) + kobaltLog(LOG_LEVEL, "Resolving artifact " + event!!.artifact) } override fun metadataDeployed(event: RepositoryEvent?) { - log(LOG_LEVEL, "Deployed " + event!!.metadata + " to " + event.repository) + kobaltLog(LOG_LEVEL, "Deployed " + event!!.metadata + " to " + event.repository) } override fun metadataDeploying(event: RepositoryEvent?) { - log(LOG_LEVEL, "Deploying " + event!!.metadata + " to " + event.repository) + kobaltLog(LOG_LEVEL, "Deploying " + event!!.metadata + " to " + event.repository) } override fun metadataInstalled(event: RepositoryEvent?) { - log(LOG_LEVEL, "Installed " + event!!.metadata + " to " + event.file) + kobaltLog(LOG_LEVEL, "Installed " + event!!.metadata + " to " + event.file) } override fun metadataInstalling(event: RepositoryEvent?) { - log(LOG_LEVEL, "Installing " + event!!.metadata + " to " + event.file) + kobaltLog(LOG_LEVEL, "Installing " + event!!.metadata + " to " + event.file) } override fun metadataInvalid(event: RepositoryEvent?) { - log(LOG_LEVEL, "Invalid metadata " + event!!.metadata) + kobaltLog(LOG_LEVEL, "Invalid metadata " + event!!.metadata) } override fun metadataResolved(event: RepositoryEvent?) { - log(LOG_LEVEL, "Resolved metadata " + event!!.metadata + " from " + event.repository) + kobaltLog(LOG_LEVEL, "Resolved metadata " + event!!.metadata + " from " + event.repository) } override fun metadataResolving(event: RepositoryEvent?) { - log(LOG_LEVEL, "Resolving metadata " + event!!.metadata + " from " + event.repository) + 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 index 108cd7a1..46d8eb15 100644 --- 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 @@ -1,7 +1,7 @@ package com.beust.kobalt.maven.aether import com.beust.kobalt.misc.KobaltLogger -import com.beust.kobalt.misc.log +import com.beust.kobalt.misc.kobaltLog import org.eclipse.aether.transfer.AbstractTransferListener import org.eclipse.aether.transfer.MetadataNotFoundException import org.eclipse.aether.transfer.TransferEvent @@ -27,10 +27,15 @@ class ConsoleTransferListener @JvmOverloads constructor(out: PrintStream? = null override fun transferInitiated(event: TransferEvent?) { val message = if (event!!.requestType == TransferEvent.RequestType.PUT) "Uploading" else "Downloading" - log(2, message + ": " + event.resource.repositoryUrl + event.resource.resourceName) + 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)) @@ -38,7 +43,7 @@ class ConsoleTransferListener @JvmOverloads constructor(out: PrintStream? = null for (entry in downloads.entries) { val total = entry.key.contentLength - val complete = entry.value.toLong() + val complete = entry.value buffer.append(getStatus(complete, total)).append(" ") } @@ -92,7 +97,7 @@ class ConsoleTransferListener @JvmOverloads constructor(out: PrintStream? = null throughput = " at " + format.format(kbPerSec) + " KB/sec" } - log(2, type + ": " + resource.repositoryUrl + resource.resourceName + " (" + len + kobaltLog(2, type + ": " + resource.repositoryUrl + resource.resourceName + " (" + len + throughput + ")") } } @@ -101,8 +106,8 @@ class ConsoleTransferListener @JvmOverloads constructor(out: PrintStream? = null transferCompleted(event) if (event.exception !is MetadataNotFoundException) { - if (KobaltLogger.LOG_LEVEL > 1) { - event.exception.printStackTrace(out) + if (KobaltLogger.LOG_LEVEL > 2) { + Exceptions.printStackTrace(event.exception) } } } @@ -117,10 +122,10 @@ class ConsoleTransferListener @JvmOverloads constructor(out: PrintStream? = null } override fun transferCorrupted(event: TransferEvent?) { - event!!.exception.printStackTrace(out) + Exceptions.printStackTrace(event!!.exception) } - protected fun toKB(bytes: Long): Long { + 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 index fc21c0de..7f019533 100644 --- 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 @@ -27,7 +27,7 @@ object ManualRepositorySystemFactory { locator.setErrorHandler(object : DefaultServiceLocator.ErrorHandler() { override fun serviceCreationFailed(type: Class<*>, impl: Class<*>, exception: Throwable) { - exception.printStackTrace() + Exceptions.printStackTrace(exception) } }) 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 index 2b6688a9..1b8a5e0f 100644 --- 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 @@ -1,5 +1,6 @@ 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 @@ -12,6 +13,7 @@ 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 { @@ -26,7 +28,7 @@ object MavenRepositorySystemUtils { return locator } - fun newSession(): DefaultRepositorySystemSession { + fun newSession(settings: KobaltSettings): DefaultRepositorySystemSession { val session = DefaultRepositorySystemSession() val depTraverser = FatArtifactTraverser() session.dependencyTraverser = depTraverser @@ -55,6 +57,13 @@ object MavenRepositorySystemUtils { 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/modules/kobalt-plugin-api/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 index 7099ed45..047754e2 100644 --- a/modules/kobalt-plugin-api/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,11 +1,13 @@ 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 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://" } @@ -18,9 +20,10 @@ open class FileDependency(open val fileName: String) : IClasspathDependency, Com 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 } } @@ -29,6 +32,8 @@ open class FileDependency(open val fileName: String) : IClasspathDependency, Com 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 index a3b5dcba..7209c51e 100644 --- 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 @@ -1,12 +1,15 @@ package com.beust.kobalt.misc -fun benchmarkMillis(run: () -> Unit) : Long { +fun benchmarkMillis(run: () -> T) : Pair { val start = System.currentTimeMillis() - run() - return System.currentTimeMillis() - start + val result = run() + return Pair(System.currentTimeMillis() - start, result) } -fun benchmarkSeconds(run: () -> Unit) = benchmarkMillis(run) / 1000 +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 index 2f73266b..d15e8056 100644 --- 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 @@ -5,42 +5,48 @@ 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. */ -public class CheckVersions @Inject constructor(val depManager: DependencyManager, - val executors : KobaltExecutors) { +class CheckVersions @Inject constructor(val depManager: DependencyManager, + val executors : KobaltExecutors, val resolver: KobaltMavenResolver) { - fun run(projects: List) { + fun run(projects: List) = projects.forEach { run(it) } + + fun run(project: Project) { val executor = executors.newExecutor("CheckVersions", 5) val newVersions = hashSetOf() - projects.forEach { - listOf(it.compileDependencies, it.testDependencies).forEach { cds -> - cds.forEach { compileDependency -> - if (MavenId.isMavenId(compileDependency.id)) { - try { - val dep = depManager.create(compileDependency.shortId) - val other = compileDependency as AetherDependency - if (dep.id != compileDependency.id - && Versions.toLongVersion(dep.version) > Versions.toLongVersion(other.version)) { - newVersions.add(dep.id) + 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) { - log(1, " Cannot resolve ${compileDependency.shortId}. ignoring") } + } catch(e: KobaltException) { + kobaltLog(1, " Cannot resolve ${dep.shortId}. ignoring") } } } } if (newVersions.size > 0) { - log(1, "New versions found:") - newVersions.forEach { log(1, " $it") } + kobaltLog(1, " New versions found:") + newVersions.forEach { kobaltLog(1, " $it") } } else { - log(1, "All dependencies up to date") + kobaltLog(1, " All dependencies up to date") } executor.shutdown() } 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 index b4bf9018..b33286e0 100644 --- 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 @@ -1,10 +1,15 @@ 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 @@ -14,12 +19,15 @@ 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 executors: KobaltExecutors, val localProperties: LocalProperties, val http: Http, + val settings:KobaltSettings, val args: Args) { companion object { const val PROPERTY_ACCESS_TOKEN = "github.accessToken" @@ -59,7 +67,7 @@ class GithubApi2 @Inject constructor( // Read only Api // private val service = Retrofit.Builder() - .client(OkHttpClient.Builder().proxy(settings.proxyConfig?.toProxy()).build()) + .client(OkHttpClient.Builder().proxy(settings.proxyConfigs?.firstOrNull()?.toProxy()).build()) .baseUrl("https://api.github.com") .addConverterFactory(GsonConverterFactory.create()) .build() @@ -70,7 +78,7 @@ class GithubApi2 @Inject constructor( class RetrofitError(var message: String = "", var errors : List = arrayListOf()) fun uploadRelease(packageName: String, tagName: String, zipFile: File) { - log(1, "Uploading release ${zipFile.name}") + kobaltLog(1, "Uploading release ${zipFile.name}") val username = localProperties.get(PROPERTY_USERNAME, DOC_URL) val accessToken = localProperties.get(PROPERTY_ACCESS_TOKEN, DOC_URL) @@ -78,15 +86,15 @@ class GithubApi2 @Inject constructor( .execute() val code = response.code() if (code != Http.CREATED) { - val error = Gson().fromJson(response.errorBody().string(), RetrofitError::class.java) + 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) + uploadAsset(accessToken, body?.uploadUrl!!, Http.TypedFile("application/zip", zipFile), tagName) .toBlocking() .forEach { action -> - log(1, "\n${zipFile.name} successfully uploaded") + kobaltLog(1, "\n${zipFile.name} successfully uploaded") } } } @@ -107,32 +115,42 @@ class GithubApi2 @Inject constructor( val latestKobaltVersion: Future get() { val callable = Callable { - var result = "0" - - val username = localProperties.getNoThrows(PROPERTY_USERNAME, DOC_URL) - val accessToken = localProperties.getNoThrows(PROPERTY_ACCESS_TOKEN, DOC_URL) - try { - val req = - if (username != null && accessToken != null) { - service.getReleases(username, "kobalt", accessToken) + 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 { - service.getReleasesNoAuth("cbeust", "kobalt") - } - val releases = req.execute().body() - if (releases != null) { - releases.firstOrNull()?.let { - try { - result = listOf(it.name, it.tagName).filterNotNull().first { !it.isBlank() } - } catch(ex: NoSuchElementException) { - throw KobaltException("Couldn't find the latest release") + warn("Didn't receive any body in the response to GitHub.getReleases()") } } - } else { - warn("Didn't receive any body in the response to GitHub.getReleases()") - } - } catch(e: Exception) { - log(1, "Couldn't retrieve releases from github: " + e.message) - e.printStackTrace() + } 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] @@ -141,8 +159,9 @@ class GithubApi2 @Inject constructor( // } // // TODO: If the credentials didn't work ("bad credentials"), should start again // // using cbeust/kobalt, like above. Right now, just bailing. -// log(2, "Couldn't retrieve releases from github, ${error.message ?: e}: " +// kobaltLog(2, "Couldn't retrieve releases from github, ${error.message ?: e}: " // + details?.code + " field: " + details?.field) + } } result } 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 index 6f8694a9..8d2b0580 100644 --- 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 @@ -1,20 +1,18 @@ package com.beust.kobalt.misc import java.io.File -import java.nio.file.Files -import java.nio.file.Paths -import java.nio.file.StandardCopyOption +import java.nio.file.* class Io(val dryRun: Boolean = false) { fun mkdirs(dir: String) { - log("mkdirs $dir") + kobaltLog("mkdirs $dir") if (! dryRun) { File(dir).mkdirs() } } fun rm(path: String) { - log("rm $path") + kobaltLog("rm $path") if (! dryRun) { File(path).deleteRecursively() @@ -22,7 +20,7 @@ class Io(val dryRun: Boolean = false) { } fun moveFile(from: File, toDir: File) { - log("mv $from $toDir") + kobaltLog("mv $from $toDir") if (! dryRun) { require(from.exists(), { -> "$from should exist" }) require(from.isFile, { -> "$from should be a file" }) @@ -34,7 +32,7 @@ class Io(val dryRun: Boolean = false) { } fun rename(from: File, to: File) { - log("rename $from $to") + kobaltLog("rename $from $to") moveFile(from, to.parentFile) if (from.name != to.name) { File(to, from.name).renameTo(to) @@ -42,13 +40,13 @@ class Io(val dryRun: Boolean = false) { } fun copyDirectory(from: File, toDir: File) { - log("cp -r $from $toDir") + kobaltLog("cp -r $from $toDir") if (! dryRun) { KFiles.copyRecursively(from, toDir) require(from.exists(), { -> "$from should exist" }) - require(from.isDirectory, { -> println("$from should be a directory")}) - require(toDir.isDirectory, { -> println("$toDir should be a file")}) + require(from.isDirectory, { -> kobaltLog(1, "$from should be a directory")}) + require(toDir.isDirectory, { -> kobaltLog(1, "$toDir should be a file")}) } } @@ -56,9 +54,9 @@ class Io(val dryRun: Boolean = false) { fun rmDir(dir: File, keep: (File) -> Boolean = { t -> false }) = rmDir(dir, keep, " ") private fun rmDir(dir: File, keep: (File) -> Boolean, indent : String) { - log("rm -rf $dir") + kobaltLog("rm -rf $dir") - require(dir.isDirectory, { -> println("$dir should be a directory")}) + require(dir.isDirectory, { -> kobaltLog(1, "$dir should be a directory")}) dir.listFiles({ p0 -> ! keep(p0!!) }).forEach { if (it.isDirectory) { @@ -66,21 +64,21 @@ class Io(val dryRun: Boolean = false) { it.deleteRecursively() } else { - log(indent + "rm $it") + kobaltLog(indent + "rm $it") if (! dryRun) it.delete() } } } fun mkdir(dir: File) { - log("rm -rf $dir") + kobaltLog("mkdir $dir") if (! dryRun) { dir.mkdirs() } } - private fun log(s: String) { - log(1, "[Io] $s") + 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 index 90b8aacc..ba83b0a6 100644 --- 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 @@ -1,19 +1,18 @@ package com.beust.kobalt.misc -import com.beust.kobalt.Glob +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.* -import java.nio.file.Paths -import java.util.jar.JarEntry +import java.io.File +import java.io.FileOutputStream +import java.io.InputStreamReader import java.util.jar.JarFile -import java.util.jar.JarInputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipException import java.util.zip.ZipFile -import java.util.zip.ZipOutputStream -public class JarUtils { +class JarUtils { companion object { val DEFAULT_HANDLER: (Exception) -> Unit = { ex: Exception -> // Ignore duplicate entry exceptions @@ -22,18 +21,15 @@ public class JarUtils { } } - fun addFiles(directory: String, files: List, target: ZipOutputStream, + fun addFiles(directory: String, files: List, metaArchive: MetaArchive, expandJarFiles: Boolean, onError: (Exception) -> Unit = DEFAULT_HANDLER) { files.forEach { - addSingleFile(directory, it, target, expandJarFiles, onError) + addSingleFile(directory, it, metaArchive, expandJarFiles, onError) } } - private val DEFAULT_JAR_EXCLUDES = - Glob("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") - - fun addSingleFile(directory: String, file: IncludedFile, outputStream: ZipOutputStream, + fun addSingleFile(directory: String, file: IncludedFile, metaArchive: MetaArchive, expandJarFiles: Boolean, onError: (Exception) -> Unit = DEFAULT_HANDLER) { val foundFiles = file.allFromFiles(directory) foundFiles.forEach { foundFile -> @@ -48,73 +44,34 @@ public class JarUtils { } if (foundFile.isDirectory) { - log(2, "Writing contents of directory $foundFile") + kobaltLog(2, " Writing contents of directory $foundFile") // Directory - var name = foundFile.name - if (!name.isEmpty()) { - if (!name.endsWith("/")) name += "/" - val entry = JarEntry(name) - entry.time = foundFile.lastModified() - try { - outputStream.putNextEntry(entry) - } catch(ex: ZipException) { - log(2, "Can't add $name: ${ex.message}") - } finally { - outputStream.closeEntry() - } - } - val includedFile = IncludedFile(From(foundFile.path), To(""), listOf(IFileSpec.GlobSpec("**"))) - addSingleFile(".", includedFile, outputStream, expandJarFiles) + val includedFile = IncludedFile(From(""), To(""), listOf(IFileSpec.GlobSpec("**"))) + addSingleFile(localFile.path, includedFile, metaArchive, expandJarFiles) } else { - if (file.expandJarFiles && foundFile.name.endsWith(".jar") && ! file.from.contains("resources")) { - log(2, "Writing contents of jar file $foundFile") - val stream = JarInputStream(FileInputStream(localFile)) - var entry = stream.nextEntry - while (entry != null) { - if (!entry.isDirectory && !KFiles.isExcluded(entry.name, DEFAULT_JAR_EXCLUDES)) { - val ins = JarFile(localFile).getInputStream(entry) - addEntry(ins, JarEntry(entry), outputStream, onError) - } - entry = stream.nextEntry + 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) } - } else { - val entryFileName = file.to(foundFile.path).path.replace("\\", "/") - val entry = JarEntry(entryFileName) - entry.time = foundFile.lastModified() - addEntry(FileInputStream(localFile), entry, outputStream, onError) + } catch(ex: Exception) { + onError(ex) } } } } - private fun addEntry(inputStream: InputStream, entry: ZipEntry, outputStream: ZipOutputStream, - onError: (Exception) -> Unit = DEFAULT_HANDLER) { - var bis: BufferedInputStream? = null - try { - outputStream.putNextEntry(entry) - bis = BufferedInputStream(inputStream) - - val buffer = ByteArray(50 * 1024) - while (true) { - val count = bis.read(buffer) - if (count == -1) break - outputStream.write(buffer, 0, count) - } - outputStream.closeEntry() - } catch(ex: Exception) { - onError(ex) - } finally { - bis?.close() - } - } - fun extractTextFile(zip : ZipFile, fileName: String) : String? { val enumEntries = zip.entries() while (enumEntries.hasMoreElements()) { val file = enumEntries.nextElement() if (file.name == fileName) { - log(2, "Found $fileName in ${zip.name}") + kobaltLog(2, "Found $fileName in ${zip.name}") zip.getInputStream(file).use { ins -> return CharStreams.toString(InputStreamReader(ins, "UTF-8")) } @@ -148,39 +105,3 @@ public class JarUtils { } } -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 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 public 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()} - } -} - -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/misc/KFiles.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KFiles.kt index 71e16358..fcf5b86a 100644 --- 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 @@ -3,24 +3,39 @@ 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.internal.build.BuildFile import com.beust.kobalt.maven.Md5 -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream +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. + * 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") @@ -32,13 +47,24 @@ class KFiles { if (jarFile.exists()) { return listOf(jarFile.absolutePath) } else { - // Will only happen when building kobalt itself: the jar file might not be in the dist/ directory - // yet since we're currently building it. Instead, use the classes directly - val result = listOf("kobalt", "kobalt-plugin-api", "kobalt-wrapper").map { - File(homeDir(KFiles.joinDir("kotlin", "kobalt", "out", "production", it))).absolutePath - } + // 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 + return result.filter { File(it).exists() } } } } @@ -65,8 +91,9 @@ class KFiles { val TEST_CLASSES_DIR : String = "test-classes" - private fun generatedDir(project: Project, variant: Variant) - = KFiles.joinDir(project.directory, project.buildDirectory, "generated", variant.toIntermediateDir()) + 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, @@ -84,10 +111,12 @@ class KFiles { */ 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) = KFiles.makeDir(KFiles.buildDir(project).path, "libs").path + 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 @@ -107,15 +136,18 @@ class KFiles { */ 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(), { s -> true }) + findRecursively(rootDir, arrayListOf(), { _ -> true }) fun findRecursively(rootDir: File, directories: List, function: Function1): List { - var result = arrayListOf() + val result = arrayListOf() val allDirs = arrayListOf() if (directories.isEmpty()) { @@ -127,7 +159,9 @@ class KFiles { val seen = hashSetOf() allDirs.forEach { dir -> if (! dir.exists()) { - log(2, "Couldn't find directory $dir") + 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 { @@ -136,7 +170,7 @@ class KFiles { result.add(File(dir, rel.toFile().path).path) seen.add(rel) } else { - log(2, "Skipped file already seen in previous flavor: $rel") + kobaltLog(2, "Skipped file already seen in previous flavor: $rel") } } } @@ -147,7 +181,7 @@ class KFiles { } fun findRecursively(directory: File, function: Function1): List { - var result = arrayListOf() + val result = arrayListOf() directory.listFiles().forEach { if (it.isFile && function(it.path)) { result.add(it.path) @@ -158,111 +192,71 @@ class KFiles { return result } - fun copyRecursively(from: File, to: File, replaceExisting: Boolean = true, deleteFirst: Boolean = false, - onError: (File, IOException) -> OnErrorAction = { file, exception -> throw exception }) { - // Need to wait until copyRecursively supports an overwrite: Boolean = false parameter - // Until then, wipe everything first - if (deleteFirst) to.deleteRecursively() -// to.mkdirs() - hackCopyRecursively(from, to, replaceExisting = replaceExisting, onError = onError) - } - - /** Private exception class, used to terminate recursive copying */ - private class TerminateException(file: File) : FileSystemException(file) {} - /** - * Copy/pasted from kotlin/io/Utils.kt to add support for overwriting. + * List the files contained in a directory or a jar file. */ - private fun hackCopyRecursively(from: File, dst: File, - replaceExisting: Boolean, - checkTimestamp: Boolean = false, - onError: (File, IOException) -> OnErrorAction = - { file, exception -> throw exception } - ): Boolean { - if (!from.exists()) { - return onError(from, NoSuchFileException(file = from, reason = "The source file doesn't exist")) != - OnErrorAction.TERMINATE - } - try { - // We cannot break for loop from inside a lambda, so we have to use an exception here - for (src in from.walkTopDown().onFail { f, e -> - if (onError(f, e) == OnErrorAction.TERMINATE) throw TerminateException(f) - }) { - if (!src.exists()) { - if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist")) == - OnErrorAction.TERMINATE) - return false - } else { - val relPath = src.relativeTo(from) - val dstFile = File(KFiles.joinDir(dst.path, relPath.path)) - if (dstFile.exists() && !replaceExisting && !(src.isDirectory && dstFile.isDirectory)) { - if (onError(dstFile, FileAlreadyExistsException(file = src, - other = dstFile, - reason = "The destination file already exists")) == OnErrorAction.TERMINATE) - return false - } else if (src.isDirectory) { - dstFile.mkdirs() - } else { - if (Features.USE_TIMESTAMPS && dstFile.exists() && Md5.toMd5(src) == Md5.toMd5(dstFile)) { - log(3, " Identical files, not copying $src to $dstFile") - } else { - val target = src.copyTo(dstFile, true) - if (target.length() != src.length()) { - if (onError(src, - IOException("src.length() != dst.length()")) == OnErrorAction.TERMINATE) - return false - } - } + 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; } } } - return true - } catch (e: TerminateException) { - return false - } - } - private fun findDotDir(buildFile: BuildFile) : File { - val result = File(buildFile.directory.parentFile.parentFile, KOBALT_DOT_DIR).apply { - mkdirs() + } else { + throw KobaltException("Can't list files of a file: " + file) } - return result } /** * The build location for build scripts is .kobalt/build */ - fun findBuildScriptLocation(buildFile: BuildFile, jarFile: String) : String { - val result = joinDir(buildFile.dotKobaltDir.absolutePath, KFiles.SCRIPT_BUILD_DIR, jarFile) - log(2, "Script jar file: $result") + 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) { - file.absoluteFile.parentFile.mkdirs() - file.writeText(text) - log(2, "Created $file") + 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"); + private fun isWindows() = System.getProperty("os.name").contains("Windows") fun copy(from: Path?, to: Path?, option: StandardCopyOption = StandardCopyOption.REPLACE_EXISTING) { - if (isWindows() && to!!.toFile().exists()) { - log(2, "Windows detected, not overwriting $to") - } else { - try { - if (from != null && to != null) { - if (!Files.exists(to) || Md5.toMd5(from.toFile()) != Md5.toMd5(to.toFile())) { - log(3, "Copy from $from to $to") - Files.copy(from, to, option) - } else { - log(3, " Not copying, indentical files: $from $to") - } + 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 - log(1, "Couldn't copy $from to $to: ${ex.message}") } + } catch(ex: IOException) { + // Windows is anal about this + kobaltLog(1, "Couldn't copy $from to $to: ${ex.message}") } } @@ -274,12 +268,18 @@ class KFiles { } } - fun createTempFile(suffix : String = "", deleteOnExit: Boolean = false) : File = - File.createTempFile("kobalt", suffix, File(SystemProperties.tmpDir)).let { + 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 { @@ -292,8 +292,6 @@ class KFiles { fun isExcluded(file: String, excludes: Glob) = isExcluded(file, listOf(excludes)) - fun isExcluded(file: File, excludes: Glob) = isExcluded(file.path, listOf(excludes)) - fun isExcluded(file: File, excludes: List) = isExcluded(file.path, excludes) fun isExcluded(file: String, excludes: List): Boolean = excludes.any { it.matches(file) } @@ -318,13 +316,60 @@ class KFiles { false }) } else { - log(3, "Skipping nonexistent source directory $sourceDir") + 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 { diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltExecutors.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltExecutors.kt index f35d6835..1dc9ab41 100644 --- a/modules/kobalt-plugin-api/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<*>) { @@ -40,8 +39,8 @@ class KobaltExecutor(name: String, threadCount: Int) } } -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 index c162a891..e2bd89de 100644 --- 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 @@ -1,89 +1,116 @@ 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 java.text.SimpleDateFormat -import java.util.* +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: String, newLine : Boolean = true) { - if (level <= KobaltLogger.LOG_LEVEL) { +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.logWrap(level: Int, text1: String, text2: String, function: () -> Unit) { - if (level <= KobaltLogger.LOG_LEVEL) { +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) { + if (level <= KobaltLogger.LOG_LEVEL && !KobaltLogger.isQuiet) { KobaltLogger.logger.log(javaClass.simpleName, text2, newLine = true) } } -fun Any.debug(text: String) { +fun Any.debug(text: CharSequence) { KobaltLogger.logger.debug(javaClass.simpleName, text) } -fun Any.warn(text: String, exception: Exception? = null) { +fun Any.warn(text: CharSequence, exception: Exception? = null) { KobaltLogger.logger.warn(javaClass.simpleName, text, exception) } -fun Any.kobaltError(text: String, e: Throwable? = null) = error(text, e) +fun Any.kobaltError(text: CharSequence, e: Throwable? = null) = error(text, e) -fun Any.error(text: String, e: Throwable? = null) { +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) + 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 = SimpleDateFormat("HH:mm:ss.SSS") + val FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss.SSS") - private fun getPattern(shortTag: String, shortMessage: String, longMessage: String, tag: String) = + private fun getPattern(shortTag: String, shortMessage: CharSequence, longMessage: CharSequence, tag: String) = if (dev) { - val ts = FORMAT.format(Date()) + val ts = LocalDateTime.now().format(FORMAT) "$shortTag/$ts [" + Thread.currentThread().name + "] $tag - $shortMessage" } else { longMessage } - final fun debug(tag: String, message: String) = + fun debug(tag: String, message: CharSequence) = println(getPattern("D", message, message, tag)) - final fun error(tag: String, message: String, e: Throwable? = null) { + 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.isBlank()) message + val text = + if (message.isNotBlank()) message else if (e != null && (! e.message.isNullOrBlank())) e.message - else { "" } + 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?.printStackTrace() + if (KobaltLogger.LOG_LEVEL > 1 && e != null) { + Exceptions.printStackTrace(e) } } - final fun warn(tag: String, message: String, e: Throwable? = null) { - val fullMessage = "***** WARNING " + (e?.message ?: message) + 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?.printStackTrace() + if (KobaltLogger.LOG_LEVEL > 1 && e != null) { + Exceptions.printStackTrace(e) } } - final fun log(tag: String, message: String, newLine: Boolean) = + 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/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltWrapperProperties.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/KobaltWrapperProperties.kt index 8af0da44..a7b5177d 100644 --- a/modules/kobalt-plugin-api/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/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/LocalProperties.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/LocalProperties.kt index 4fe16a5c..2cf0775e 100644 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/LocalProperties.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/LocalProperties.kt @@ -6,6 +6,9 @@ import java.nio.file.Files import java.nio.file.Paths import java.util.* +/** + * Encapsulate read access to local.properties. + */ @Singleton class LocalProperties { val localProperties: Properties by lazy { @@ -22,11 +25,11 @@ class LocalProperties { result } - fun getNoThrows(name: String, docUrl: String? = null) = localProperties.getProperty(name) + fun getNoThrows(name: String): String? = localProperties.getProperty(name) fun get(name: String, docUrl: String? = null) : String { - val result = getNoThrows(name, docUrl) + 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/NewRunCommand.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/NewRunCommand.kt index a0178c68..e105133b 100644 --- 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 @@ -19,6 +19,7 @@ class RunCommandInfo { */ 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 @@ -37,6 +38,8 @@ class RunCommandInfo { ! hasErrors } + + var containsErrors: ((List) -> Boolean)? = null } fun runCommand(init: RunCommandInfo.() -> Unit) = NewRunCommand(RunCommandInfo().apply { init() }).invoke() @@ -45,11 +48,11 @@ open class NewRunCommand(val info: RunCommandInfo) { companion object { val DEFAULT_SUCCESS = { output: List -> } - // val DEFAULT_SUCCESS_VERBOSE = { output: List -> log(2, "Success:\n " + output.joinToString("\n"))} + // val DEFAULT_SUCCESS_VERBOSE = { output: List -> kobaltLog(2, "Success:\n " + output.joinToString + // ("\n"))} // val defaultSuccess = DEFAULT_SUCCESS - val DEFAULT_ERROR = { - output: List -> - kotlin.error(output.joinToString("\n ")) + val DEFAULT_ERROR = { output: List -> + kobaltError(output.joinToString("\n ")) } } @@ -65,7 +68,7 @@ open class NewRunCommand(val info: RunCommandInfo) { val pb = ProcessBuilder(allArgs) pb.directory(info.directory) - log(2, "Running command in directory ${info.directory.absolutePath}" + + kobaltLog(2, "Running command in directory ${info.directory.absolutePath}" + "\n " + allArgs.joinToString(" ").replace("\\", "/")) pb.environment().let { pbEnv -> info.env.forEach { @@ -76,17 +79,31 @@ open class NewRunCommand(val info: RunCommandInfo) { val process = pb.start() // Run the command and collect the return code and streams - val returnCode = process.waitFor(30, TimeUnit.SECONDS) - val input = if (process.inputStream.available() > 0) fromStream(process.inputStream) - else listOf() - val error = if (process.errorStream.available() > 0) fromStream(process.errorStream) - else listOf() + 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 = isSuccess(returnCode, input, error) + val isSuccess = + if (info.containsErrors != null) ! info.containsErrors!!(error) + else isSuccess(if (info.ignoreExitValue) true else processFinished, input, error) if (isSuccess) { - info.successCallback(input) + if (!info.useErrorStreamAsErrorIndicator) { + info.successCallback(error + input) + } else { + info.successCallback(input) + } } else { info.errorCallback(error + input) } @@ -99,12 +116,12 @@ open class NewRunCommand(val info: RunCommandInfo) { * have various ways to signal errors. */ open protected fun isSuccess(isSuccess: Boolean, input: List, error: List) : Boolean { - var hasErrors = ! isSuccess + var hasErrors: Boolean = ! isSuccess if (info.useErrorStreamAsErrorIndicator && ! hasErrors) { - hasErrors = hasErrors || error.size > 0 + hasErrors = hasErrors || error.isNotEmpty() } if (info.useInputStreamAsErrorIndicator && ! hasErrors) { - hasErrors = hasErrors || input.size > 0 + hasErrors = hasErrors || input.isNotEmpty() } return ! hasErrors diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Node.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Node.kt index 4fd2f216..26707f03 100644 --- a/modules/kobalt-plugin-api/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/RunCommand.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/RunCommand.kt deleted file mode 100644 index 76231527..00000000 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/RunCommand.kt +++ /dev/null @@ -1,90 +0,0 @@ -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 - -open class RunCommand(val command: String) { - val DEFAULT_SUCCESS = { output: List -> } -// val DEFAULT_SUCCESS_VERBOSE = { output: List -> log(2, "Success:\n " + output.joinToString("\n"))} - val defaultSuccess = DEFAULT_SUCCESS - val DEFAULT_ERROR = { - output: List -> error(output.joinToString("\n ")) - } - - var directory = File(".") - var env = 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 = true - var useInputStreamAsErrorIndicator = false - - fun useErrorStreamAsErrorIndicator(f: Boolean) : RunCommand { - useErrorStreamAsErrorIndicator = f - return this - } - - open fun run(args: List, - errorCallback: Function1, Unit> = DEFAULT_ERROR, - successCallback: Function1, Unit> = defaultSuccess) : Int { - val allArgs = arrayListOf() - allArgs.add(command) - allArgs.addAll(args) - - val pb = ProcessBuilder(allArgs) - pb.directory(directory) - log(2, "Running command in directory ${directory.absolutePath}" + - "\n " + allArgs.joinToString(" ")) - val process = pb.start() - pb.environment().let { pbEnv -> - env.forEach {it -> - pbEnv.put(it.key, it.value) - } - } - val callSucceeded = process.waitFor(30, TimeUnit.SECONDS) - val input = if (process.inputStream.available() > 0) fromStream(process.inputStream) else emptyList() - val error = if (process.errorStream.available() > 0) fromStream(process.errorStream) else emptyList() - val isSuccess = isSuccess(callSucceeded, input, error) - - if (isSuccess) { - successCallback(input) - } else { - errorCallback(error + input) - } - - return if (isSuccess) 0 else 1 - } - - open protected fun isSuccess(callSucceeded: Boolean, input: List, error: List) : Boolean { - var hasErrors = ! callSucceeded - if (useErrorStreamAsErrorIndicator && ! hasErrors) { - hasErrors = hasErrors || error.size > 0 - } - if (useInputStreamAsErrorIndicator && ! hasErrors) { - hasErrors = hasErrors || input.size > 0 - } - - return ! hasErrors - } - - 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 - -// val result = CharStreams.toString(InputStreamReader(ins, Charset.defaultCharset())) -// return result.split("\n") - } -} 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/Topological.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/misc/Topological.kt index b0a92315..38c4c503 100644 --- a/modules/kobalt-plugin-api/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.toMutableList()) - } - /** * @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 index 0cb8b397..205c2ced 100644 --- 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 @@ -1,41 +1,10 @@ package com.beust.kobalt.misc import com.beust.kobalt.maven.MavenId -import com.google.common.base.CharMatcher +import java.lang.* import java.math.BigInteger import java.util.* -public class Versions { - companion object { - /** - * Turn "6.9.4" into 600090004 - */ - public fun toLongVersion(version: String) : Long { - val count = version.countChar('.') - val normalizedVersion = if (count == 2) version else if (count == 1) version + ".0" - else version + ".0.0" - - fun parseLong(s: String, radix: Int) : Long { - try { - return java.lang.Long.parseLong(s, radix) - } catch(ex: NumberFormatException) { - warn("Couldn't parse version \"${version}\"") - return 0L - } - } - - return normalizedVersion - .split(".") - .take(3) - .map { - val s = CharMatcher.inRange('0', '9').or(CharMatcher.`is`('.')).retainFrom(it) - parseLong(s, 10) - } - .fold(0L, { n, s -> s + n * 10000 }) - } - } -} - class Version(val version: String, val snapshotTimestamp: String? = null): Comparable { companion object { @@ -107,7 +76,7 @@ class Version(val version: String, val snapshotTimestamp: String? = null): Compa var lowerExclusive = version.startsWith("(") var upperExclusive = version.endsWith(")") - val split = version.drop(1).dropLast(1).split(",") + val split = version.drop(1).dropLast(1).split(',') val lower = Version.of(split[0].substring(1)) val upper = if(split.size > 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 new file mode 100644 index 00000000..c0b5dafe --- /dev/null +++ b/modules/wrapper/build.gradle @@ -0,0 +1,38 @@ +jar { + manifest { + attributes 'Main-Class': '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/kobalt-wrapper.iml b/modules/wrapper/kobalt-wrapper.iml deleted file mode 100644 index 1a90945d..00000000 --- a/modules/wrapper/kobalt-wrapper.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file 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 fcd32d01..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; @@ -23,6 +25,7 @@ public class Main { 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"; @@ -33,6 +36,7 @@ public class Main { private final Properties wrapperProperties = new Properties(); + private static int logQuietLevel = 0; private static int logLevel = 1; private boolean noOverwrite = false; @@ -43,7 +47,6 @@ public class Main { private int installAndLaunchMain(String[] argv) throws IOException, InterruptedException { String version = getVersion(); - initWrapperFile(version); List kobaltArgv = new ArrayList<>(); boolean noLaunch = false; @@ -74,9 +77,10 @@ public class Main { } int result = 0; if (! exit) { + initWrapperFile(version); Path kobaltJarFile = installDistribution(); if (!noLaunch) { - result = launchMain(kobaltJarFile, kobaltArgv.toArray(new String[kobaltArgv.size()])); + result = launchMain(kobaltJarFile, kobaltArgv); } } return result; @@ -114,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 { @@ -129,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) { @@ -144,7 +148,7 @@ public class Main { return System.getProperty("os.name").contains("Windows"); } - private static final String[] FILES = new String[] { KOBALTW, "kobalt/wrapper/" + FILE_NAME + "-wrapper.jar" }; + private static final String[] FILES = new String[] { KOBALTW, KOBALTW_BAT, "kobalt/wrapper/" + FILE_NAME + "-wrapper.jar" }; private Path installDistribution() throws IOException { String wrapperVersion; @@ -164,11 +168,9 @@ public class Main { log(2, "Wrapper version: " + wrapperVersion); - boolean isNew = Float.parseFloat(version) * 1000 >= 650; - String toZipOutputDir = DISTRIBUTIONS_DIR; Path kobaltJarFile = Paths.get(toZipOutputDir, - isNew ? "kobalt-" + wrapperVersion : "", + "kobalt-" + wrapperVersion, getWrapperDir().getPath() + "/" + FILE_NAME + "-" + wrapperVersion + ".jar"); boolean downloadedZipFile = false; if (! Files.exists(localZipFile) || ! Files.exists(kobaltJarFile)) { @@ -218,6 +220,20 @@ public class Main { log(2, " Couldn't find $VERSION_TEXT, overwriting the installed wrapper"); installWrapperFiles(version, wrapperVersion); } + + // + // 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; @@ -232,6 +248,8 @@ public class Main { 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 { @@ -267,22 +285,23 @@ public class Main { private void generateKobaltW(Path filePath) throws IOException { // - // For kobaltw: try to generate it with the correct env shebang. If this fails, - // we'll generate it without the env shebang + // For kobaltw: try to generate it with the correct env shebang. // - File envFile = new File("/bin/env"); - if (!envFile.exists()) { - envFile = new File("/usr/bin/env"); + 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 = ""; + String content = "#!" + envPath + " sh\n" + + "java -jar \"`dirname \"$0\"`/kobalt/wrapper/kobalt-wrapper.jar\" $*\n"; - if (envFile.exists()) { - content = "#!" + envFile.getAbsolutePath() + " bash\n"; - } - log(2, " Generating " + KOBALTW + (envFile.exists() ? " with shebang" : "") + "."); - - content += "java -jar $(dirname $0)/kobalt/wrapper/kobalt-wrapper.jar $*\n"; + log(2, " Generating " + KOBALTW + " with shebang."); Files.write(filePath, content.getBytes()); @@ -293,6 +312,21 @@ public class Main { } } + 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 */ @@ -311,6 +345,11 @@ 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); } @@ -344,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) { @@ -405,12 +449,14 @@ 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; + boolean hasTerminal = System.console() != null && System.getProperty(PROPERTY_NO_ANIMATIONS) == null; if (! hasTerminal) { log2(1, "\rDownloading " + url); } @@ -447,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); } @@ -457,12 +503,22 @@ public class Main { System.out.println("[Wrapper error] *** " + s); } - private int 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(); @@ -471,4 +527,4 @@ public class Main { return process.waitFor(); } -} \ 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 new file mode 100644 index 00000000..c36e45fd --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +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/Main.kt b/src/main/kotlin/com/beust/kobalt/Main.kt index e389e979..19b85404 100644 --- a/src/main/kotlin/com/beust/kobalt/Main.kt +++ b/src/main/kotlin/com/beust/kobalt/Main.kt @@ -3,83 +3,86 @@ package com.beust.kobalt import com.beust.jcommander.JCommander import com.beust.kobalt.api.IClasspathDependency import com.beust.kobalt.api.Kobalt -import com.beust.kobalt.api.PluginTask -import com.beust.kobalt.api.Project -import com.beust.kobalt.app.* -import com.beust.kobalt.app.remote.DependencyData +import com.beust.kobalt.app.MainModule +import com.beust.kobalt.app.UpdateKobalt import com.beust.kobalt.app.remote.KobaltClient -import com.beust.kobalt.app.remote.KobaltServer -import com.beust.kobalt.internal.Gc import com.beust.kobalt.internal.KobaltSettings import com.beust.kobalt.internal.PluginInfo -import com.beust.kobalt.internal.TaskManager -import com.beust.kobalt.internal.build.BuildFile 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.common.collect.HashMultimap import java.io.File import java.net.URLClassLoader -import java.nio.file.Paths -import java.util.* import javax.inject.Inject fun main(argv: Array) { - val result = mainNoExit(argv) + 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) -} - -fun mainNoExit(argv: Array): Int { - val (jc, args) = parseArgs(argv) - Kobalt.init(MainModule(args, KobaltSettings.readSettingsXml())) - val result = Kobalt.INJECTOR.getInstance(Main::class.java).run { - val runResult = run(jc, args, argv) - pluginInfo.cleanUp() - executors.shutdown() - runResult - } - return result -} - -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 dependencyManager: DependencyManager, - val checkVersions: CheckVersions, val github: GithubApi2, val updateKobalt: UpdateKobalt, val client: KobaltClient, val pluginInfo: PluginInfo, - val dependencyData: DependencyData, - 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 installCommandLinePlugins(args: Args) : ClassLoader { + 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) }) + dependencies.addAll(it.split(',').map { dependencyManager.create(it) }) } args.pluginJarFiles?.let { - dependencies.addAll(it.split(",").map { FileDependency(it) }) + dependencies.addAll(it.split(',').map { FileDependency(it) }) } if (dependencies.size > 0) { val urls = dependencies.map { it.jarFile.get().toURI().toURL() } @@ -95,236 +98,39 @@ private class Main @Inject constructor( // // Install plug-ins requested from the command line // - val pluginClassLoader = installCommandLinePlugins(args) - - // --listTemplates - if (args.listTemplates) { - Templates().list(pluginInfo) - return 0 - } + installCommandLinePlugins(args) if (args.client) { client.run() return 0 } - var result = 0 + var result = 1 + val latestVersionFuture = github.latestKobaltVersion - val seconds = benchmarkSeconds { - try { - result = runWithArgs(jc, args, argv, pluginClassLoader) - } catch(ex: KobaltException) { - error("", ex.cause ?: ex) - result = 1 - } + try { + result = runWithArgs(jc, args, argv) + } catch(ex: Throwable) { + error("", ex.cause ?: ex) } - if (! args.update) { - log(1, if (result != 0) "BUILD FAILED: $result" else "BUILD SUCCESSFUL ($seconds seconds)") - + if (!args.update) { updateKobalt.checkForNewVersion(latestVersionFuture) } return result } - // public fun runTest() { - // val file = File("src\\main\\resources\\META-INF\\plugin.ml") - // } - - private fun runWithArgs(jc: JCommander, args: Args, argv: Array, pluginClassLoader: ClassLoader): 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() + 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 - val buildFile = BuildFile(Paths.get(p.absolutePath), p.name) - + if (!args.update) { - println(AsciiArt.banner + Kobalt.version + "\n") + kobaltLog(1, AsciiArt.banner + Kobalt.version + "\n") } - if (args.templates != null) { - // - // --init: create a new build project and install the wrapper - // Make sure the wrapper won't call us back with --noLaunch - // - projectGenerator.run(args, pluginClassLoader) - // The wrapper has to call System.exit() in order to set the exit code, - // so make sure we call it last (or possibly launch it in a separate JVM). - com.beust.kobalt.wrapper.Main.main(arrayOf("--noLaunch") + argv) - } else if (args.usage) { - jc.usage() - } else if (args.serverMode) { - // --server - val port = KobaltServer(args.force, args.port, - { buildFile -> initForBuildFile(BuildFile(Paths.get(buildFile), buildFile), args)}, - { cleanUp() }) - .call() - } else { - // Options that don't need Build.kt to be parsed first - if (args.gc) { - Gc().run() - } else if (args.update) { - // --update - updateKobalt.updateKobalt() - } else { - // - // Everything below requires to parse the build file first - // - if (!buildFile.exists()) { - error(buildFile.path.toFile().path + " does not exist") - } else { - - val allProjects = initForBuildFile(buildFile, args) - - // DONOTCOMMIT -// val data = dependencyData.dependenciesDataFor(homeDir("kotlin/klaxon/kobalt/src/Build.kt"), Args()) -// println("Data: $data") - - if (args.projectInfo) { - // --projectInfo - allProjects.forEach { - resolveDependency.run(it.compileDependencies.map {it.id}) - } - } else if (args.dependencies != null) { - // --resolve - resolveDependency.run(args.dependencies!!.split(",").toList()) - } else if (args.tasks) { - // --tasks - displayTasks() - } else if (args.checkVersions) { - // --checkVersions - checkVersions.run(allProjects) - } else if (args.download) { - // -- download - updateKobalt.downloadKobalt() - } else { - // - // Launch the build - // - val runTargetResult = taskManager.runTargets(args.targets, allProjects) - if (result == 0) { - result = runTargetResult.exitCode - } - - // Shutdown all plug-ins - plugins.shutdownPlugins() - - log(3, "Timings:\n " + runTargetResult.messages.joinToString("\n ")) - } - } - } - } - return result + return options.run(jc, args, argv) } - private fun cleanUp() { - pluginInfo.cleanUp() - taskManager.cleanUp() - } - private fun initForBuildFile(buildFile: BuildFile, args: Args): List { - val findProjectResult = buildFileCompilerFactory.create(listOf(buildFile), pluginInfo) - .compileBuildFiles(args) - if (! findProjectResult.taskResult.success) { - throw KobaltException("Couldn't compile build file: " - + findProjectResult.taskResult.errorMessage) - } - - val allProjects = findProjectResult.projects - - // - // Now that we have projects, add all the repos from repo contributors that need a Project - // - allProjects.forEach { project -> - pluginInfo.repoContributors.forEach { - it.reposFor(project).forEach { - Kobalt.addRepo(it) - } - } - } - - // - // Run all the dependencies through the IDependencyInterceptors - // - runClasspathInterceptors(allProjects) - - log(2, "Final list of repos:\n " + Kobalt.repos.joinToString("\n ")) - - // - // Call apply() on all plug-ins now that the repos are set up - // - plugins.applyPlugins(Kobalt.context!!, allProjects) - - return allProjects - } - - private fun displayTasks() { - // - // List of tasks, --tasks - // - val tasksByPlugins = HashMultimap.create() - taskManager.annotationTasks.forEach { - tasksByPlugins.put(it.plugin.name, it) - } - val sb = StringBuffer("List of tasks\n") - tasksByPlugins.keySet().forEach { name -> - sb.append("\n " + AsciiArt.horizontalDoubleLine + " $name " - + AsciiArt.horizontalDoubleLine + "\n") - tasksByPlugins[name].distinctBy { - it.name - }.sortedBy { - it.name - }.forEach { task -> - sb.append(" ${task.name}\t\t${task.doc}\n") - } - } - - println(sb.toString()) - } - - private fun runClasspathInterceptors(allProjects: List) { - allProjects.forEach { - runClasspathInterceptors(it, it.compileDependencies) - runClasspathInterceptors(it, it.compileProvidedDependencies) - runClasspathInterceptors(it, it.compileRuntimeDependencies) - runClasspathInterceptors(it, it.testProvidedDependencies) - runClasspathInterceptors(it, it.testDependencies) - } - } - - private fun runClasspathInterceptors(project: Project, dependencies: ArrayList) - = with(dependencies) { - if (pluginInfo.classpathInterceptors.size > 0) { - val deps = interceptDependencies(project, pluginInfo, this) - clear() - addAll(deps) - } else { - this - } - } - - private fun interceptDependencies(project: Project, pluginInfo: PluginInfo, - dependencies: ArrayList) : ArrayList { - val result = arrayListOf() - pluginInfo.classpathInterceptors.forEach { - result.addAll(it.intercept(project, dependencies)) - } - return result - } - - private fun findBuildFile() : File { - val deprecatedLocation = File(Constants.BUILD_FILE_NAME) - val result: File = - if (deprecatedLocation.exists()) { - warn(Constants.BUILD_FILE_NAME + " is in a deprecated location, please move it to " - + Constants.BUILD_FILE_DIRECTORY) - deprecatedLocation - } else { - File(KFiles.joinDir(Constants.BUILD_FILE_DIRECTORY, Constants.BUILD_FILE_NAME)) - } - return result - } } 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