diff options
author | Christopher Vogt <oss.nsp@cvogt.org> | 2016-11-07 02:21:50 -0500 |
---|---|---|
committer | Christopher Vogt <oss.nsp@cvogt.org> | 2016-11-07 02:21:50 -0500 |
commit | c6b9a480879c101028b20b9cc8716b8ffa773630 (patch) | |
tree | fff15c1d6595455f6994f5793b63b08c5bf24d4a | |
parent | c89f87c9c9a0c7b256f225e37c55cb34f060aa6c (diff) | |
parent | fd849d293448d55c6bcb6f8440f44838b51fc860 (diff) | |
download | cbt-c6b9a480879c101028b20b9cc8716b8ffa773630.tar.gz cbt-c6b9a480879c101028b20b9cc8716b8ffa773630.tar.bz2 cbt-c6b9a480879c101028b20b9cc8716b8ffa773630.zip |
Merge remote-tracking branch 'origin/master' into integrate-eval
188 files changed, 8501 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37ff16e --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +config +cache +classes +lib +out +scala_classes +target/ +*.login +cache_ +.idea +realpath/realpath +node_modules +*fastopt* +*fullopt* diff --git a/DEVELOPER_GUIDE.txt b/DEVELOPER_GUIDE.txt new file mode 100644 index 0000000..268852e --- /dev/null +++ b/DEVELOPER_GUIDE.txt @@ -0,0 +1,41 @@ +Welcome developer. + +CBT has a very easy code base that's easy to master. +Don't shy away from submiting PRs :). And because CBT bootstraps from source +you already have the code there. + +The only tricky parts are class loading and cache invalidation. Most changes +will not need to interfere with this though. + +The ./cbt bash script starts the process. + +You currently need javac, nailgun, gpg and realpath or gcc installed. + +If you have any troubles with class not found, method not found, +abstract method error, NullPointerException, etc. +To restart nailgun try `killall -KILL java` or `kill -kill (jps|grep nailgun|cut -f1 -d " " -)`. +Or try `cbt kill` or `cbt direct <taskname>` to circumvent nailgun. +It can also help to delete all target folders `find .|grep target\$|xargs rm -rf` +inside of CBT. Or (almost never) the `cache/` directory. + +To edit/debug CBT in IntelliJ, add the whole directory as a new scala project. +Add the source folders manually and exclude the nested target folders. + +CBT's directory structure + +cbt Shell script launching cbt. Can be symlinked. +compatibility/ Java interfaces that all CBT versions are source compatible to. For communication + between composed builds of different versions. +nailgun_launcher/ Self-contained helper that allows using Nailgun with minimal permanent classpath. (Is this actually needed?) +realpath/ Self-contained realpath source code to correctly figure our CBTs home directory. (Open for replacement ideas.) +stage1/ CBT's code that only relies only on Scala/Java built-ins. Contains a Maven resolver to download libs for stage2. +stage2/ CBT's code that requires additional libs, e.g. barbary watchservice. +test/ Unit tests that can serve as example builds +sonatype.login Sonatype credentials for deployment. Not in git obviously. + +CBT follows an optimistic merging approach. (See http://hintjens.com/blog:106). +We strongly suggest well polished PRs, but don't want to stall improvements +by discussions about minor flaws. As long as the PR does not break anything and +improves the product, we should merge it, polishing remaining things afterwards. + +On OSX `brew install coreutils` to have gdate and get nanosecond timings during bash script. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d2cc0ea --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright 2016 Jan Christopher Vogt + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d471bb --- /dev/null +++ b/README.md @@ -0,0 +1,457 @@ +[![Join us on gitter](http://badges.gitter.im/cvogt/cbt.png)](https://gitter.im/cvogt/cbt) + +(For a tutorial scroll down.) + +Chris' Build Tool (CBT) for Scala +============================================ + +Easy to learn and master, lightning fast and backed by a thriving community of enthusiasts and contributors. For talks, development roadmap, projects using cbt, etc see the wiki. + +What is CBT? +---------------------------------------------- +CBT is a build tool meaning it helps orchestrating compilation, code and +documentation generation, packaging, deployment and custom tooling for +your project. It mainly targets Scala projects but is not exclusive to them. + +CBT builds are full programs written using vanilla Scala code. +Familiar concepts make you feel right at home - build files are classes, +tasks are defs, you customize using method overrides. You already know these +things and everything behaves as expected. That way implementing any +build related requirement becomes as easy as writing any other Scala code. + +CBT is simple in the sense that it uses very few concepts. +A single build uses classes, defs and inheritance. +Builds and binary dependencies can be composed to model +modules depending on each other. + +CBT believes good integration with existing tools to be very +helpful. In that spirit CBT aims for excellent integration with +the command line and your shell. + +CBT considers source files to be an excellent way to distribute code +and has first class support for source and git dependencies. + +How is CBT different from other build tools? +---------------------------------------------- +Not all build tools allow you to write builds in a full programming language. +CBT is based on the assumption that builds are complex enough problems to +warrant this and abstraction and re-use is better handled through libraries +rather than some restricted, declarative DSL. CBT shares this philosophy with SBT. +(This also means that integration with external tools such as an IDE better happens +programmatically through an api rather than a static data representation such as xml.) + +Like SBT, CBT chooses Scala as its language of choice, trying to appeal to +Scala programmers allowing them to re-use their knowledge and the safety of the language. + +Unlike SBT 0.11 and later, CBT maps task execution to JVM method invocations. +SBT implements its own self-contained task graph model and interpreter. +This allows SBT to have its model exactly fit the requirements. +CBT instead uses existing JVM concepts for the solution and adds +custom concepts only when necessary. CBT assumes this to lead to better +ease of use due to familarity and better integration with existing tools +such as interactive debuggers or stack traces because CBT's task call stack +IS the JVM call stack. + +SBT 0.7 shared this design decision as many may have forgotten. +However, CBT is still quite a bit simpler than even SBT 0.7 as +CBT gets away with fewer concepts. SBT 0.7 had notions of +main vs test sources, multi-project builds, task-dependencies +which weren't invocations and other concepts. CBT maps all of +these to def invocations and build composition instead. + +System requirements +------------------- + +CBT is best tested under OSX. People are also using it also under +Ubuntu and Windows via cygwin. It should be easy to port CBT to +other systems or drop the cygwin requirement. You will only +have to touch the launcher bash or .bat scripts. +Please contribute back if you fixed something :). + +You currently need javac and realpath or gcc installed. +nailgun is optional for speedup. gpg is required only for publishing maven artifacts. + +Features +-------- + +CBT supports the basic needs for Scala builds right now: +Compiling, running, testing, packaging, publishing local and to sonatype, +scaladoc, maven dependencies, source dependencies (e.g. for modularized projects), +triggering tasks on file changes, cross-compilation, reproducible builds. + +There is also a growing number of plugins in `plugins/` and `stage2/plugins/`, +but some things you'd like may still be missing. Consider writing +a plugin in that case. It's super easy, just a trait. Share it :). + + +Tutorial +-------- + +This section explains how to get started with cbt step-by-step. +There are also example projects with build files in examples/ and test/. + +### Installation + +If you haven't cloned cbt yet, clone it now. Cloning is how you install cbt. +We know that's a bit unusual, but roll with it, there are good reasons :). +Open a shell, cd to the directory where you want to install cbt and execute: + +``` +$ git clone git@github.com:cvogt/cbt.git +``` + +There are a bash script `cbt` and a `cbt.bat` in the checkout directory. +Add one to your `$PATH`, e.g. symlink it from `~/bin/cbt`. + +Check that it works by calling `cbt`. You should see CBT compiling itself +and showing a list of built-in tasks. + +Great, you're all set up. Now, let's use cbt for a new example project. +Follow the below steps. (There is also an experimental GUI described +later to create a project, but going through the steps this time will help +you understand what exactly is going on.) + +### Creating your first project + +Create a new directory and cd into it. E.g. `my-project`. + +``` +$ mkdir my-project +$ cd my-project +``` + +Let's create a tiny sample app. CBT can generate it for you. Just run: + +``` +$ cbt tools createMain +``` + +### Running your code + +Now there should be a file `Main.scala`, which prints `Hello World` when run. +So let's run it: + +``` +$ cbt run +``` + +You should see how CBT first compiles your project, then runs it and prints +`Hello World`. CBT created the file `Main.scala` top-level in your directory. +You can alternatively place `.scala` or `.java` files in `src/` +or any of its subdirectories. + +### Creating a build file + +Without a build file, CBT just uses some default build settings. +Let's make the build more concrete by creating a build file. + +CBT can help you with that. Execute: + +``` +$ cbt tools createBuild +``` + +Now there should be a file `build/build.scala` with a sample `Build` class. + +Btw., a build file can have its own build and so on recursively like in SBT. +When you create a file `build/build/build.scala` and change `Build` class in there +to extend `BuildBuild`, it will be used to build your `build/build.scala`. You can +add built-time dependencies like plugins this way. + +### Adding dependencies + +In the generated `build/build.scala` there are +several examples for dependencies. We recommend using the constructor syntax +`ScalaDependency` (for automatically adding the scala version to the artifact id) +or `MavenDependency` (for leaving the artifact id as is). The SBT-Style `%`-DSL +syntax is also supported for copy-and-paste convenience, but discouraged. + +Alright, let's enable the `override def dependencies`. Make sure to include +`super.dependencies`, which currently only includes the Scala standard library. +Add a dependency of your choice, start using it from `Main.scala` and `cbt run` again. + +As you can see CBT makes choice of the maven repository explicit. It does so for clarity. + +### Calling other tasks + +Tasks are just defs. You can call any public zero-arguments method of your +`Build class or its parents straight from the command line. To see how it works +let's call the compile task. + +``` +$ cbt compile +``` + +### Creating custom tasks + +In order to create a custom task, simply add a new def to your Build class, e.g. + +``` +class Build...{ + ... + def foo = "asdf" +} +``` + +Now call the def from the command line: + +``` +$ cbt foo +``` + +As you can see it prints `asdf`. Adding tasks is that easy. + +### Triggering tasks on file-changes + +When you call a task, you can prefix it with `loop`. +CBT then watches the source files, the build files and even CBT's own +source code and re-runs the task when anything changes. If necessary, +this forces CBT to re-build itself, the project's dependencies and the project itself. + +Let's try it. Let's loop the run task. Call this from the shell: + +``` +$ cbt loop run +``` + +Now change `Main.scala and see how cbt picks it up and re-runs it. +CBT is fast. It may already be done re-compiling and re-running before +you managed to change windows back from your editor to the shell. + +Try changing the build file and see how CBT reacts to it as well. + +### Adding tests + +The simplest way to add tests is putting a few assertions into the previously +created Main.scala and be done with it. Alternatively you can add a test +framework plugin to your build file to use something more sophisticated. + +This however means that the class files of your tests will be included in the +jar should you create one. If that's fine, you are done :). If it is not you +need to create another project, which depends on your previous project. This +project will be packaged separately or you can disable packaging there. Let's create +such a project now. + +Your project containing tests can be anywhere but a recommended location is a +sub-folder called `test/` in your main project. Let's create it and create a +Main class and build file: + +``` +$ mkdir test +$ cd test +$ rm ../Main.scala +$ cbt tools createMain +$ cbt tools createBuild +``` + +We also deleted the main projects Main.scala, because now that we created a new one +we would have two classes with the same name on the classpath which can be very confusing. + +Now that we have a Main file in our test project, we can add some assertions to it. +In order for them to see the main projects code, we still need to do one more thing - +add a `DirectoryDependency` to your test project's build file. There is a similar example +in the generated build.scala. What you need is this: + +``` +override def dependencies = super.dependencies ++ Seq( + DirectoryDependency( projectDirectory ++ "/.." ) +) +``` + +This successfully makes your test project's code see the main projects code. +Add some class to your main project, e.g. `case class Foo(i: Int = 5)`. Now +put an assertion into the Main class of your test project, e.g. +`assert(Foo().i == 5)` and hit `cbt run` inside your test project. + +Make sure you deleted your main projects class Main when running your tests. + +Congratulations, you successfully created a dependent project and ran your tests. + +### Multi-projects Builds + +A single build only handles a single project in CBT. So there isn't exactly +such a things as a Multi-project Build. Instead you can simply write multiple +projects that depend on each other. We have already done that with tests above, +but you can do the exact same thing to modularize your project into multiple ones. + +### Reproducible builds + +To achieve reproducible builds, you'll need to tie your build files to a particular +CBT-version. It doesn't matter what version of CBT you are actually running, +as long as the `BuildInterface` is compatible (which should be true for a large number +of versions and we may find a better solution long term. If you see a compile error +during compilation of CBT itself that some method in BuildInterface was not +implemented or incorrectly implemented, you may be running an incompatible CBT +version. We'll try to fix that later, but for now you might have to checkout +the required hash of CBT by hand.). + +When you specify a particular version, CBT will use that one instead of the installed one. + +You can specify one by adding one line right before `class Build`. It looks like this: + +``` +// cbt:https://github.com/cvogt/cbt.git#75c32537cd8f29f9d12db37bf06ad942806f02393 +class Build... +``` + +The URL points to any git repository containing one of CBT's forks. You currently +have to use a stable reference - i.e. a hash or tag. (Checkouts are currently not +updated. If you refer to a branch or tag which is moved on the remote, CBT +will not realize that and keep using the old version). + +### Using CBT like a boss + +Do you own your Build Tool or does your Build Tool own you? CBT makes it easy for YOU +to be in control. We try to work on solid documentation, but even good +documentation never tells the whole truth. Documentation can tell how to use +something and why things are happening, but only the code can tell all the +details of what exactly is happening. Reading the code can be intimidating for +many Scala libraries, but not so with CBT. The source code is easy to read +to the point that even Scala beginners will be able to understand it. So +don't be afraid to actually look under the hood and check out what's happening. + +And guess what, you already have the source code on your disk, because +you installed CBT by cloning its git repository. You can even debug CBT and +your build files in an interactive debugger like IntelliJ after some minor setup. + +Finally, you can easily change CBT's code. Then CBT re-builds itself when you try +to use it the next time. This means any changes you make are instantly reflected. +This and the simple code make it super easy to fix bugs or add features yourself +and feed them back into main line CBT. + + +When debugging things, it can help to enable CBT's debug logging by passing +`-Dlog=all` to CBT (or a logger name instead of `all`). + + +Other design decisions +-------------------- + +CBT tries to couple its code very loosely. OO is used for configuration in build files. +Interesting logic is in simple supporting library classes/objects, which can be used +independently. You could even build a different configuration api than OO on top of them. + + +Known limitations +-------------------------- +- currently CBT supports no generic task scoping. A solution is known, but not implemented. + For now manually create intermediate tasks which serve as scoping for + known situations and encode the scope in the name, e.g. fastOptScalaJsOptions +- currently CBT supports no dynamic overrides of tasks. A solution is known, but not implemented. + scalaVersion and version are passed through the context instead for dynamic overrides. +- there is currently no built-in support for resources being added to a jar. + Should be simple to add, consider a PR +- test framework support is currently a bit spotty but actively being worked on +- concurrent task and build execution is currently disabled +- CBT uses its own custom built maven resolver, which is really fast, + but likely does not work in some edge cases. Those may or may not be easy to fix. + We should add optional coursier integration back for a more complete solution. + +Known bugs +------------- +- currently there is a bug in CBT where dependent builds may miss changes in the things + they depend on. Deleting all target directories and starting from scratch helps. +- There are some yet unknown bugs which can be solved by killing the nailgun background process + and/or re-running the desired cbt task multiple times until it succeeds. +- if you ever see no output from a command but expect some, make sure you are in the right directory + and try to see if any of the above recommendations help + + +Shell completions +------------------- + +### Bash completions +To auto-complete cbt task names in bash do this: + +``` +mkdir ~/.bash_completion.d/ +cp shell-integration/cbt-completions.bash ~/.bash_completion.d/ +``` + +Add this to your .bashrc +``` +for f in ~/.bash_completion.d/*; do + source $f +done +``` + +### Fish shell completions + +copy this line into your fish configuration, on OSX: `/.config/fish/config.fish` + +``` +complete -c cbt -a '(cbt taskNames)' +``` + +### Zsh completions + +##### Manual installation +Add the following to your `.zshrc` +``` +source /path/to/cbt/shell-integration/cbt-completions.zsh +``` +##### oh-my-zsh +If using oh-my-zsh, you can install it as a plugin: +``` +mkdir ~/.oh-my-zsh/custom/plugins/cbt +cp shell-integration/cbt-completions.zsh ~/.oh-my-zsh/custom/plugins/cbt/cbt.plugin.zsh +``` +Then enable it in your `.zshrc`: +``` +plugins=( ... cbt) +``` +Experimental GUI +-------------------- + +### Creating a project via the GUI + +`cd` into the directory inside of which you want to create a new project directory and run `cbt tools gui`. + +E.g. + +``` +$ cd ~/my-projects +$ cbt tools gui +``` + +This should start UI server at http://localhost:9080. There you can create Main class, CBT build, +add libraries, plugins, readme and other things. Let's say you choose `my-project` as the project name. +The GUI will create `~/my-projects/my-project` for you. + + +Plugin-author guide +-------------------- + +A CBT plugin is a trait that is mixed into a Build class. +Only use this trait only for wiring things together. +Don't put logic in there. Instead simply call methods +on a separate class or object which serves as a library +for your actual logic. It should be callable and testable +outside of a Build class. This way the code of your plugin +will be easier to test and easier to re-use. Feel free +to make your logic rely on CBT's logger. + + See `plugins/` for examples. + +Scala.js support +---------------- + +CBT supports cross-project Scala.js builds. +It preserves same structure as in SBT (https://www.scala-js.org/doc/project/cross-build.html) + + 1. Example for user scalajs project is in: `$CBT_HOME/cbt/examples/build-scalajs` + 2. `$CBT_HOME/cbt compile` + Will compile JVM and JS sources + `$CBT_HOME/cbt jsCompile` + Will compile JS sources + `$CBT_HOME/cbt jvmCompile` + Will compile JVM sources + 3. `$CBT_HOME/cbt fastOptJS` and `$CBT_HOME/cbt fullOptJS` + Same as in Scala.js sbt project + + Note: Scala.js support is under ongoing development. + + Currently missing features: + * No support for jsDependencies: + It means that all 3rd party dependencies should added manually, see scalajs build example + * No support for test diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..b6f4c15 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,34 @@ +TODO: + - in progress + - improve logging + + - immediate features + - fix main project main method being run during tests + - investigate and solve multiple compilations of the same SourceDependency Build. Maybe introduce global Build map. + + - cleanup + - move from java File to nio Path + + - near future features + - make cbt's own re-build concurrency safe + - unify with sbts key names where sensible + - allow updating snapshots + - cbt cli options inject add dependencies into default build + - dependency exclusion, etc. + - use cli friendly responses by default everywhere + - class path debugging + - broken jars detection + - invalid files in lib folder + - integrate / build out maven search + - use zinc nailgun multi platform nailgun wrapper https://github.com/typesafehub/zinc/tree/7af98ba11d27d7667301c2222c1e702c7092bc44/src/universal/bin + + + - future features + - loop compiling with cancelling running runs/compiles + - shell tab completion + - maybe scripts for bash/zsh/fish + - maybe interactive shell + - maybe one that exists immediately after execution + + - potential features + - running in-project tasks in parallel using Monad diff --git a/build/build.scala b/build/build.scala new file mode 100644 index 0000000..8770ef6 --- /dev/null +++ b/build/build.scala @@ -0,0 +1,31 @@ +import cbt._ + +class Build(val context: Context) extends Publish{ + // FIXME: somehow consolidate this with cbt's own boot-strapping from source. + override def dependencies = { + super.dependencies ++ Resolver(mavenCentral).bind( + MavenDependency("net.incongru.watchservice","barbary-watchservice","1.0"), + MavenDependency("org.eclipse.jgit", "org.eclipse.jgit", "4.2.0.201601211800-r"), + MavenDependency("com.typesafe.zinc","zinc","0.3.9"), + ScalaDependency("org.scala-lang.modules","scala-xml","1.0.5") + ) + } + override def sources = Seq( + "nailgun_launcher", "stage1", "stage2", "compatibility" + ).map(d => projectDirectory ++ ("/" + d)) + + def groupId: String = "org.cvogt" + + def defaultVersion: String = "0.1" + def name: String = "cbt" + + // Members declared in cbt.Publish + def description: String = "Fast, intuitive Build Tool for Scala" + def developers: Seq[cbt.Developer] = Nil + def inceptionYear: Int = 2016 + def licenses: Seq[cbt.License] = Seq( License.Apache2 ) + def organization: Option[cbt.Organization] = None + def scmConnection: String = "" + def scmUrl: String = "" + def url: java.net.URL = new java.net.URL("http://github.com/cvogt/cbt/") +} @@ -0,0 +1,240 @@ +#!/usr/bin/env bash +# Launcher bash script that bootstraps CBT from source. +# (Some of the code for reporting missing dependencies and waiting for nailgun to come up is a bit weird.) +# This is intentionally kept as small as possible. +# Welcome improvements to this file: +# - reduce code size through better ideas +# - reduce code size by moving more of this into type-checked Java/Scala code (if possible without performance loss). +# - reduction of dependencies +# - performance improvements +shopt -qs extglob + +seconds() { + date +"%s" +} + +nanos() { + n=$(date +"%N") + if [ "$n" = "N" ]; then + n=$(gdate +"%N" 2>/dev/null) + fi + if [ "$n" = "" ]; then + n="0" + fi + echo $n +} + +start_seconds=$(seconds) +start_nanos=1$(nanos) + +time_taken() { + i=$(( $(seconds) - start_seconds )) + n=$(( $(( 1$(nanos) - start_nanos )) / 1000000 )) + if [[ ( "$n" < 0 ) ]]; then + i=$(( i-1 )) + n=$(( n+1000 )) + fi + echo "$i.$n" +} + +# utility function to log message to stderr with stating the time +log () { + msg=$1 + enabled=1 + while test $# -gt 0; do + case "$1" in + "-Dlog=time") enabled=0 ;; + "-Dlog=all") enabled=0 ;; + esac + shift + done + if [ $enabled -eq 0 ]; then + delta=$(time_taken) + echo "[$delta] $msg" 1>&2 + fi +} + +log "Checking for dependencies" $* + +which javac 2>&1 > /dev/null +javac_installed=$? +if [ ! $javac_installed -eq 0 ]; then + echo "You need to install javac 1.7 or later! CBT needs it to bootstrap from Java sources into Scala." 1>&2 + exit 1 +fi + +# log "cutting javac version" $* +# javac_version=$(javac -version 2>&1) # e.g. "javac 1.8.0_u60" +# javac_version_update=${javac_version/javac 1./} # e.g. "8.0_u60" +# javac_version_minor_pointed=${javac_version_update%_*} # e.g. "8.0" +# javac_version_minor=${javac_version_minor_pointed%.*} # e.g. "8" +# log "cutting javac version done" $* +# if [ ! "$javac_version_minor" -ge "7" ]; then +# echo "You need to install javac version 1.7 or greater!" 2>&1 +# echo "Current javac version is $javac_version" 2>&1 +# exit 1 +# fi + +NG_EXECUTABLE=$(which ng || which ng-nailgun) +NG_SERVER=$(which ng-server || ls /usr/share/java/nailgun-server-*.jar 2>/dev/null | awk '{print "java -jar " $0}') +nailgun_installed=0 +if [ "$NG_EXECUTABLE" == "" ] || [ "$NG_SERVER" == "" ]; then + nailgun_installed=1 + echo "(Note: nailgun not found. It makes CBT faster! Try 'brew install nailgun' or 'apt install nailgun'.)" 1>&2 +fi +which realpath 2>&1 > /dev/null +realpath_installed=$? +which gcc 2>&1 > /dev/null +gcc_installed=$? +if [ ! $realpath_installed -eq 0 ] && [ ! $gcc_installed -eq 0 ]; then + echo "You need realpath or gcc installed! CBT needs it to locate itself reliably." 1>&2 + exit 1 +fi + +which gpg 2>&1 > /dev/null +gpg_installed=$? +if [ ! $gpg_installed -eq 0 ]; then + echo "(Note: gpg not found. In order to use publishSigned you'll need it.)" 1>&2 +fi + +NAILGUN_PORT=4444 +NG="$NG_EXECUTABLE --nailgun-port $NAILGUN_PORT" + +CWD=$(pwd) +_DIR=$(dirname $(readlink "$0") 2>/dev/null || dirname "$0" 2>/dev/null ) + +log "Find out real path. Build realpath if needed." $* + +export CBT_HOME=$(dirname $($_DIR/realpath/realpath.sh $0)) + +export NAILGUN=$CBT_HOME/nailgun_launcher/ +export TARGET=target/scala-2.11/classes/ +mkdir -p $NAILGUN$TARGET + +nailgun_out=$NAILGUN/target/nailgun.stdout.log +nailgun_err=$NAILGUN/target/nailgun.strerr.log +foo(){ + while test $# -gt 0; do + case "$1" in + "-Dlog=nailgun") + nailgun_out=/dev/stderr + nailgun_err=/dev/stderr + ;; + "-Dlog=all") + nailgun_out=/dev/stderr + nailgun_err=/dev/stderr + ;; + esac + shift + done +} + +foo $@ + +if [ "$1" = "kill" ]; then + echo "Stopping background process (nailgun)" 1>&2 + $NG ng-stop >> $nailgun_out 2>> $nailgun_err & + exit 1 +fi + +which nc 2>&1 > /dev/null +nc_installed=$? + +log "Check for running nailgun with nc." $* + +server_up=1 +if [ $nc_installed -eq 0 ]; then + nc -z -n -w 1 127.0.0.1 $NAILGUN_PORT > /dev/null 2>&1 + server_up=$? +else + echo "(Note: nc not found. It will make slightly startup faster.)" 1>&2 +fi + +use_nailgun=0 +if [ $nailgun_installed -eq 1 ] || [ "$1" = "publishSigned" ] || [ "$2" = "publishSigned" ] || [ "$1" = "direct" ] || [ "$2" = "direct" ]; then + use_nailgun=1 +fi + +if [ $use_nailgun -eq 0 ] && [ ! $server_up -eq 0 ]; then + log "Starting background process (nailgun)" $* + # try to start nailgun-server, just in case it's not up + $NG_SERVER 127.0.0.1:$NAILGUN_PORT >> $nailgun_out 2>> $nailgun_err & +fi + +stage1 () { + log "Checking for changes in cbt/nailgun_launcher" $* + NAILGUN_INDICATOR=$NAILGUN$TARGET/cbt/NailgunLauncher.class + changed=0 + for file in `ls $NAILGUN/*.java`; do + if [ $file -nt $NAILGUN_INDICATOR ]; then changed=1; fi + done + compiles=0 + if [ $changed -eq 1 ]; then + #rm $NAILGUN$TARGET/cbt/*.class 2>/dev/null # defensive delete of potentially broken class files + echo "Compiling cbt/nailgun_launcher" 1>&2 + javac -Xlint:deprecation -Xlint:unchecked -d $NAILGUN$TARGET `ls $NAILGUN*.java` + compiles=$? + if [ $compiles -ne 0 ]; then + rm $NAILGUN$TARGET/cbt/*.class 2>/dev/null # triggers recompilation next time. + break + fi + if [ $use_nailgun -eq 0 ]; then + echo "Stopping background process (nailgun)" 1>&2 + $NG ng-stop >> $nailgun_out 2>> $nailgun_err & + sleep 1 + echo "Restarting background process (nailgun)" 1>&2 + ng-server 127.0.0.1:$NAILGUN_PORT >> $nailgun_out 2>> $nailgun_err & + sleep 1 + fi + fi + + log "run CBT and loop if desired. This allows recompiling CBT itself as part of compile looping." $* + + if [ $use_nailgun -eq 1 ] + then + log "Running JVM directly" $* + # -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=localhost:5005 + # JVM options to improve startup time. See https://github.com/cvogt/cbt/pull/262 + java $JAVA_OPTS -Xmx6072m -Xss10M -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Xverify:none -cp $NAILGUN$TARGET cbt.NailgunLauncher $(time_taken) "$CWD" $* + else + log "Running via background process (nailgun)" $* + for i in 0 1 2 3 4 5 6 7 8 9; do + log "Adding classpath." $* + $NG ng-cp $NAILGUN$TARGET >> $nailgun_out 2>> $nailgun_err + log "Checking if nailgun is up yet." $* + $NG cbt.NailgunLauncher check-alive >> $nailgun_out 2>> $nailgun_err + alive=$? + if [ $alive -eq 131 ] || [ $alive -eq 33 ]; then + # the 33 is not working right now + # echo "Nailgun call failed. Try 'cbt kill' and check the error log cbt/nailgun_launcher/target/nailgun.stderr.log" 1>&2 + #elif [ $alive -eq 33 ]; then + break + else + log "Nope. Sleeping for 0.5 seconds" $* + #if [ "$i" -gt 1 ]; then + # echo "Waiting for nailgun to start... (In case of problems try -Dlog=nailgun or check logs in cbt/nailgun_launcher/target/*.log)" 1>&2 + #fi + fi + sleep 0.3 + done + log "Running CBT via Nailgun." $* + $NG cbt.NailgunLauncher $(time_taken) "$CWD" $* + fi + exitCode=$? + log "Done running CBT." $* +} + +while true; do + stage1 $* + if [ ! "$1" = "loop" ]; then + break + fi + echo "======= Restarting CBT =======" 1>&2 +done + +if [ $compiles -ne 0 ]; then + exitCode=1 +fi + +log "Exiting CBT" $* +exit $exitCode @@ -0,0 +1,196 @@ +@ECHO OFF
+REM Launcher bash script that bootstraps CBT from source.
+REM (Some of the code for reporting missing dependencies and waiting for nailgun to come up is a bit weird.)
+REM This is inentionally kept as small as posible.
+REM Welcome improvements to this file:
+REM - reduce code size through better ideas
+REM - reduce code size by moving more of this into type-checked Java/Scala code (if possible without performance loss).
+REM - reduction of dependencies
+REM - performance improvements
+
+REM utility function to log message to stderr with stating the time
+setlocal EnableExtensions EnableDelayedExpansion
+
+where javac > nul 2>&1
+
+if ERRORLEVEL 1 (
+ ECHO You need to install javac! CBT needs it to bootstrap from Java sources into Scala.
+ GOTO :EOF
+)
+
+javac -version > %CBT_HOME%temp.txt 2>&1
+SET /p javac_version_output=<%CBT_HOME%temp.txt
+
+FOR /f "tokens=2" %%G IN ("%javac_version_output%") DO SET javac_version=%%G
+
+FOR /f "tokens=2 delims=." %%G IN ("%javac_version%") DO SET javac_version_minor=%%G
+
+IF javac_version_minor LSS 8 (
+ echo You need to install javac version 1.7 or greater!
+ echo Current javac version is %javac_version
+ GOTO :EOF
+)
+
+SET nailgun_installed=0
+
+where ng > nul 2>&1
+
+if ERRORLEVEL 1 (
+ ECHO You need to install Nailgun to make CBT slightly faster. > nul 2>&1
+) ELSE ( SET nailgun_installed=1 )
+
+where ng-server > nul 2>&1
+
+if ERRORLEVEL 1 (
+ ECHO You need to install Nailgun Server to make CBT slightly faster.
+) ELSE ( SET nailgun_installed=1 )
+
+IF %nailgun_installed%==0 ECHO Note: nailgun not found. It makes CBT faster! > nul 2>&1
+
+where gpg > nul 2>&1
+SET gpg_installed=0
+if ERRORLEVEL 1 (
+ ECHO You need to install gpg for login.
+) ELSE ( SET gpg_installed=0 )
+
+
+
+SET NAILGUN_PORT=4444
+SET NG=ng --nailgun-port %NAILGUN_PORT%
+
+for /f "delims=" %%i in ('chdir') do SET CWD=%%i
+
+SET CBT_HOME=%~dp0
+
+SET SCALA_VERSION=2.11.8
+
+SET NAILGUN=%CBT_HOME%nailgun_launcher\
+SET STAGE1=%CBT_HOME%stage1\
+SET TARGET=target\scala-2.11\classes\
+
+mkdir %NAILGUN%%TARGET% > nul 2>&1
+mkdir %STAGE1%%TARGET% > nul 2>&1
+
+SET nailgun_out=%NAILGUN%\target\nailgun.stdout.log
+SET nailgun_err=%NAILGUN%\target\nailgun.strerr.log
+
+where nc > nul 2>&1
+
+SET nc_installed=1
+SET server_up=0
+
+IF ERRORLEVEL 1 (
+ ECHO Note: nc not found. It will make slightly startup faster.
+) ELSE (
+ nc -z -n -w 1 127.0.0.1 %NAILGUN_PORT% > nul 2>&1
+ SET server_up=1
+)
+
+IF "%1%"=="kill" (
+ echo Stopping nailgun 1>&2
+ %NG% ng-stop >> %nailgun_out 2>> %nailgun_err%
+ GOTO :EOF
+)
+
+SET use_nailgun=1
+
+SET res=0
+IF %nailgun_installed%==0 SET res=1
+IF "%1%"=="publishSigned" SET res=1
+IF "%2%"=="publishSigned" SET res=1
+IF "%1%"=="direct" SET res=1
+IF "%2%"=="direct" SET res=1
+
+IF %res%==1 SET use_nailgun=0
+
+REM IF NOT %server_up%==1
+REM >> %nailgun_out 2>> %nailgun_err
+IF %use_nailgun%==1 (
+ REM try to start nailgun-server, just in case it's not up
+ START ng-server 127.0.0.1:%NAILGUN_PORT%
+)
+
+:RUN
+ CALL :stage1 %*
+ IF NOT "%1%"=="loop" (
+ GOTO :EOF
+ ) ELSE (
+ GOTO :RUN
+ echo "======= Restarting CBT =======" 1>&2
+ )
+
+:stage1
+SETLOCAL
+SET NAILGUN_INDICATOR=%NAILGUN%%TARGET%cbt\NailgunLauncher.class
+SET changed=0
+
+IF EXIST %NAILGUN_INDICATOR% (
+ REM this should recompile nailgun_launcher/ if any .java file is newer than NailgunLauncher.class, FIXME doesn't work
+ FOR %%i IN (%NAILGUN%*.java) DO (
+ FOR /F %%z IN ('DIR /B /O:D %%i%% %NAILGUN_INDICATOR% 2^> nul') DO SET NEWEST=%%z
+ if "%NEWEST:~-4%"=="java" ( SET changed=1 )
+ )
+) ELSE (
+ SET changed=1
+)
+
+IF %changed%==1 (
+ REM defensive delete of potentially broken class files
+ REM DEL /s /q /f %NAILGUN%%TARGET%cbt\*.class > nul 2>&1
+
+ echo Compiling cbt/nailgun_launcher
+
+ dir /B /A:-D %NAILGUN%\*.java > %CBT_HOME%temp.txt
+
+ FOR /F "tokens=*" %%j in (%CBT_HOME%temp.txt) do SET "files=!files! %NAILGUN%%%j"
+
+ javac -Xlint:deprecation -d %NAILGUN%%TARGET% !files!
+
+ IF ERRORLEVEL 1 (
+ REM triggers recompilation next time.
+ DEL %NAILGUN%TARGET/cbt/*.class 2> NUL REM triggers recompilation next time.
+ GOTO :eof
+ )
+
+ IF %use_nailgun%==0 (
+ REM echo "Stopping nailgun" 1>&2
+ %NG% ng-stop >> %nailgun_out% 2>> %nailgun_err%
+ REM echo "Restarting nailgun" 1>&2
+ ng-server 127.0.0.1:%NAILGUN_PORT% >> %nailgun_out% 2>> %nailgun_err%
+ )
+)
+
+REM TODO 0.0 should be replaced by actual spent time, see ./cbt
+IF %use_nailgun%==0 java %JAVA_OPTS% -Xmx6072m -Xss10M -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Xverify:none -cp %NAILGUN%%TARGET% cbt.NailgunLauncher 0.0 %CWD% %*
+IF %use_nailgun%==0 GOTO :ENDIF
+
+SET /A counter=0
+:BEGINFOR
+REM ECHO %counter%
+IF /I "%counter%" EQU "10" GOTO :ENDFOR
+%NG% ng-cp %NAILGUN%%TARGET%
+%NG% cbt.NailgunLauncher check-alive
+%NG% ng-cp %NAILGUN%%TARGET% >> %nailgun_out% 2>> %nailgun_err%
+%NG% cbt.NailgunLauncher check-alive >> %nailgun_out% 2>> %nailgun_err%
+
+SET isAlive=0
+IF %errorlevel%==131 SET isAlive=1
+IF %errorlevel%==33 SET isAlive=1
+
+
+REM IF ERRORLEVEL 1 (
+ REM ECHO Waiting for nailgun
+REM ) ELSE (
+ REM GOTO :ENDFOR
+REM )
+
+SLEEP 2
+SET /A counter=%counter% + 1
+REM GOTO :BEGINFOR
+:ENDFOR
+%NG% cbt.NailgunLauncher %CWD% %*
+ENDLOCAL
+:ENDIF
+:ENDPROGRAM
+ENDLOCAL
+REM DEL %CBT_HOME%temp.txt > NUL 1>&2
\ No newline at end of file diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..1615ad5 --- /dev/null +++ b/circle.yml @@ -0,0 +1,17 @@ +machine: + java: + version: oraclejdk7 + +dependencies: + cache_directories: + - "cache" + override: + - ./cbt compile + - ./cbt direct + - ./cbt -Dlog=all + +test: + override: + - rm ~/.gitconfig # avoid url replacement breaking jgit + - ./cbt direct test + - ./cbt test diff --git a/compatibility/ArtifactInfo.java b/compatibility/ArtifactInfo.java new file mode 100644 index 0000000..a2e6006 --- /dev/null +++ b/compatibility/ArtifactInfo.java @@ -0,0 +1,7 @@ +package cbt; + +public interface ArtifactInfo extends Dependency{ + public abstract String artifactId(); + public abstract String groupId(); + public abstract String version(); +} diff --git a/compatibility/BuildInterface.java b/compatibility/BuildInterface.java new file mode 100644 index 0000000..f061832 --- /dev/null +++ b/compatibility/BuildInterface.java @@ -0,0 +1,10 @@ +package cbt; +import java.io.*; + +public abstract class BuildInterface implements Dependency{ + public abstract BuildInterface copy(Context context); // needed to configure builds + public abstract String scalaVersion(); // needed to propagate scalaVersion to dependent builds + public abstract String[] crossScalaVersionsArray(); // FIXME: this probably can't use Scala classes + public abstract BuildInterface finalBuild(); // needed to propagage through build builds. Maybe we can get rid of this. + public abstract File[] triggerLoopFilesArray(); // needed for watching files across composed builds +} diff --git a/compatibility/Context.java b/compatibility/Context.java new file mode 100644 index 0000000..921087f --- /dev/null +++ b/compatibility/Context.java @@ -0,0 +1,22 @@ +package cbt; +import java.io.*; +import java.util.concurrent.ConcurrentHashMap; + +// TODO: try to reduce the number of members +public abstract class Context{ + public abstract File projectDirectory(); + public abstract File cwd(); + public abstract String[] argsArray(); + public abstract String[] enabledLoggersArray(); + public abstract Long startCompat(); + public abstract Boolean cbtHasChangedCompat(); + public abstract String versionOrNull(); + public abstract String scalaVersionOrNull(); // needed to propagate scalaVersion to dependendee builds + public abstract ConcurrentHashMap<String,Object> permanentKeys(); + public abstract ConcurrentHashMap<Object,ClassLoader> permanentClassLoaders(); + public abstract File cache(); + public abstract File cbtHome(); + public abstract File cbtRootHome(); + public abstract File compatibilityTarget(); + public abstract BuildInterface parentBuildOrNull(); +} diff --git a/compatibility/Dependency.java b/compatibility/Dependency.java new file mode 100644 index 0000000..d491174 --- /dev/null +++ b/compatibility/Dependency.java @@ -0,0 +1,10 @@ +package cbt; +import java.io.*; + +public interface Dependency{ + public abstract String show(); + public abstract Boolean needsUpdateCompat(); + public abstract Dependency[] dependenciesArray(); + public abstract File[] dependencyClasspathArray(); + public abstract File[] exportedClasspathArray(); +} diff --git a/compatibility/Result.java b/compatibility/Result.java new file mode 100644 index 0000000..220aa3a --- /dev/null +++ b/compatibility/Result.java @@ -0,0 +1,11 @@ +/* +package cbt; +import java.io.*; +public interface Result<T>{ + public abstract Integer exitCode(); + public abstract OutputStream out(); + public abstract OutputStream err(); + public abstract InputStream in(); + public abstract T value(); +} +*/ diff --git a/coursier/Coursier.scala b/coursier/Coursier.scala new file mode 100644 index 0000000..8a66aee --- /dev/null +++ b/coursier/Coursier.scala @@ -0,0 +1,47 @@ +/* +package cbt +object Coursier{ + implicit class CoursierDependencyResolution(d: JavaDependency){ + import d._ + def resolveCoursier = { + import coursier._ + val repositories = Seq( + Cache.ivy2Local, + MavenResolver("https://repo1.maven.org/maven2") + ) + + val start = Resolution( + Set( + JavaDependency( + Module(groupId, artifactId), version + ) + ) + ) + + val fetch = Fetch.from(repositories, Cache.fetch()) + + + val resolution = start.process.run(fetch).run + + val errors: Seq[(JavaDependency, Seq[String])] = resolution.errors + + if(errors.nonEmpty) throw new Exception(errors.toString) + + import java.io.File + import scalaz.\/ + import scalaz.concurrent.Task + + val localArtifacts: Seq[FileError \/ File] = Task.gatherUnordered( + resolution.artifacts.map(Cache.file(_).run) + ).run + + val files = localArtifacts.map(_.toEither match { + case Left(error) => throw new Exception(error.toString) + case Right(file) => file + }) + + resolution.dependencies.map( d => cbt.JavaDependency(d.module.organization,d.module.name, d.version)).to[collection.immutable.Seq] + } + } +} +*/
\ No newline at end of file diff --git a/examples/build-info-example/BuildInfo.scala b/examples/build-info-example/BuildInfo.scala new file mode 100644 index 0000000..ac0e680 --- /dev/null +++ b/examples/build-info-example/BuildInfo.scala @@ -0,0 +1,8 @@ +// generated file +import java.io._ +object BuildInfo{ +def artifactId = "build-info-example" +def groupId = "cbt.examples" +def version = "0.1" +def scalaVersion = "2.11.8" +} diff --git a/examples/build-info-example/Main.scala b/examples/build-info-example/Main.scala new file mode 100644 index 0000000..cb4ad75 --- /dev/null +++ b/examples/build-info-example/Main.scala @@ -0,0 +1,9 @@ +object Main{ + def main(args: Array[String]): Unit = { + import BuildInfo._ + println("scalaVersion: "+scalaVersion) + println("groupId: "+groupId) + println("artifactId: "+artifactId) + println("version: "+version) + } +}
\ No newline at end of file diff --git a/examples/build-info-example/Readme.md b/examples/build-info-example/Readme.md new file mode 100644 index 0000000..acbb84e --- /dev/null +++ b/examples/build-info-example/Readme.md @@ -0,0 +1,3 @@ +This is an example how to propagate build-time information +such as version or scalaVersion to runtime. +The advantage of the approach taken here is simplicity. diff --git a/examples/build-info-example/build/build.scala b/examples/build-info-example/build/build.scala new file mode 100644 index 0000000..f6fc7a4 --- /dev/null +++ b/examples/build-info-example/build/build.scala @@ -0,0 +1,27 @@ +import cbt._ +import java.nio.file.Files._ + +class Build(val context: Context) extends PackageJars{ + def name = "build-info-example" + def groupId = "cbt.examples" + def defaultVersion = "0.1" + override def defaultScalaVersion = "2.11.8" + override def compile = { + val file = (projectDirectory ++ "/BuildInfo.scala").toPath + val contents = s"""// generated file +import java.io._ +object BuildInfo{ +def artifactId = "$artifactId" +def groupId = "$groupId" +def version = "$version" +def scalaVersion = "$scalaVersion" +} +""" + if( exists(file) && contents != new String(readAllBytes(file)) ) + write( + (projectDirectory ++ "/BuildInfo.scala").toPath, + contents.getBytes + ) + super.compile + } +} diff --git a/examples/dotty-example/README.md b/examples/dotty-example/README.md new file mode 100644 index 0000000..bc0f6b0 --- /dev/null +++ b/examples/dotty-example/README.md @@ -0,0 +1,3 @@ +Dotty example project compiling hello world with the next version of Scala. + +All you need to do to enable Dotty is `extends Dotty` in your build.scala . diff --git a/examples/dotty-example/build/build.scala b/examples/dotty-example/build/build.scala new file mode 100644 index 0000000..eb67d93 --- /dev/null +++ b/examples/dotty-example/build/build.scala @@ -0,0 +1,2 @@ +import cbt._ +class Build(val context: Context) extends Dotty diff --git a/examples/dotty-example/src/Main.scala b/examples/dotty-example/src/Main.scala new file mode 100644 index 0000000..fdc2068 --- /dev/null +++ b/examples/dotty-example/src/Main.scala @@ -0,0 +1,12 @@ +package dotty_example +object Main extends Foo("Hello Dotty - trait parameters, yay"){ + def main(args: Array[String]): Unit = { + println(hello) + + // Sanity check the classpath: this won't run if the dotty jar is not present. + val x: Int => Int = z => z + x(1) + } +} + +trait Foo(val hello: String) diff --git a/examples/multi-project-example/build/build.scala b/examples/multi-project-example/build/build.scala new file mode 100644 index 0000000..9a67488 --- /dev/null +++ b/examples/multi-project-example/build/build.scala @@ -0,0 +1,10 @@ +import cbt._ +class Build(val context: Context) extends SharedCbtBuild{ + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here + Seq( + // source dependency + DirectoryDependency( projectDirectory ++ "/sub1" ), + DirectoryDependency( projectDirectory ++ "/sub2" ) + ) +} diff --git a/examples/multi-project-example/build/build/build.scala b/examples/multi-project-example/build/build/build.scala new file mode 100644 index 0000000..be72a13 --- /dev/null +++ b/examples/multi-project-example/build/build/build.scala @@ -0,0 +1,10 @@ +import cbt._ + +class Build(val context: Context) extends BuildBuild{ + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here + Seq( + // source dependency + DirectoryDependency( projectDirectory.getParentFile ++ "/shared-build" ) + ) +} diff --git a/examples/multi-project-example/common/SomeSharedClass.scala b/examples/multi-project-example/common/SomeSharedClass.scala new file mode 100644 index 0000000..1f32c5a --- /dev/null +++ b/examples/multi-project-example/common/SomeSharedClass.scala @@ -0,0 +1 @@ +class SomeSharedClass
\ No newline at end of file diff --git a/examples/multi-project-example/common/build/build.scala b/examples/multi-project-example/common/build/build.scala new file mode 100644 index 0000000..0fbea50 --- /dev/null +++ b/examples/multi-project-example/common/build/build.scala @@ -0,0 +1,3 @@ +import cbt._ + +class Build(val context: Context) extends SharedCbtBuild diff --git a/examples/multi-project-example/common/build/build/build.scala b/examples/multi-project-example/common/build/build/build.scala new file mode 100644 index 0000000..efeeb77 --- /dev/null +++ b/examples/multi-project-example/common/build/build/build.scala @@ -0,0 +1,10 @@ +import cbt._ + +class Build(val context: Context) extends BuildBuild{ + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here + Seq( + // source dependency + DirectoryDependency( projectDirectory.getParentFile.getParentFile ++ "/shared-build" ) + ) +} diff --git a/examples/multi-project-example/shared-build/SharedCbtBuild.scala b/examples/multi-project-example/shared-build/SharedCbtBuild.scala new file mode 100644 index 0000000..38e4cc1 --- /dev/null +++ b/examples/multi-project-example/shared-build/SharedCbtBuild.scala @@ -0,0 +1,4 @@ +import cbt._ +trait SharedCbtBuild extends BaseBuild{ + override def defaultScalaVersion = "2.10.6" +}
\ No newline at end of file diff --git a/examples/multi-project-example/shared-build/build/build.scala b/examples/multi-project-example/shared-build/build/build.scala new file mode 100644 index 0000000..332519e --- /dev/null +++ b/examples/multi-project-example/shared-build/build/build.scala @@ -0,0 +1,6 @@ +import cbt._ +class Build(val context: Context) extends BaseBuild{ + override def dependencies = + super.dependencies :+ // don't forget super.dependencies here + context.cbtDependency +} diff --git a/examples/multi-project-example/sub1/SomeConcreteClass.scala b/examples/multi-project-example/sub1/SomeConcreteClass.scala new file mode 100644 index 0000000..2f8f715 --- /dev/null +++ b/examples/multi-project-example/sub1/SomeConcreteClass.scala @@ -0,0 +1 @@ +class SomeConcreteClass extends SomeSharedClass diff --git a/examples/multi-project-example/sub1/build/build.scala b/examples/multi-project-example/sub1/build/build.scala new file mode 100644 index 0000000..2c39a54 --- /dev/null +++ b/examples/multi-project-example/sub1/build/build.scala @@ -0,0 +1,10 @@ +import cbt._ + +class Build(val context: Context) extends SharedCbtBuild{ + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here + Seq( + // source dependency + DirectoryDependency( projectDirectory.getParentFile ++ "/common" ) + ) +} diff --git a/examples/multi-project-example/sub1/build/build/build.scala b/examples/multi-project-example/sub1/build/build/build.scala new file mode 100644 index 0000000..efeeb77 --- /dev/null +++ b/examples/multi-project-example/sub1/build/build/build.scala @@ -0,0 +1,10 @@ +import cbt._ + +class Build(val context: Context) extends BuildBuild{ + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here + Seq( + // source dependency + DirectoryDependency( projectDirectory.getParentFile.getParentFile ++ "/shared-build" ) + ) +} diff --git a/examples/multi-project-example/sub2/SomeOtherConcreteClass.scala b/examples/multi-project-example/sub2/SomeOtherConcreteClass.scala new file mode 100644 index 0000000..56b0aa3 --- /dev/null +++ b/examples/multi-project-example/sub2/SomeOtherConcreteClass.scala @@ -0,0 +1 @@ +class SomeOtherConcreteClass extends SomeSharedClass diff --git a/examples/multi-project-example/sub2/build/build.scala b/examples/multi-project-example/sub2/build/build.scala new file mode 100644 index 0000000..2c39a54 --- /dev/null +++ b/examples/multi-project-example/sub2/build/build.scala @@ -0,0 +1,10 @@ +import cbt._ + +class Build(val context: Context) extends SharedCbtBuild{ + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here + Seq( + // source dependency + DirectoryDependency( projectDirectory.getParentFile ++ "/common" ) + ) +} diff --git a/examples/multi-project-example/sub2/build/build/build.scala b/examples/multi-project-example/sub2/build/build/build.scala new file mode 100644 index 0000000..efeeb77 --- /dev/null +++ b/examples/multi-project-example/sub2/build/build/build.scala @@ -0,0 +1,10 @@ +import cbt._ + +class Build(val context: Context) extends BuildBuild{ + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here + Seq( + // source dependency + DirectoryDependency( projectDirectory.getParentFile.getParentFile ++ "/shared-build" ) + ) +} diff --git a/examples/resources-example/build/build.scala b/examples/resources-example/build/build.scala new file mode 100644 index 0000000..aec5d79 --- /dev/null +++ b/examples/resources-example/build/build.scala @@ -0,0 +1,21 @@ +import cbt._ +class Build(val context: Context) extends BaseBuild{ + /* + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here + Seq( + // source dependency + DirectoryDependency( projectDirectory ++ "/subProject" ) + ) ++ + Resolver( mavenCentral ).bind( + // CBT-style Scala dependencies + ScalaDependency( "com.lihaoyi", "ammonite-ops", "0.5.5" ) + MavenDependency( "com.lihaoyi", "ammonite-ops_2.11", "0.5.5" ) + + // SBT-style dependencies + "com.lihaoyi" %% "ammonite-ops" % "0.5.5" + "com.lihaoyi" % "ammonite-ops_2.11" % "0.5.5" + ) + */ + override def resourceClasspath = super.resourceClasspath ++ ClassPath(Seq(projectDirectory ++ "/my-resources")) +} diff --git a/examples/resources-example/build/build/build.scala b/examples/resources-example/build/build/build.scala new file mode 100644 index 0000000..f700060 --- /dev/null +++ b/examples/resources-example/build/build/build.scala @@ -0,0 +1,20 @@ +import cbt._ +class Build(val context: Context) extends BuildBuild{ + /* + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here + Seq( + // source dependency + DirectoryDependency( projectDirectory ++ "/subProject" ) + ) ++ + Resolver( mavenCentral ).bind( + // CBT-style Scala dependencies + ScalaDependency( "com.lihaoyi", "ammonite-ops", "0.5.5" ) + MavenDependency( "com.lihaoyi", "ammonite-ops_2.11", "0.5.5" ) + + // SBT-style dependencies + "com.lihaoyi" %% "ammonite-ops" % "0.5.5" + "com.lihaoyi" % "ammonite-ops_2.11" % "0.5.5" + ) + */ +} diff --git a/examples/resources-example/my-resources/foo.text b/examples/resources-example/my-resources/foo.text new file mode 100644 index 0000000..f82e417 --- /dev/null +++ b/examples/resources-example/my-resources/foo.text @@ -0,0 +1 @@ +Hello from a resource in my-resources/ (the additional location manually added here in build.scala) diff --git a/examples/resources-example/resources/foo.text b/examples/resources-example/resources/foo.text new file mode 100644 index 0000000..6d7c85a --- /dev/null +++ b/examples/resources-example/resources/foo.text @@ -0,0 +1 @@ +Hello from a resource in resources/ (the default location) diff --git a/examples/resources-example/src/Main.scala b/examples/resources-example/src/Main.scala new file mode 100644 index 0000000..3bc0943 --- /dev/null +++ b/examples/resources-example/src/Main.scala @@ -0,0 +1,27 @@ +import java.nio.file.{Files, Paths} +object Main extends App { + // Be aware that CBT currently isolates classloaders of dependencies + // your dependencies will not see the resources of your project + // This means that e.g. spray will not see a application.conf in your project's + // resources/ directory. See https://github.com/cvogt/cbt/issues/176 + println( + "foo.text in resources contains: " ++ + new String( + Files.readAllBytes( + Paths.get( getClass.getClassLoader.getResource("foo.text").getFile ) + ) + ) + ) + import scala.collection.JavaConverters._ + println( + "foo.text in resources and my-resources:\n" ++ + getClass.getClassLoader.getResources("foo.text").asScala.map( + resource => + new String( + Files.readAllBytes( + Paths.get( resource.getFile ) + ) + ) + ).mkString + ) +} diff --git a/examples/scalafmt-example/.scalafmt.conf b/examples/scalafmt-example/.scalafmt.conf new file mode 100644 index 0000000..4fc0088 --- /dev/null +++ b/examples/scalafmt-example/.scalafmt.conf @@ -0,0 +1,11 @@ +style: defaultWithAlign +danglingParentheses: true + +spaces { + inImportCurlyBraces: true +} + +rewriteTokens { + "->": "→" + "=>": "⇒" +} diff --git a/examples/scalafmt-example/README.md b/examples/scalafmt-example/README.md new file mode 100644 index 0000000..0a59f96 --- /dev/null +++ b/examples/scalafmt-example/README.md @@ -0,0 +1,16 @@ +This example shows integration with scalafmt plugin. + +Reformat executed on every `cbt compile` call, and affects only *.scala source files. + +You can provide your custom scalfmt preferences in build via `scalafmtConfig`. + +To see formatting in action: execute `cbt breakFormatting` to break formatting and then execute`cbt scalafmt` to get formatting back. + +To check if your code is properly formatted(for example as part of CI validation), you can execute: + +``` +cbt scalafmt +git diff --exit-code +``` + +Last command will return non-zero code, if your code isn't properly formatted. diff --git a/examples/scalafmt-example/build/build.scala b/examples/scalafmt-example/build/build.scala new file mode 100644 index 0000000..4f5545e --- /dev/null +++ b/examples/scalafmt-example/build/build.scala @@ -0,0 +1,26 @@ +import cbt._ + +class Build(val context: Context) extends BaseBuild with Scalafmt { + override def compile = { + scalafmt + super.compile + } + + def breakFormatting = { + import java.nio.file._ + import java.nio.charset.Charset + import scala.collection.JavaConverters._ + val utf8 = Charset.forName("UTF-8") + sourceFiles foreach { file => + val path = file.toPath + val fileLines = Files.readAllLines(path, utf8).asScala + val brokenLines = fileLines map (l => + l.dropWhile(_ == ' ') + .replaceAll("⇒", "=>") + .replaceAll("→", "->") + ) + Files.write(path, brokenLines.asJava, utf8) + } + System.err.println("Done breaking formatting") + } +} diff --git a/examples/scalafmt-example/build/build/build.scala b/examples/scalafmt-example/build/build/build.scala new file mode 100644 index 0000000..aa70f36 --- /dev/null +++ b/examples/scalafmt-example/build/build/build.scala @@ -0,0 +1,5 @@ +import cbt._ + +class Build(val context: Context) extends BuildBuild { + override def dependencies = super.dependencies :+ plugins.scalafmt +} diff --git a/examples/scalafmt-example/resources/reference.conf b/examples/scalafmt-example/resources/reference.conf new file mode 100644 index 0000000..f3e122d --- /dev/null +++ b/examples/scalafmt-example/resources/reference.conf @@ -0,0 +1,8 @@ +// should not reformat this, cause it is not in source files +some { + inside { + foo: 22 + bar: false + baz: "hello" + } +} diff --git a/examples/scalafmt-example/src/Main.scala b/examples/scalafmt-example/src/Main.scala new file mode 100644 index 0000000..465e27a --- /dev/null +++ b/examples/scalafmt-example/src/Main.scala @@ -0,0 +1,16 @@ +import scala.concurrent.{ Await, Future } +import scala.concurrent.duration._ + +object Main extends App { + println("fooo") + val futureRes = Await.result(Future.successful(1), 5.seconds) + List(1, 2, 4, 5, 6) match { + case h :: _ ⇒ println("not empty list") + case Nil ⇒ println("empty list") + } + + List(1 → 2, 2 → 3, 3 → 4) match { + case (1, 2) :: _ ⇒ 90 → 1 + case (22, 44) :: _ ⇒ 1 → 150 + } +} diff --git a/examples/scalajs-react-example/README.md b/examples/scalajs-react-example/README.md new file mode 100644 index 0000000..74e015a --- /dev/null +++ b/examples/scalajs-react-example/README.md @@ -0,0 +1,11 @@ + +Compilation instructions +------------------------------------------- +1. `cbt fastOptJS` + +Execution instructions +------------------------------------------- +1. `cd server` +2. `npm install` +3. `node app.js` +4. Go to http://localhost:3000 in a browser
\ No newline at end of file diff --git a/examples/scalajs-react-example/js/App.scala b/examples/scalajs-react-example/js/App.scala new file mode 100644 index 0000000..0cd170e --- /dev/null +++ b/examples/scalajs-react-example/js/App.scala @@ -0,0 +1,15 @@ +package prototype + +import japgolly.scalajs.react.ReactDOM +import org.scalajs.dom + +import scala.scalajs.js.JSApp +import scala.scalajs.js.annotation.JSExport + +@JSExport("App") +object App extends JSApp { + def main(): Unit = { + val doc = dom.document + ReactDOM.render(Pictures.PictureComponent(), doc.getElementById("main")) + } +} diff --git a/examples/scalajs-react-example/js/Pictures.scala b/examples/scalajs-react-example/js/Pictures.scala new file mode 100644 index 0000000..db1d7ef --- /dev/null +++ b/examples/scalajs-react-example/js/Pictures.scala @@ -0,0 +1,102 @@ +package prototype + +import japgolly.scalajs.react.{ReactComponentB, BackendScope, Callback} +import org.scalajs.dom + +import scala.scalajs._ +import japgolly.scalajs.react.vdom.all._ + +import scala.scalajs.js.JSON + +object Pictures { + + case class State(pictures: List[Picture], favourites: List[Picture]) + + type PicClick = (String, Boolean) => Callback + + class Backend($: BackendScope[Unit, State]) { + + def onPicClick(id: String, favorite: Boolean) = + $.state flatMap { s => + if (favorite) { + val newPics = s.pictures.map(p => if (p.id == id) p.copy(favorite = false) else p) + val newFavs = s.favourites.filter(p => p.id != id) + $.modState(_ => State(newPics, newFavs)) + } else { + var newPic: Picture = null + val newPics = s.pictures.map(p => if (p.id == id) { + newPic = p.copy(favorite = true); newPic + } else p) + val newFavs = s.favourites.+:(newPic) + $.modState(_ => State(newPics, newFavs)) + } + } + + def render(s: State) = + div( + h1("Popular Pixabay Pics"), + pictureList((s.pictures, onPicClick)), + h1("Your favorites"), + favoriteList((s.favourites, onPicClick))) + } + + val picture = ReactComponentB[(Picture, PicClick)]("picture") + .render_P { case (p, b) => + div(if (p.favorite) cls := "picture favorite" else cls := "picture", onClick --> b(p.id, p.favorite))( + img(src := p.src, title := p.title) + ) + } + .build + + val pictureList = ReactComponentB[(List[Picture], PicClick)]("pictureList") + .render_P { case (list, b) => + div(`class` := "pictures")( + if (list.isEmpty) span("Loading Pics..") + else { + list.map(p => picture.withKey(p.id)((p, b))) + } + ) + } + .build + + val favoriteList = ReactComponentB[(List[Picture], PicClick)]("favoriteList") + .render_P { case (list, b) => + div(`class` := "favorites")( + if (list.isEmpty) span("Click an image to mark as favorite") + else { + list.map(p => picture.withKey(p.id)((p, b))) + } + ) + } + .build + + val PictureComponent = ReactComponentB[Unit]("PictureComponent") + .initialState(State(Nil, Nil)) + .renderBackend[Backend] + .componentDidMount(scope => Callback { + import scalajs.js.Dynamic.{global => g} + def isDefined(g: js.Dynamic): Boolean = + g.asInstanceOf[js.UndefOr[AnyRef]].isDefined + val url = "http://localhost:3000/data" + val xhr = new dom.XMLHttpRequest() + xhr.open("GET", url) + xhr.onload = { (e: dom.Event) => + if (xhr.status == 200) { + val result = JSON.parse(xhr.responseText) + if (isDefined(result) && isDefined(result.hits)) { + val hits = result.hits.asInstanceOf[js.Array[js.Dynamic]] + val pics = hits.toList.map(item => Picture( + item.id.toString, + item.pageURL.toString, + item.previewURL.toString, + if (item.tags != null) item.tags.asInstanceOf[js.Array[String]].mkString(",") else "")) + //if (item.caption != null) item.caption.text.toString else "")) + scope.modState(_ => State(pics, Nil)).runNow() + } + } + } + xhr.send() + }) + .buildU + +} diff --git a/examples/scalajs-react-example/js/build/build.scala b/examples/scalajs-react-example/js/build/build.scala new file mode 100644 index 0000000..29f1c73 --- /dev/null +++ b/examples/scalajs-react-example/js/build/build.scala @@ -0,0 +1,22 @@ +import cbt._ +class Build(val context: Context) extends ScalaJsBuild{ + override val projectName = "my-project" + + override def sources = super.sources ++ Seq( + projectDirectory.getParentFile ++ "/shared" + ) + + override def dependencies = ( + super.dependencies ++ + Resolver( mavenCentral ).bind( + //"org.scalatest" %%% "scalatest" % "3.0.0-RC2", + "com.github.japgolly.scalajs-react" %%% "core" % "0.10.4", // for example + // for example if you want explicitely state scala version + "org.scala-js" % "scalajs-dom_sjs0.6_2.11" % "0.9.0" + ) + ) + + override protected def fastOptJSFile = { + projectDirectory.getParentFile ++ "/server/public" ++ ("/"++super.fastOptJSFile.getName) + } +} diff --git a/examples/scalajs-react-example/js/build/build/build.scala b/examples/scalajs-react-example/js/build/build/build.scala new file mode 100644 index 0000000..b30e005 --- /dev/null +++ b/examples/scalajs-react-example/js/build/build/build.scala @@ -0,0 +1,4 @@ +import cbt._ +class Build(val context: Context) extends BuildBuild{ + override def dependencies = super.dependencies :+ plugins.scalaJs +} diff --git a/examples/scalajs-react-example/jvm/build/build.scala b/examples/scalajs-react-example/jvm/build/build.scala new file mode 100644 index 0000000..327d705 --- /dev/null +++ b/examples/scalajs-react-example/jvm/build/build.scala @@ -0,0 +1,6 @@ +import cbt._ +class Build(val context: Context) extends BaseBuild{ + override def sources = super.sources ++ Seq( + projectDirectory.getParentFile ++ "/shared" + ) +} diff --git a/examples/scalajs-react-example/server/app.js b/examples/scalajs-react-example/server/app.js new file mode 100644 index 0000000..620c26a --- /dev/null +++ b/examples/scalajs-react-example/server/app.js @@ -0,0 +1,39 @@ +var express = require('express'); +var https = require('https'); + +var app = express(); + +app.get('/data', function(req, res){ + + var request = https.get( + //'https://api.instagram.com/v1/media/popular?client_id=642176ece1e7445e99244cec26f4de1f&callback=?', + 'https://pixabay.com/api/?key=2741116-9706ac6d4a58f2b5416225505&q=yellow+flowers&image_type=photo', + function(response) { + + var body = ""; + response.on('data', function(data) { + body += data; + }); + response.on('end', function() { + console.log(body); + try { + res.send(JSON.parse(body)); + } catch (e) { + return console.error(e); + } + }); + }); + request.on('error', function(e) { + console.log('Problem with request: ' + e.message); + }); + request.end(); +}); + +app.use(express.static(__dirname + '/public')); + +var server = app.listen(3000, function () { + var host = server.address().address; + var port = server.address().port; + + console.log('Example app listening at http://%s:%s', host, port); +}); diff --git a/examples/scalajs-react-example/server/package.json b/examples/scalajs-react-example/server/package.json new file mode 100644 index 0000000..d20ec98 --- /dev/null +++ b/examples/scalajs-react-example/server/package.json @@ -0,0 +1,14 @@ +{ + "name": "server", + "version": "1.0.0", + "description": "prototype", + "main": "app.js", + "author": "Katrin", + "license": "UNLICENSED", + "private": true, + "dependencies": { + "express": "^4.13.3", + "express-ws": "^0.2.6", + "body-parser": "^1.14.1" + } +} diff --git a/examples/scalajs-react-example/server/public/index.html b/examples/scalajs-react-example/server/public/index.html new file mode 100644 index 0000000..08de20d --- /dev/null +++ b/examples/scalajs-react-example/server/public/index.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> + <title>Prototype</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> +</head> +<body> + + <div id="main"></div> + + <script src="https://fb.me/react-15.1.0.min.js"></script> + <script src="https://fb.me/react-dom-15.1.0.min.js"></script> + <script type="text/javascript" src="./my-project-fastopt.js"></script> + <script type="text/javascript"> + App().main(); + </script> +</body> +</html> diff --git a/examples/scalajs-react-example/shared/Picture.scala b/examples/scalajs-react-example/shared/Picture.scala new file mode 100644 index 0000000..dbb985a --- /dev/null +++ b/examples/scalajs-react-example/shared/Picture.scala @@ -0,0 +1,3 @@ +package prototype + +case class Picture(id: String, url: String, src: String, title: String, favorite: Boolean = false) diff --git a/examples/scalariform-example/README.md b/examples/scalariform-example/README.md new file mode 100644 index 0000000..28ad226 --- /dev/null +++ b/examples/scalariform-example/README.md @@ -0,0 +1,16 @@ +This example shows integration with scalariform plugin. + +Reformat executed on every `cbt compile` call, and affects only *.scala source files. + +You can provide your custom scalariform preferences in build via `scalariformPreferences`. + +To see formatting in action: execute `cbt breakFormatting` to break formatting and then execute `cbt scalariformFormat` to get formatting back. + +To check if your code is properly formatted(for example as part of CI validation), you can execute: + +``` +cbt scalariformFormat +git diff --exit-code +``` + +Last command will return non-zero code, if your code isn't properly formatted. diff --git a/examples/scalariform-example/build/build.scala b/examples/scalariform-example/build/build.scala new file mode 100644 index 0000000..91ff67a --- /dev/null +++ b/examples/scalariform-example/build/build.scala @@ -0,0 +1,29 @@ +import cbt._ +import scalariform.formatter.preferences._ + +class Build(val context: Context) extends BaseBuild with Scalariform { + override def compile = { + scalariformFormat + super.compile + } + + override def scalariformPreferences = + FormattingPreferences() + .setPreference(SpacesAroundMultiImports, true) + .setPreference(DoubleIndentClassDeclaration, true) + .setPreference(RewriteArrowSymbols, true) + + final def breakFormatting = { + import java.nio.file._ + import java.nio.charset.Charset + import scala.collection.JavaConverters._ + val utf8 = Charset.forName("UTF-8") + sourceFiles foreach { file => + val path = file.toPath + val fileLines = Files.readAllLines(path, utf8).asScala + val brokenLines = fileLines map (_.dropWhile(_ == ' ')) + Files.write(path, brokenLines.asJava, utf8) + } + System.err.println("Done breaking formatting") + } +} diff --git a/examples/scalariform-example/build/build/build.scala b/examples/scalariform-example/build/build/build.scala new file mode 100644 index 0000000..59ab8d1 --- /dev/null +++ b/examples/scalariform-example/build/build/build.scala @@ -0,0 +1,5 @@ +import cbt._ + +class Build(val context: Context) extends BuildBuild { + override def dependencies = super.dependencies :+ plugins.scalariform +} diff --git a/examples/scalariform-example/resources/reference.conf b/examples/scalariform-example/resources/reference.conf new file mode 100644 index 0000000..f3e122d --- /dev/null +++ b/examples/scalariform-example/resources/reference.conf @@ -0,0 +1,8 @@ +// should not reformat this, cause it is not in source files +some { + inside { + foo: 22 + bar: false + baz: "hello" + } +} diff --git a/examples/scalariform-example/src/Main.scala b/examples/scalariform-example/src/Main.scala new file mode 100644 index 0000000..d299aad --- /dev/null +++ b/examples/scalariform-example/src/Main.scala @@ -0,0 +1,11 @@ +import scala.concurrent.{ Await, Future } +import scala.concurrent.duration._ + +object Main extends App { + println("fooo") + val futureRes = Await.result(Future.successful(1), 5.seconds) + List(1, 2, 4, 5, 6) match { + case h :: _ ⇒ println("not empty list") + case Nil ⇒ println("empty list") + } +} diff --git a/examples/scalatest-example/build/build.scala b/examples/scalatest-example/build/build.scala new file mode 100644 index 0000000..48248fd --- /dev/null +++ b/examples/scalatest-example/build/build.scala @@ -0,0 +1,9 @@ +import cbt._ +class Build(val context: Context) extends SbtLayoutMain { + outer => + override def test: Option[ExitCode] = Some{ + new BasicBuild(context) with ScalaTest with SbtLayoutTest{ + override def dependencies = outer +: super.dependencies + }.run + } +} diff --git a/examples/scalatest-example/build/build/build.scala b/examples/scalatest-example/build/build/build.scala new file mode 100644 index 0000000..d641b51 --- /dev/null +++ b/examples/scalatest-example/build/build/build.scala @@ -0,0 +1,8 @@ +import cbt._ + +class Build(val context: Context) extends BuildBuild{ + override def dependencies = super.dependencies ++ Seq( + plugins.scalaTest, + plugins.sbtLayout + ) +} diff --git a/examples/scalatest-example/src/main/scala/Hello.scala b/examples/scalatest-example/src/main/scala/Hello.scala new file mode 100644 index 0000000..099a84d --- /dev/null +++ b/examples/scalatest-example/src/main/scala/Hello.scala @@ -0,0 +1,7 @@ +object Main extends App{ + + println("Hello World") + + def square(x: Int) = x * x + +} diff --git a/examples/scalatest-example/src/test/scala/Test.scala b/examples/scalatest-example/src/test/scala/Test.scala new file mode 100644 index 0000000..48b0a36 --- /dev/null +++ b/examples/scalatest-example/src/test/scala/Test.scala @@ -0,0 +1,23 @@ +import collection.mutable.Stack +import org.scalatest._ + +class Test extends FlatSpec with Matchers { + "square" should "return double" in { + Main.square(2) should be (4) + } + + "A Stack" should "pop values in last-in-first-out order" in { + val stack = new Stack[Int] + stack.push(1) + stack.push(2) + stack.pop() should be (2) + stack.pop() should be (1) + } + + it should "throw NoSuchElementException if an empty stack is popped" in { + val emptyStack = new Stack[Int] + a [NoSuchElementException] should be thrownBy { + emptyStack.pop() + } + } +}
\ No newline at end of file diff --git a/examples/simple-example/build/build.scala b/examples/simple-example/build/build.scala new file mode 100644 index 0000000..9320990 --- /dev/null +++ b/examples/simple-example/build/build.scala @@ -0,0 +1,20 @@ +import cbt._ +class Build(val context: Context) extends BaseBuild{ + /* + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here + Seq( + // source dependency + DirectoryDependency( projectDirectory ++ "/subProject" ) + ) ++ + Resolver( mavenCentral ).bind( + // CBT-style Scala dependencies + ScalaDependency( "com.lihaoyi", "ammonite-ops", "0.5.5" ) + MavenDependency( "com.lihaoyi", "ammonite-ops_2.11", "0.5.5" ) + + // SBT-style dependencies + "com.lihaoyi" %% "ammonite-ops" % "0.5.5" + "com.lihaoyi" % "ammonite-ops_2.11" % "0.5.5" + ) + */ +} diff --git a/examples/simple-example/build/build/build.scala b/examples/simple-example/build/build/build.scala new file mode 100644 index 0000000..f700060 --- /dev/null +++ b/examples/simple-example/build/build/build.scala @@ -0,0 +1,20 @@ +import cbt._ +class Build(val context: Context) extends BuildBuild{ + /* + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here + Seq( + // source dependency + DirectoryDependency( projectDirectory ++ "/subProject" ) + ) ++ + Resolver( mavenCentral ).bind( + // CBT-style Scala dependencies + ScalaDependency( "com.lihaoyi", "ammonite-ops", "0.5.5" ) + MavenDependency( "com.lihaoyi", "ammonite-ops_2.11", "0.5.5" ) + + // SBT-style dependencies + "com.lihaoyi" %% "ammonite-ops" % "0.5.5" + "com.lihaoyi" % "ammonite-ops_2.11" % "0.5.5" + ) + */ +} diff --git a/examples/simple-example/src/Main.scala b/examples/simple-example/src/Main.scala new file mode 100644 index 0000000..88a18d3 --- /dev/null +++ b/examples/simple-example/src/Main.scala @@ -0,0 +1,3 @@ +object Main extends App { + println("Hello World") +} diff --git a/examples/sonatype-release-example/README.md b/examples/sonatype-release-example/README.md new file mode 100644 index 0000000..a099036 --- /dev/null +++ b/examples/sonatype-release-example/README.md @@ -0,0 +1 @@ +TBD diff --git a/examples/sonatype-release-example/build/build.scala b/examples/sonatype-release-example/build/build.scala new file mode 100644 index 0000000..6af452d --- /dev/null +++ b/examples/sonatype-release-example/build/build.scala @@ -0,0 +1,31 @@ +import java.net.URL + +import cbt._ + +class Build(val context: Context) extends SonatypeRelease { + def groupId: String = "com.github.rockjam" + def defaultVersion: String = "0.0.15" + def name: String = "cbt-sonatype" + + def description: String = "Plugin for CBT to release artifacts to sonatype OSS" + def developers: Seq[Developer] = Seq( + Developer( + "rockjam", + "Nikolay Tatarinov", + "GMT+3", + new URL("https://github.com/rockjam") + ) + ) + def inceptionYear: Int = 2016 + def licenses: Seq[cbt.License] = Seq(License.Apache2) + def organization: Option[cbt.Organization] = None + def scmConnection: String = "" + def scmUrl: String = "https://github.com/rockjam/cbt-sonatype.git" + def url: java.net.URL = new URL("https://github.com/rockjam/cbt-sonatype") + + override def dependencies = + super.dependencies ++ + Resolver( mavenCentral ).bind( + ScalaDependency("com.chuusai", "shapeless", "2.3.2") + ) +} diff --git a/examples/sonatype-release-example/build/build/build.scala b/examples/sonatype-release-example/build/build/build.scala new file mode 100644 index 0000000..a47d3e1 --- /dev/null +++ b/examples/sonatype-release-example/build/build/build.scala @@ -0,0 +1,5 @@ +import cbt._ + +class Build(val context: Context) extends BuildBuild { + override def dependencies = super.dependencies :+ plugins.sonatypeRelease +} diff --git a/examples/sonatype-release-example/src/Main.scala b/examples/sonatype-release-example/src/Main.scala new file mode 100644 index 0000000..5e03d27 --- /dev/null +++ b/examples/sonatype-release-example/src/Main.scala @@ -0,0 +1,3 @@ +object Main extends App { + println("This is serious app that does nothing, but has shapeless dependency") +} diff --git a/examples/uber-jar-example/README.md b/examples/uber-jar-example/README.md new file mode 100644 index 0000000..2460084 --- /dev/null +++ b/examples/uber-jar-example/README.md @@ -0,0 +1,41 @@ +### Uber-jar plugin example + +This example shows how to build uber jar(aka fat jar) with `UberJar` plugin. + +In order to create uber jar: execute `cbt uberJar`. Produced jar will be in target folder. + +By default, jar name is your `cbt projectName`, you can provide other name via overriding `uberJarName` task. + +By default, main class is `Main`. You can provide custom main class via overriding `uberJarMainClass` task. + +To run your main class you can execute `java -jar your-jar-name.jar`. + +You can also run scala REPL with your jar classpath and classes with this command: `scala -cp your-jar-name.jar`. + +In scala REPL you will have access to all your project classes and dependencies. + +``` +scala -cp uber-jar-example-0.0.1.jar +Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_72). +Type in expressions for evaluation. Or try :help. + +scala> import com.github.someguy.ImportantLib +import com.github.someguy.ImportantLib + +scala> ImportantLib.add(1,2) +res0: Int = 3 + +scala> ImportantLib.currentDirectory +Current directory is: /Users/rockjam/projects/cbt/examples/uber-jar-example/target + +scala> Main.main(Array.empty) +fooo +Current directory is: /Users/rockjam/projects/cbt/examples/uber-jar-example/target +not empty list + +scala> import shapeless._ +import shapeless._ + +scala> 1 :: "String" :: 3 :: HNil +res3: shapeless.::[Int,shapeless.::[String,shapeless.::[Int,shapeless.HNil]]] = 1 :: String :: 3 :: HNil +``` diff --git a/examples/uber-jar-example/build/build.scala b/examples/uber-jar-example/build/build.scala new file mode 100644 index 0000000..fec58ae --- /dev/null +++ b/examples/uber-jar-example/build/build.scala @@ -0,0 +1,16 @@ +import cbt._ + +class Build(val context: Context) extends BaseBuild with UberJar { + + override def projectName: String = "uber-jar-example" + + override def dependencies = super.dependencies ++ + Resolver( mavenCentral ).bind( + ScalaDependency("com.chuusai", "shapeless", "2.3.1"), + ScalaDependency("com.lihaoyi", "fansi", "0.1.3"), + ScalaDependency("org.typelevel", "cats", "0.6.0") + ) + + override def uberJarName = projectName + "-0.0.1" + ".jar" + +} diff --git a/examples/uber-jar-example/build/build/build.scala b/examples/uber-jar-example/build/build/build.scala new file mode 100644 index 0000000..2938ffd --- /dev/null +++ b/examples/uber-jar-example/build/build/build.scala @@ -0,0 +1,5 @@ +import cbt._ + +class Build(val context: Context) extends BuildBuild { + override def dependencies = super.dependencies :+ plugins.uberJar +} diff --git a/examples/uber-jar-example/src/Main.scala b/examples/uber-jar-example/src/Main.scala new file mode 100644 index 0000000..f60634f --- /dev/null +++ b/examples/uber-jar-example/src/Main.scala @@ -0,0 +1,21 @@ +import scala.concurrent.{ Await, Future } +import scala.concurrent.duration._ + +import com.github.someguy.ImportantLib + +object Main extends App { + println("fooo") + val futureRes = Await.result(Future.successful(1), 5.seconds) + + ImportantLib.currentDirectory() + + val hlist = { + import shapeless._ + 1 :: "string" :: 3 :: HNil + } + + List(1, 2, 4, 5, 6) match { + case h :: _ ⇒ println("not empty list") + case Nil ⇒ println("empty list") + } +} diff --git a/examples/uber-jar-example/src/com/github/someguy/ImportantLib.scala b/examples/uber-jar-example/src/com/github/someguy/ImportantLib.scala new file mode 100644 index 0000000..34baf2f --- /dev/null +++ b/examples/uber-jar-example/src/com/github/someguy/ImportantLib.scala @@ -0,0 +1,11 @@ +package com.github.someguy + +import java.nio.file.Paths + +object ImportantLib { + def add(a: Int, b: Int): Int = a + b + def currentDirectory() = { + println(fansi.Color.Green(s"Current directory is: ${Paths.get("").toAbsolutePath}")) + } + +} diff --git a/examples/wartremover-example/build/build.scala b/examples/wartremover-example/build/build.scala new file mode 100644 index 0000000..c715f20 --- /dev/null +++ b/examples/wartremover-example/build/build.scala @@ -0,0 +1,9 @@ +import cbt._ + +import org.wartremover.warts.{ Null, Var } +import org.wartremover.WartTraverser + +class Build(val context: Context) extends BuildBuild with WartRemover { + + override def wartremoverErrors: Seq[WartTraverser] = Seq(Var, Null) +} diff --git a/examples/wartremover-example/build/build/build.scala b/examples/wartremover-example/build/build/build.scala new file mode 100644 index 0000000..eb2f193 --- /dev/null +++ b/examples/wartremover-example/build/build/build.scala @@ -0,0 +1,5 @@ +import cbt._ + +class Build(val context: Context) extends BuildBuild { + override def dependencies = super.dependencies :+ plugins.wartremover +} diff --git a/examples/wartremover-example/src/Main.scala b/examples/wartremover-example/src/Main.scala new file mode 100644 index 0000000..dc4b3da --- /dev/null +++ b/examples/wartremover-example/src/Main.scala @@ -0,0 +1,5 @@ +object Main { + def main(args: Array[String]): Unit = { + var nastyVar = null + } +} diff --git a/nailgun_launcher/CbtURLClassLoader.java b/nailgun_launcher/CbtURLClassLoader.java new file mode 100644 index 0000000..38fc905 --- /dev/null +++ b/nailgun_launcher/CbtURLClassLoader.java @@ -0,0 +1,59 @@ +package cbt; +import java.io.*; +import java.net.*; +import java.util.*; +import static cbt.Stage0Lib.*; +import java.util.concurrent.ConcurrentHashMap; +public class CbtURLClassLoader extends java.net.URLClassLoader{ + public String toString(){ + return ( + super.toString() + + "(\n " + + Arrays.toString(getURLs()) + + ",\n " + + join("\n ",(getParent() == null?"":getParent().toString()).split("\n")) + + "\n)" + ); + } + ClassLoaderCache2<Class> cache = new ClassLoaderCache2<Class>( + new ConcurrentHashMap<String, Object>(), + new ConcurrentHashMap<Object, Class>() + ); + public Class loadClass(String name) throws ClassNotFoundException{ + Class _class = super.loadClass(name); + if(_class == null) throw new ClassNotFoundException(name); + else return _class; + } + public Class loadClass(String name, Boolean resolve) throws ClassNotFoundException{ + //System.out.println("loadClass("+name+") on \n"+this); + if(!cache.contains(name)) + try{ + cache.put(super.loadClass(name, resolve), name); + } catch (ClassNotFoundException e){ + cache.put(Object.class, name); + } + Class _class = cache.get(name); + if(_class == Object.class){ + if( name == "java.lang.Object" ) + return Object.class; + else return null; + } else { + return _class; + } + } + void assertExist(URL[] urls){ + for(URL url: urls){ + if(!new File(url.getPath()).exists()){ + throw new AssertionError("File does not exist when trying to create CbtURLClassLoader: "+url); + } + } + } + public CbtURLClassLoader(URL[] urls, ClassLoader parent){ + super(urls, parent); + assertExist(urls); + } + public CbtURLClassLoader(URL[] urls){ + super(urls, null); + assertExist(urls); + } +}
\ No newline at end of file diff --git a/nailgun_launcher/ClassLoaderCache2.java b/nailgun_launcher/ClassLoaderCache2.java new file mode 100644 index 0000000..bf9ca3b --- /dev/null +++ b/nailgun_launcher/ClassLoaderCache2.java @@ -0,0 +1,37 @@ +package cbt; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import static java.io.File.pathSeparator; +import static cbt.Stage0Lib.*; + +final class ClassLoaderCache2<T>{ + ConcurrentHashMap<String,Object> keys; + ConcurrentHashMap<Object,T> values; + + public ClassLoaderCache2( + ConcurrentHashMap<String,Object> keys, + ConcurrentHashMap<Object,T> values + ){ + this.keys = keys; + this.values = values; + } + + public T get( String key ){ + return values.get( + keys.get( key ) + ); + } + + public Boolean contains( String key ){ + return keys.containsKey( key ); + } + + public T put( T value, String key ){ + LockableKey2 keyObject = new LockableKey2(); + keys.put( key, keyObject ); + values.put( keyObject, value ); + return value; + } +} +class LockableKey2{}
\ No newline at end of file diff --git a/nailgun_launcher/EarlyDependencies.java b/nailgun_launcher/EarlyDependencies.java new file mode 100644 index 0000000..4ffbdfd --- /dev/null +++ b/nailgun_launcher/EarlyDependencies.java @@ -0,0 +1,143 @@ +// This file was auto-generated using `cbt tools cbtEarlyDependencies` +package cbt; +import java.io.*; +import java.nio.file.*; +import java.net.*; +import java.security.*; +import static cbt.Stage0Lib.*; +import static cbt.NailgunLauncher.*; + +class EarlyDependencies{ + + /** ClassLoader for stage1 */ + ClassLoader classLoader; + String[] classpathArray; + /** ClassLoader for zinc */ + ClassLoader zinc; + + String scalaReflect_2_11_8_File; + String scalaCompiler_2_11_8_File; + String scalaXml_1_0_5_File; + String scalaLibrary_2_11_8_File; + String zinc_0_3_9_File; + String incrementalCompiler_0_13_9_File; + String compilerInterface_0_13_9_File; + String scalaCompiler_2_10_5_File; + String sbtInterface_0_13_9_File; + String scalaReflect_2_10_5_File; + String scalaLibrary_2_10_5_File; + + public EarlyDependencies( + String mavenCache, String mavenUrl, ClassLoaderCache2<ClassLoader> classLoaderCache, ClassLoader rootClassLoader + ) throws Throwable { + scalaReflect_2_11_8_File = mavenCache + "/org/scala-lang/scala-reflect/2.11.8/scala-reflect-2.11.8.jar"; + scalaCompiler_2_11_8_File = mavenCache + "/org/scala-lang/scala-compiler/2.11.8/scala-compiler-2.11.8.jar"; + scalaXml_1_0_5_File = mavenCache + "/org/scala-lang/modules/scala-xml_2.11/1.0.5/scala-xml_2.11-1.0.5.jar"; + scalaLibrary_2_11_8_File = mavenCache + "/org/scala-lang/scala-library/2.11.8/scala-library-2.11.8.jar"; + zinc_0_3_9_File = mavenCache + "/com/typesafe/zinc/zinc/0.3.9/zinc-0.3.9.jar"; + incrementalCompiler_0_13_9_File = mavenCache + "/com/typesafe/sbt/incremental-compiler/0.13.9/incremental-compiler-0.13.9.jar"; + compilerInterface_0_13_9_File = mavenCache + "/com/typesafe/sbt/compiler-interface/0.13.9/compiler-interface-0.13.9-sources.jar"; + scalaCompiler_2_10_5_File = mavenCache + "/org/scala-lang/scala-compiler/2.10.5/scala-compiler-2.10.5.jar"; + sbtInterface_0_13_9_File = mavenCache + "/com/typesafe/sbt/sbt-interface/0.13.9/sbt-interface-0.13.9.jar"; + scalaReflect_2_10_5_File = mavenCache + "/org/scala-lang/scala-reflect/2.10.5/scala-reflect-2.10.5.jar"; + scalaLibrary_2_10_5_File = mavenCache + "/org/scala-lang/scala-library/2.10.5/scala-library-2.10.5.jar"; + + download(new URL(mavenUrl + "/org/scala-lang/scala-reflect/2.11.8/scala-reflect-2.11.8.jar"), Paths.get(scalaReflect_2_11_8_File), "b74530deeba742ab4f3134de0c2da0edc49ca361"); + download(new URL(mavenUrl + "/org/scala-lang/scala-compiler/2.11.8/scala-compiler-2.11.8.jar"), Paths.get(scalaCompiler_2_11_8_File), "fe1285c9f7b58954c5ef6d80b59063569c065e9a"); + + // org.scala-lang:scala-library:2.10.5 + download(new URL(mavenUrl + "/org/scala-lang/scala-library/2.10.5/scala-library-2.10.5.jar"), Paths.get(scalaLibrary_2_10_5_File), "57ac67a6cf6fd591e235c62f8893438e8d10431d"); + + String[] scalaLibrary_2_10_5_ClasspathArray = new String[]{scalaLibrary_2_10_5_File}; + String scalaLibrary_2_10_5_Classpath = classpath( scalaLibrary_2_10_5_ClasspathArray ); + ClassLoader scalaLibrary_2_10_5_ = + classLoaderCache.contains( scalaLibrary_2_10_5_Classpath ) + ? classLoaderCache.get( scalaLibrary_2_10_5_Classpath ) + : classLoaderCache.put( classLoader( scalaLibrary_2_10_5_File, rootClassLoader ), scalaLibrary_2_10_5_Classpath ); + + // org.scala-lang:scala-reflect:2.10.5 + download(new URL(mavenUrl + "/org/scala-lang/scala-reflect/2.10.5/scala-reflect-2.10.5.jar"), Paths.get(scalaReflect_2_10_5_File), "7392facb48876c67a89fcb086112b195f5f6bbc3"); + + String[] scalaReflect_2_10_5_ClasspathArray = new String[]{scalaLibrary_2_10_5_File, scalaReflect_2_10_5_File}; + String scalaReflect_2_10_5_Classpath = classpath( scalaReflect_2_10_5_ClasspathArray ); + ClassLoader scalaReflect_2_10_5_ = + classLoaderCache.contains( scalaReflect_2_10_5_Classpath ) + ? classLoaderCache.get( scalaReflect_2_10_5_Classpath ) + : classLoaderCache.put( classLoader( scalaReflect_2_10_5_File, scalaLibrary_2_10_5_ ), scalaReflect_2_10_5_Classpath ); + + // com.typesafe.sbt:sbt-interface:0.13.9 + download(new URL(mavenUrl + "/com/typesafe/sbt/sbt-interface/0.13.9/sbt-interface-0.13.9.jar"), Paths.get(sbtInterface_0_13_9_File), "29848631415402c81b732e919be88f268df37250"); + + String[] sbtInterface_0_13_9_ClasspathArray = new String[]{sbtInterface_0_13_9_File, scalaLibrary_2_10_5_File, scalaReflect_2_10_5_File}; + String sbtInterface_0_13_9_Classpath = classpath( sbtInterface_0_13_9_ClasspathArray ); + ClassLoader sbtInterface_0_13_9_ = + classLoaderCache.contains( sbtInterface_0_13_9_Classpath ) + ? classLoaderCache.get( sbtInterface_0_13_9_Classpath ) + : classLoaderCache.put( classLoader( sbtInterface_0_13_9_File, scalaReflect_2_10_5_ ), sbtInterface_0_13_9_Classpath ); + + // org.scala-lang:scala-compiler:2.10.5 + download(new URL(mavenUrl + "/org/scala-lang/scala-compiler/2.10.5/scala-compiler-2.10.5.jar"), Paths.get(scalaCompiler_2_10_5_File), "f0f5bb444ca26a6e489af3dd35e24f7e2d2d118e"); + + String[] scalaCompiler_2_10_5_ClasspathArray = new String[]{sbtInterface_0_13_9_File, scalaCompiler_2_10_5_File, scalaLibrary_2_10_5_File, scalaReflect_2_10_5_File}; + String scalaCompiler_2_10_5_Classpath = classpath( scalaCompiler_2_10_5_ClasspathArray ); + ClassLoader scalaCompiler_2_10_5_ = + classLoaderCache.contains( scalaCompiler_2_10_5_Classpath ) + ? classLoaderCache.get( scalaCompiler_2_10_5_Classpath ) + : classLoaderCache.put( classLoader( scalaCompiler_2_10_5_File, sbtInterface_0_13_9_ ), scalaCompiler_2_10_5_Classpath ); + + // com.typesafe.sbt:compiler-interface:0.13.9 + download(new URL(mavenUrl + "/com/typesafe/sbt/compiler-interface/0.13.9/compiler-interface-0.13.9-sources.jar"), Paths.get(compilerInterface_0_13_9_File), "2311addbed1182916ad00f83c57c0eeca1af382b"); + + String[] compilerInterface_0_13_9_ClasspathArray = new String[]{compilerInterface_0_13_9_File, sbtInterface_0_13_9_File, scalaCompiler_2_10_5_File, scalaLibrary_2_10_5_File, scalaReflect_2_10_5_File}; + String compilerInterface_0_13_9_Classpath = classpath( compilerInterface_0_13_9_ClasspathArray ); + ClassLoader compilerInterface_0_13_9_ = + classLoaderCache.contains( compilerInterface_0_13_9_Classpath ) + ? classLoaderCache.get( compilerInterface_0_13_9_Classpath ) + : classLoaderCache.put( classLoader( compilerInterface_0_13_9_File, scalaCompiler_2_10_5_ ), compilerInterface_0_13_9_Classpath ); + + // com.typesafe.sbt:incremental-compiler:0.13.9 + download(new URL(mavenUrl + "/com/typesafe/sbt/incremental-compiler/0.13.9/incremental-compiler-0.13.9.jar"), Paths.get(incrementalCompiler_0_13_9_File), "fbbf1cadbed058aa226643e83543c35de43b13f0"); + + String[] incrementalCompiler_0_13_9_ClasspathArray = new String[]{compilerInterface_0_13_9_File, incrementalCompiler_0_13_9_File, sbtInterface_0_13_9_File, scalaCompiler_2_10_5_File, scalaLibrary_2_10_5_File, scalaReflect_2_10_5_File}; + String incrementalCompiler_0_13_9_Classpath = classpath( incrementalCompiler_0_13_9_ClasspathArray ); + ClassLoader incrementalCompiler_0_13_9_ = + classLoaderCache.contains( incrementalCompiler_0_13_9_Classpath ) + ? classLoaderCache.get( incrementalCompiler_0_13_9_Classpath ) + : classLoaderCache.put( classLoader( incrementalCompiler_0_13_9_File, compilerInterface_0_13_9_ ), incrementalCompiler_0_13_9_Classpath ); + + // com.typesafe.zinc:zinc:0.3.9 + download(new URL(mavenUrl + "/com/typesafe/zinc/zinc/0.3.9/zinc-0.3.9.jar"), Paths.get(zinc_0_3_9_File), "46a4556d1f36739879f4b2cc19a73d12b3036e9a"); + + String[] zinc_0_3_9_ClasspathArray = new String[]{compilerInterface_0_13_9_File, incrementalCompiler_0_13_9_File, sbtInterface_0_13_9_File, zinc_0_3_9_File, scalaCompiler_2_10_5_File, scalaLibrary_2_10_5_File, scalaReflect_2_10_5_File}; + String zinc_0_3_9_Classpath = classpath( zinc_0_3_9_ClasspathArray ); + ClassLoader zinc_0_3_9_ = + classLoaderCache.contains( zinc_0_3_9_Classpath ) + ? classLoaderCache.get( zinc_0_3_9_Classpath ) + : classLoaderCache.put( classLoader( zinc_0_3_9_File, incrementalCompiler_0_13_9_ ), zinc_0_3_9_Classpath ); + + // org.scala-lang:scala-library:2.11.8 + download(new URL(mavenUrl + "/org/scala-lang/scala-library/2.11.8/scala-library-2.11.8.jar"), Paths.get(scalaLibrary_2_11_8_File), "ddd5a8bced249bedd86fb4578a39b9fb71480573"); + + String[] scalaLibrary_2_11_8_ClasspathArray = new String[]{scalaLibrary_2_11_8_File}; + String scalaLibrary_2_11_8_Classpath = classpath( scalaLibrary_2_11_8_ClasspathArray ); + ClassLoader scalaLibrary_2_11_8_ = + classLoaderCache.contains( scalaLibrary_2_11_8_Classpath ) + ? classLoaderCache.get( scalaLibrary_2_11_8_Classpath ) + : classLoaderCache.put( classLoader( scalaLibrary_2_11_8_File, rootClassLoader ), scalaLibrary_2_11_8_Classpath ); + + // org.scala-lang.modules:scala-xml_2.11:1.0.5 + download(new URL(mavenUrl + "/org/scala-lang/modules/scala-xml_2.11/1.0.5/scala-xml_2.11-1.0.5.jar"), Paths.get(scalaXml_1_0_5_File), "77ac9be4033768cf03cc04fbd1fc5e5711de2459"); + + String[] scalaXml_1_0_5_ClasspathArray = new String[]{scalaXml_1_0_5_File, scalaLibrary_2_11_8_File}; + String scalaXml_1_0_5_Classpath = classpath( scalaXml_1_0_5_ClasspathArray ); + ClassLoader scalaXml_1_0_5_ = + classLoaderCache.contains( scalaXml_1_0_5_Classpath ) + ? classLoaderCache.get( scalaXml_1_0_5_Classpath ) + : classLoaderCache.put( classLoader( scalaXml_1_0_5_File, scalaLibrary_2_11_8_ ), scalaXml_1_0_5_Classpath ); + + classLoader = scalaXml_1_0_5_; + classpathArray = scalaXml_1_0_5_ClasspathArray; + + zinc = zinc_0_3_9_; + } +} diff --git a/nailgun_launcher/MultiClassLoader2.java b/nailgun_launcher/MultiClassLoader2.java new file mode 100644 index 0000000..46e7527 --- /dev/null +++ b/nailgun_launcher/MultiClassLoader2.java @@ -0,0 +1,46 @@ +package cbt; +import java.net.*; +import java.io.*; +import java.util.*; + +public class MultiClassLoader2 extends ClassLoader{ + public ClassLoader[] parents; + public ClassLoader[] parents(){ + return this.parents; + } + public MultiClassLoader2(ClassLoader... parents){ + super(null); + this.parents = parents; + } + public Class findClass(String name) throws ClassNotFoundException{ + for(ClassLoader parent: parents){ + try{ + return parent.loadClass(name); + } catch (ClassNotFoundException e) { + if(e.getMessage() != name) throw e; + } + } + // FIXME: have a logger in Java land + // System.err.println("NOT FOUND: "+name); + return null; + } + public URL findResource(String name){ + for(ClassLoader parent: parents){ + URL res = parent.getResource(name); + if(res != null) return res; + } + return null; + } + public Enumeration<URL> findResources(String name) throws IOException{ + ArrayList<URL> resources = new ArrayList<URL>(); + for(ClassLoader parent: parents){ + for(URL resource: Collections.list(parent.getResources(name))){ + resources.add( resource ); + } + } + return Collections.enumeration(resources); + } + public String toString(){ + return super.toString() + "(" + Arrays.toString(parents) +")"; + } +} diff --git a/nailgun_launcher/NailgunLauncher.java b/nailgun_launcher/NailgunLauncher.java new file mode 100644 index 0000000..6639218 --- /dev/null +++ b/nailgun_launcher/NailgunLauncher.java @@ -0,0 +1,200 @@ +package cbt; +import java.io.*; +import java.lang.reflect.*; +import java.net.*; +import java.security.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import static cbt.Stage0Lib.*; +import static java.io.File.pathSeparator; + +/** + * This launcher allows to start the JVM without loading anything else permanently into its + * classpath except for the launcher itself. That's why it is written in Java without + * dependencies outside the JDK. + */ +public class NailgunLauncher{ + /** Persistent cache for caching classloaders for the JVM life time. */ + private final static ClassLoaderCache2<ClassLoader> classLoaderCache = new ClassLoaderCache2<ClassLoader>( + new ConcurrentHashMap<String,Object>(), + new ConcurrentHashMap<Object,ClassLoader>() + ); + + public final static SecurityManager initialSecurityManager + = System.getSecurityManager(); + + public static String TARGET = System.getenv("TARGET"); + private static String NAILGUN = "nailgun_launcher/"; + private static String STAGE1 = "stage1/"; + + @SuppressWarnings("unchecked") + public static Object getBuild( Object context ) throws Throwable{ + BuildStage1Result res = buildStage1( + (Boolean) get(context, "cbtHasChangedCompat"), + (Long) get(context, "startCompat"), + ((File) get(context, "cache")).toString() + "/", + ((File) get(context, "cbtHome")).toString(), + ((File) get(context, "compatibilityTarget")).toString() + "/", + new ClassLoaderCache2<ClassLoader>( + (ConcurrentHashMap<String,Object>) get(context, "permanentKeys"), + (ConcurrentHashMap<Object,ClassLoader>) get(context, "permanentClassLoaders") + ) + ); + return + res + .classLoader + .loadClass("cbt.Stage1") + .getMethod( "getBuild", Object.class, Boolean.class ) + .invoke(null, context, res.changed); + } + + public static void main( String[] args ) throws Throwable { + Long _start = System.currentTimeMillis(); + if(args[0].equals("check-alive")){ + System.exit(33); + return; + } + + System.setSecurityManager( new TrapSecurityManager() ); + installProxySettings(); + String[] diff = args[0].split("\\."); + long start = _start - (Long.parseLong(diff[0]) * 1000L) - Long.parseLong(diff[1]); + + // if nailgun didn't install it's threadLocal stdout/err replacements, install CBT's. + // this hack allows to later swap out System.out/err while still affecting things like + // scala.Console, which captured them at startup + try{ + System.out.getClass().getDeclaredField("streams"); // nailgun ThreadLocalPrintStream + assert(System.out.getClass().getName() == "com.martiansoftware.nailgun.ThreadLocalPrintStream"); + } catch( NoSuchFieldException e ){ + System.setOut( new PrintStream(new ThreadLocalOutputStream(System.out), true) ); + } + try{ + System.err.getClass().getDeclaredField("streams"); // nailgun ThreadLocalPrintStream + assert(System.err.getClass().getName() == "com.martiansoftware.nailgun.ThreadLocalPrintStream"); + } catch( NoSuchFieldException e ){ + System.setErr( new PrintStream(new ThreadLocalOutputStream(System.err), true) ); + } + // --------------------- + + _assert(System.getenv("CBT_HOME") != null, "environment variable CBT_HOME not defined"); + String CBT_HOME = System.getenv("CBT_HOME"); + String cache = CBT_HOME + "/cache/"; + String compatibilityTarget = CBT_HOME + "/compatibility/" + TARGET; + BuildStage1Result res = buildStage1( + false, start, cache, CBT_HOME, compatibilityTarget, classLoaderCache + ); + + try{ + System.exit( + (Integer) res + .classLoader + .loadClass("cbt.Stage1") + .getMethod( + "run", + String[].class, File.class, File.class, Boolean.class, + File.class, Long.class, ConcurrentHashMap.class, ConcurrentHashMap.class + ) + .invoke( + null, + (Object) args, new File(cache), new File(CBT_HOME), res.changed, + new File(compatibilityTarget), start, classLoaderCache.keys, classLoaderCache.values + ) + ); + } catch (java.lang.reflect.InvocationTargetException e) { + throw unwrapInvocationTargetException(e); + } + } + + public static Throwable unwrapInvocationTargetException(Throwable e){ + if(e instanceof java.lang.reflect.InvocationTargetException){ + return unwrapInvocationTargetException(((java.lang.reflect.InvocationTargetException) e).getCause()); + } else{ + return e; + } + } + + public static BuildStage1Result buildStage1( + Boolean changed, long start, String cache, String cbtHome, String compatibilityTarget, ClassLoaderCache2<ClassLoader> classLoaderCache + ) throws Throwable { + _assert(TARGET != null, "environment variable TARGET not defined"); + String nailgunTarget = cbtHome + "/" + NAILGUN + TARGET; + String stage1Sources = cbtHome + "/" + STAGE1; + String stage1Target = stage1Sources + TARGET; + File compatibilitySources = new File(cbtHome + "/compatibility"); + String mavenCache = cache + "maven"; + String mavenUrl = "https://repo1.maven.org/maven2"; + + ClassLoader rootClassLoader = new CbtURLClassLoader( new URL[]{}, ClassLoader.getSystemClassLoader().getParent() ); // wrap for caching + EarlyDependencies earlyDeps = new EarlyDependencies(mavenCache, mavenUrl, classLoaderCache, rootClassLoader); + + ClassLoader compatibilityClassLoader; + if(!compatibilityTarget.startsWith(cbtHome)){ + compatibilityClassLoader = classLoaderCache.get( compatibilityTarget ); + } else { + List<File> compatibilitySourceFiles = new ArrayList<File>(); + for( File f: compatibilitySources.listFiles() ){ + if( f.isFile() && (f.toString().endsWith(".scala") || f.toString().endsWith(".java")) ){ + compatibilitySourceFiles.add(f); + } + } + changed = compile(changed, start, "", compatibilityTarget, earlyDeps, compatibilitySourceFiles); + + if( classLoaderCache.contains( compatibilityTarget ) ){ + compatibilityClassLoader = classLoaderCache.get( compatibilityTarget ); + } else { + compatibilityClassLoader = classLoaderCache.put( classLoader(compatibilityTarget, rootClassLoader), compatibilityTarget ); + } + } + + String[] nailgunClasspathArray = append( earlyDeps.classpathArray, nailgunTarget ); + String nailgunClasspath = classpath( nailgunClasspathArray ); + ClassLoader nailgunClassLoader = new CbtURLClassLoader( new URL[]{}, NailgunLauncher.class.getClassLoader() ); // wrap for caching + if( !classLoaderCache.contains( nailgunClasspath ) ){ + nailgunClassLoader = classLoaderCache.put( nailgunClassLoader, nailgunClasspath ); + } + + String[] stage1ClasspathArray = + append( append( nailgunClasspathArray, compatibilityTarget ), stage1Target ); + String stage1Classpath = classpath( stage1ClasspathArray ); + + List<File> stage1SourceFiles = new ArrayList<File>(); + for( File f: new File(stage1Sources).listFiles() ){ + if( f.isFile() && f.toString().endsWith(".scala") ){ + stage1SourceFiles.add(f); + } + } + changed = compile(changed, start, stage1Classpath, stage1Target, earlyDeps, stage1SourceFiles); + + ClassLoader stage1classLoader; + if( !changed && classLoaderCache.contains( stage1Classpath ) ){ + stage1classLoader = classLoaderCache.get( stage1Classpath ); + } else { + stage1classLoader = + classLoaderCache.put( + classLoader( + stage1Target, + new MultiClassLoader2( + nailgunClassLoader, + compatibilityClassLoader, + earlyDeps.classLoader + ) + ), + stage1Classpath + ); + } + + return new BuildStage1Result( + changed, + stage1classLoader + ); + } +} +class BuildStage1Result{ + Boolean changed; + ClassLoader classLoader; + BuildStage1Result( Boolean changed, ClassLoader classLoader ){ + this.changed = changed; + this.classLoader = classLoader; + } +} diff --git a/nailgun_launcher/ProxySecurityManager.java b/nailgun_launcher/ProxySecurityManager.java new file mode 100644 index 0000000..1a6e49c --- /dev/null +++ b/nailgun_launcher/ProxySecurityManager.java @@ -0,0 +1,102 @@ +package cbt; + +import java.security.*; +import java.io.FileDescriptor; +import java.net.InetAddress; + +/* +SecurityManager proxy that forwards all calls to the provided target if != null. +Useful to replace a previously installed SecurityManager, overriding some methods +but forwarding the rest. +*/ +public class ProxySecurityManager extends SecurityManager{ + private SecurityManager target; + public ProxySecurityManager(SecurityManager target){ + this.target = target; + } + public Object getSecurityContext() { + if(target != null) + return target.getSecurityContext(); + else return super.getSecurityContext(); + } + public void checkPermission(Permission perm) { + if(target != null) target.checkPermission(perm); + } + public void checkPermission(Permission perm, Object context) { + if(target != null) target.checkPermission(perm, context); + } + public void checkCreateClassLoader() { + if(target != null) target.checkCreateClassLoader(); + } + public void checkAccess(Thread t) { + if(target != null) target.checkAccess(t); + } + public void checkAccess(ThreadGroup g) { + if(target != null) target.checkAccess(g); + } + public void checkExit(int status) { + if(target != null) target.checkExit(status); + } + public void checkExec(String cmd) { + if(target != null) target.checkExec(cmd); + } + public void checkLink(String lib) { + if(target != null) target.checkLink(lib); + } + public void checkRead(FileDescriptor fd) { + if(target != null) target.checkRead(fd); + } + public void checkRead(String file) { + if(target != null) target.checkRead(file); + } + public void checkRead(String file, Object context) { + if(target != null) target.checkRead(file, context); + } + public void checkWrite(FileDescriptor fd) { + if(target != null) target.checkWrite(fd); + } + public void checkWrite(String file) { + if(target != null) target.checkWrite(file); + } + public void checkDelete(String file) { + if(target != null) target.checkDelete(file); + } + public void checkConnect(String host, int port) { + if(target != null) target.checkConnect(host, port); + } + public void checkConnect(String host, int port, Object context) { + if(target != null) target.checkConnect(host, port, context); + } + public void checkListen(int port) { + if(target != null) target.checkListen(port); + } + public void checkAccept(String host, int port) { + if(target != null) target.checkAccept(host, port); + } + public void checkMulticast(InetAddress maddr) { + if(target != null) target.checkMulticast(maddr); + } + public void checkPropertiesAccess() { + if(target != null) target.checkPropertiesAccess(); + } + public void checkPropertyAccess(String key) { + if(target != null) target.checkPropertyAccess(key); + } + public void checkPrintJobAccess() { + if(target != null) target.checkPrintJobAccess(); + } + public void checkPackageAccess(String pkg) { + if(target != null) target.checkPackageAccess(pkg); + } + public void checkPackageDefinition(String pkg) { + if(target != null) target.checkPackageDefinition(pkg); + } + public void checkSetFactory() { + if(target != null) target.checkSetFactory(); + } + public ThreadGroup getThreadGroup() { + if(target != null) + return target.getThreadGroup(); + else return super.getThreadGroup(); + } +} diff --git a/nailgun_launcher/Stage0Lib.java b/nailgun_launcher/Stage0Lib.java new file mode 100644 index 0000000..452bdae --- /dev/null +++ b/nailgun_launcher/Stage0Lib.java @@ -0,0 +1,197 @@ +package cbt; +import java.io.*; +import java.lang.reflect.*; +import java.net.*; +import java.nio.*; +import java.nio.file.*; +import java.security.*; +import java.util.*; +import javax.xml.bind.annotation.adapters.HexBinaryAdapter; +import static java.io.File.pathSeparator; +import static cbt.NailgunLauncher.*; +import java.nio.file.*; +import java.nio.file.attribute.FileTime; + +public class Stage0Lib{ + public static void _assert(Boolean condition, Object msg){ + if(!condition){ + throw new AssertionError("Assertion failed: "+msg); + } + } + + public static int runMain(String cls, String[] args, ClassLoader cl) throws Throwable{ + Boolean trapExitCodeBefore = TrapSecurityManager.trapExitCode().get(); + try{ + TrapSecurityManager.trapExitCode().set(true); + cl.loadClass(cls) + .getMethod("main", String[].class) + .invoke( null, (Object) args); + return 0; + }catch( InvocationTargetException exception ){ + Throwable cause = exception.getCause(); + if(TrapSecurityManager.isTrappedExit(cause)){ + return TrapSecurityManager.exitCode(cause); + } + throw exception; + } finally { + TrapSecurityManager.trapExitCode().set(trapExitCodeBefore); + } + } + + public static Object get(Object object, String method) throws Throwable{ + return object.getClass().getMethod( method ).invoke(object); + } + + public static String classpath( String... files ){ + Arrays.sort(files); + return join( pathSeparator, files ); + } + + public static File write(File file, String content, OpenOption... options) throws Throwable{ + file.getParentFile().mkdirs(); + Files.write(file.toPath(), content.getBytes(), options); + return file; + } + + public static Boolean compile( + Boolean changed, Long start, String classpath, String target, + EarlyDependencies earlyDeps, List<File> sourceFiles + ) throws Throwable{ + File statusFile = new File( new File(target) + ".last-success" ); + Long lastSuccessfullCompile = statusFile.lastModified(); + for( File file: sourceFiles ){ + if( file.lastModified() > lastSuccessfullCompile ){ + changed = true; + break; + } + } + if(changed){ + List<String> zincArgs = new ArrayList<String>( + Arrays.asList( + new String[]{ + "-scala-compiler", earlyDeps.scalaCompiler_2_11_8_File, + "-scala-library", earlyDeps.scalaLibrary_2_11_8_File, + "-scala-extra", earlyDeps.scalaReflect_2_11_8_File, + "-sbt-interface", earlyDeps.sbtInterface_0_13_9_File, + "-compiler-interface", earlyDeps.compilerInterface_0_13_9_File, + "-cp", classpath, + "-d", target, + "-S-deprecation", + "-S-feature", + "-S-unchecked" + } + ) + ); + + for( File f: sourceFiles ){ + zincArgs.add(f.toString()); + } + + PrintStream oldOut = System.out; + try{ + System.setOut(System.err); + int exitCode = runMain( "com.typesafe.zinc.Main", zincArgs.toArray(new String[zincArgs.size()]), earlyDeps.zinc ); + if( exitCode == 0 ){ + write( statusFile, "" ); + Files.setLastModifiedTime( statusFile.toPath(), FileTime.fromMillis(start) ); + } else { + System.exit( exitCode ); + } + } finally { + System.setOut(oldOut); + } + } + return changed; + } + + public static ClassLoader classLoader( String file ) throws Throwable{ + return new CbtURLClassLoader( + new URL[]{ new URL("file:"+file) } + ); + } + public static ClassLoader classLoader( String file, ClassLoader parent ) throws Throwable{ + return new CbtURLClassLoader( + new URL[]{ new URL("file:"+file) }, parent + ); + } + + private static String getVarFromEnv(String envKey) { + String value = System.getenv(envKey); + if(value==null || value.isEmpty()) { + value = System.getenv(envKey.toUpperCase()); + } + return value; + } + + private static void setProxyfromPropOrEnv(String envKey, String propKeyH, String propKeyP) { + String proxyHost = System.getProperty(propKeyH); + String proxyPort = System.getProperty(propKeyP); + if((proxyHost==null || proxyHost.isEmpty()) && (proxyPort==null || proxyPort.isEmpty())) { + String envVar = getVarFromEnv(envKey); + if(envVar != null && !envVar.isEmpty()) { + String[] proxy = envVar.replaceFirst("^https?://", "").split(":", 2); + System.setProperty(propKeyH, proxy[0]); + System.setProperty(propKeyP, proxy[1]); + } + } + } + + public static void installProxySettings() throws URISyntaxException { + setProxyfromPropOrEnv("http_proxy", "http.proxyHost", "http.proxyPort"); + setProxyfromPropOrEnv("https_proxy", "https.proxyHost", "https.proxyPort"); + String nonHosts = System.getProperty("http.nonProxyHosts"); + if(nonHosts==null || nonHosts.isEmpty()) { + String envVar = getVarFromEnv("no_proxy"); + if(envVar != null && !envVar.isEmpty()) { + System.setProperty("http.nonProxyHosts", envVar.replaceAll(",","|")); + } + } + } + + private static final ProxySelector ps = ProxySelector.getDefault(); + + public static HttpURLConnection openConnectionConsideringProxy(URL urlString) + throws IOException, URISyntaxException { + java.net.Proxy proxy = ps.select(urlString.toURI()).get(0); + return (HttpURLConnection) urlString.openConnection(proxy); + } + + public static void download(URL urlString, Path target, String sha1) throws Throwable { + final Path unverified = Paths.get(target+".unverified"); + if(!Files.exists(target)) { + new File(target.toString()).getParentFile().mkdirs(); + System.err.println("downloading " + urlString); + System.err.println("to " + target); + final InputStream stream = openConnectionConsideringProxy(urlString).getInputStream(); + Files.copy(stream, unverified, StandardCopyOption.REPLACE_EXISTING); + stream.close(); + final String checksum = sha1(Files.readAllBytes(unverified)); + if(sha1 == null || sha1.toLowerCase().equals(checksum)) { + Files.move(unverified, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } else { + System.err.println(target + " checksum does not match.\nExpected: |" + sha1 + "|\nFound: |" + checksum + "|"); + System.exit(1); + } + } + } + + public static String sha1(byte[] bytes) throws Throwable { + final MessageDigest sha1 = MessageDigest.getInstance("SHA1"); + sha1.update(bytes, 0, bytes.length); + return (new HexBinaryAdapter()).marshal(sha1.digest()).toLowerCase(); + } + + public static String join(String separator, String[] parts){ + String result = parts[0]; + for(int i = 1; i < parts.length; i++){ + result += separator + parts[i]; + } + return result; + } + + public static String[] append( String[] array, String item ){ + String[] copy = Arrays.copyOf(array, array.length + 1); + copy[array.length] = item; + return copy; + } +} diff --git a/nailgun_launcher/ThreadLocalOutputStream.java b/nailgun_launcher/ThreadLocalOutputStream.java new file mode 100644 index 0000000..0b106d7 --- /dev/null +++ b/nailgun_launcher/ThreadLocalOutputStream.java @@ -0,0 +1,34 @@ +package cbt; +import java.io.*; + +public class ThreadLocalOutputStream extends OutputStream{ + final public ThreadLocal<OutputStream> threadLocal; + final private OutputStream initialValue; + + public ThreadLocalOutputStream( OutputStream initialValue ){ + this.initialValue = initialValue; + threadLocal = new ThreadLocal<OutputStream>() { + @Override protected OutputStream initialValue() { + return ThreadLocalOutputStream.this.initialValue; + } + }; + } + + public OutputStream get(){ + return threadLocal.get(); + } + + public void set( OutputStream outputStream ){ + threadLocal.set( outputStream ); + } + + public void write( int b ) throws IOException{ + // after implementing this I realized NailgunLauncher uses the same hack, + // so probably this is not a problem performance + get().write(b); + } + + public void flush() throws IOException{ + get().flush(); + } +} diff --git a/nailgun_launcher/TrapSecurityManager.java b/nailgun_launcher/TrapSecurityManager.java new file mode 100644 index 0000000..48e152b --- /dev/null +++ b/nailgun_launcher/TrapSecurityManager.java @@ -0,0 +1,83 @@ +package cbt; +import java.security.*; +/* +When enabled, this SecurityManager turns System.exit(...) calls into exceptions that can be caught and handled. +Installing a SecurityManager is a global side-effect and thus needs extra care in a persistent +background process like CBT's. The current approach is install it once during JVM-startup. +When disabled this delegates to the SecurityManager installed before if any, which +would be Nailgun's if running on Nailgun. If we do not delegate to Nailgun, it seems we +could in some cases kill the server process +*/ +public class TrapSecurityManager extends ProxySecurityManager{ + public static ThreadLocal<Boolean> trapExitCode(){ + // storing the flag in the installed security manager + // instead of e.g. a static member is necessary because + // we run multiple versions of CBT with multiple TrapSecurityManager classes + // but we need to affect the installed one + SecurityManager sm = System.getSecurityManager(); + if(sm instanceof TrapSecurityManager){ + return ((TrapSecurityManager) sm)._trapExitCode; + } else { + try{ + @SuppressWarnings("unchecked") + ThreadLocal<Boolean> res = + (ThreadLocal<Boolean>) sm.getClass().getMethod("trapExitCode").invoke(null); + return res; + } catch(Exception e) { + throw new RuntimeException(e); + } + } + } + + private final ThreadLocal<Boolean> _trapExitCode = + new ThreadLocal<Boolean>() { + @Override protected Boolean initialValue() { + return false; + } + }; + + public TrapSecurityManager(){ + super(NailgunLauncher.initialSecurityManager); + } + + public void checkPermission( Permission permission ){ + /* + NOTE: is it actually ok, to just make these empty? + Calling .super leads to ClassNotFound exteption for a lambda. + Calling to the previous SecurityManager leads to a stack overflow + */ + if(!TrapSecurityManager.trapExitCode().get()){ + super.checkPermission(permission); + } + } + public void checkPermission( Permission permission, Object context ){ + /* Does this methods need to be overidden? */ + if(!TrapSecurityManager.trapExitCode().get()){ + super.checkPermission(permission, context); + } + } + + private static final String prefix = "[TrappedExit] "; + + @Override + public void checkExit( int status ){ + if(TrapSecurityManager.trapExitCode().get()){ + // using a RuntimeException and a prefix here instead of a custom + // exception type because this is thrown by the installed TrapSecurityManager + // but other versions of cbt need to be able to catch it, that do not have access + // to that version of the TrapSecurityManager class + throw new RuntimeException(prefix+status); + } + super.checkExit(status); + } + + public static boolean isTrappedExit( Throwable t ){ + return t instanceof RuntimeException && t.getMessage() != null && t.getMessage().startsWith(prefix); + } + + public static int exitCode( Throwable t ){ + assert(isTrappedExit(t)); + return Integer.parseInt( t.getMessage().substring(prefix.length()) ); + } + +} diff --git a/plugins/readme.txt b/plugins/readme.txt new file mode 100644 index 0000000..c3f561f --- /dev/null +++ b/plugins/readme.txt @@ -0,0 +1,5 @@ +This directory is for plugins plugins which are shipped with CBT, +but need to be explicitly dependended upon in the BuildBuild. +This is nice for plugins, which themselves have dependencies +that we do not want CBT to depend on. +See stage2/plugins/ for built-in plugins, that have no dependencies. diff --git a/plugins/sbt_layout/SbtLayout.scala b/plugins/sbt_layout/SbtLayout.scala new file mode 100644 index 0000000..5cd7a03 --- /dev/null +++ b/plugins/sbt_layout/SbtLayout.scala @@ -0,0 +1,10 @@ +package cbt + +trait SbtLayoutTest extends BaseBuild{ + override def sources = Seq(projectDirectory ++ "/src/test/scala") + override def compileTarget = super.compileTarget.getParentFile ++ "/test-classes" +} + +trait SbtLayoutMain extends BaseBuild{ + override def sources = Seq( projectDirectory ++ "/src/main/scala" ) +} diff --git a/plugins/sbt_layout/build/build.scala b/plugins/sbt_layout/build/build.scala new file mode 100644 index 0000000..80ff3ba --- /dev/null +++ b/plugins/sbt_layout/build/build.scala @@ -0,0 +1,2 @@ +import cbt._ +class Build(val context: Context) extends Plugin diff --git a/plugins/scalafmt/Scalafmt.scala b/plugins/scalafmt/Scalafmt.scala new file mode 100644 index 0000000..1f8bf2d --- /dev/null +++ b/plugins/scalafmt/Scalafmt.scala @@ -0,0 +1,99 @@ +package cbt + +import org.scalafmt.Error.Incomplete +import org.scalafmt.Formatted +import org.scalafmt.cli.StyleCache +import org.scalafmt.config.ScalafmtConfig +import java.io.File +import java.nio.file.Files._ +import java.nio.file.{ FileSystems, Path, Paths } + +/** + * This plugin provides scalafmt support for cbt. + * + */ +trait Scalafmt extends BaseBuild { + /** + * Reformat scala source code according to `scalafmtConfig` rules + * + * @return always returns `ExitCode.Success` + */ + final def scalafmt: ExitCode = { + Scalafmt.format(sourceFiles, scalafmtConfig) + ExitCode.Success + } + + /** + * Scalafmt formatting config. + * + * Tries to get style in following order: + * • project local .scalafmt.conf + * • global ~/.scalafmt.conf + * • default scalafmt config + * + * Override this task if you want to provide + * scalafmt config programmatically on your own. + */ + def scalafmtConfig: ScalafmtConfig = + Scalafmt.getStyle( + project = projectDirectory.toPath, + home = Option(System.getProperty("user.home")) map (p => Paths.get(p)) + ) +} + +object Scalafmt { + + def getStyle(project: Path, home: Option[Path]): ScalafmtConfig = { + val local = getConfigPath(project) + val global = home flatMap getConfigPath + val customStyle = for { + configPath <- local.orElse(global) + style <- StyleCache.getStyleForFile(configPath.toString) + } yield style + + customStyle.getOrElse(ScalafmtConfig.default) + } + + def format(files: Seq[File], style: ScalafmtConfig): Unit = { + var reformattedCount: Int = 0 + scalaSourceFiles(files) foreach { path => + handleFormatted(path, style) { case (original, result) => + result match { + case Formatted.Success(formatted) => + if (original != formatted) { + write(path, formatted.getBytes) + reformattedCount += 1 + } + case Formatted.Failure(e: Incomplete) => + System.err.println(s"Couldn't complete file reformat: $path") + case Formatted.Failure(e) => + System.err.println(s"Failed to format file: $path, cause: ${e}") + } + } + } + if (reformattedCount > 0) System.err.println(s"Formatted $reformattedCount Scala sources") + } + + private val scalaFileMatcher = FileSystems.getDefault.getPathMatcher("glob:**.scala") + + private def scalaSourceFiles(files: Seq[File]): Seq[Path] = { + files collect { + case f if f.exists + && scalaFileMatcher.matches(f.toPath) => f.toPath + } + } + + private def handleFormatted[T](path: Path, style: ScalafmtConfig)(handler: (String, Formatted) => T): T = { + val original = new String(readAllBytes(path)) + val result = org.scalafmt.Scalafmt.format(original, style) + handler(original, result) + } + + private def getConfigPath(base: Path): Option[Path] = { + val location = base.resolve(".scalafmt.conf").toFile + Option(location.exists && location.isFile) collect { + case true => location.toPath.toAbsolutePath + } + } + +} diff --git a/plugins/scalafmt/build/build.scala b/plugins/scalafmt/build/build.scala new file mode 100644 index 0000000..2631908 --- /dev/null +++ b/plugins/scalafmt/build/build.scala @@ -0,0 +1,12 @@ +import cbt._ + +class Build(val context: Context) extends Plugin { + private val ScalafmtVersion = "0.4.2" + + override def dependencies = + super.dependencies ++ + Resolver( mavenCentral ).bind( + ScalaDependency("com.geirsson", "scalafmt", ScalafmtVersion), + ScalaDependency("com.geirsson", "scalafmt-cli", ScalafmtVersion) + ) +} diff --git a/plugins/scalajs/ScalaJsBuild.scala b/plugins/scalajs/ScalaJsBuild.scala new file mode 100644 index 0000000..9374f66 --- /dev/null +++ b/plugins/scalajs/ScalaJsBuild.scala @@ -0,0 +1,52 @@ +package cbt +import java.io.File +import java.net.URL + +trait ScalaJsBuild extends BaseBuild { + final protected val scalaJsLib = ScalaJsLib( + scalaJsVersion, + scalaVersion, + context.cbtHasChanged, + context.classLoaderCache, + context.paths.mavenCache + ) + import scalaJsLib.{link => _,_} + + def scalaJsVersion = "0.6.8" + final protected val scalaJsMajorVersion: String = lib.libMajorVersion(scalaJsVersion) + final protected val artifactIdSuffix = s"_sjs$scalaJsMajorVersion" + + override def dependencies = super.dependencies :+ scalaJsLibraryDependency + override def scalacOptions = super.scalacOptions ++ scalaJsLib.scalacOptions + + /** Note: We make same assumption about scala version. + In order to be able to choose different scala version, one has to use %. */ + implicit class ScalaJsDependencyBuilder(groupId: String){ + def %%%(artifactId: String) = new DependencyBuilder2( + groupId, artifactId + artifactIdSuffix, Some(scalaMajorVersion)) + } + + private def link(mode: ScalaJsOutputMode, outputPath: File) = scalaJsLib.link( + mode, outputPath, scalaJsOptions, + target +: dependencies.collect{case d: BoundMavenDependency => d.jar} + ) + + def scalaJsOptions: Seq[String] = Seq() + def scalaJsOptionsFastOpt: Seq[String] = scalaJsOptions + def scalaJsOptionsFullOpt: Seq[String] = scalaJsOptions + + private def output(mode: ScalaJsOutputMode) = target ++ s"/$projectName-${mode.fileSuffix}.js" + protected def fastOptJSFile: File = output(FastOptJS) + protected def fullOptJSFile: File = output(FullOptJS) + + def fastOptJS = { + compile + link(FastOptJS, fastOptJSFile) + fastOptJSFile + } + def fullOptJS = { + compile + link(FullOptJS, fullOptJSFile) + fullOptJSFile + } +} diff --git a/plugins/scalajs/ScalaJsLib.scala b/plugins/scalajs/ScalaJsLib.scala new file mode 100644 index 0000000..0355850 --- /dev/null +++ b/plugins/scalajs/ScalaJsLib.scala @@ -0,0 +1,51 @@ +package cbt +import java.io.File + +case class ScalaJsLib( + scalaJsVersion: String, scalaVersion: String, + cbtHasChanged: Boolean, classLoaderCache: ClassLoaderCache, mavenCache: File +)(implicit logger: Logger){ + sealed trait ScalaJsOutputMode { + def option: String + def fileSuffix: String + } + case object FastOptJS extends ScalaJsOutputMode{ + override val option = "--fastOpt" + override val fileSuffix = "fastopt" + } + case object FullOptJS extends ScalaJsOutputMode{ + override val option = "--fullOpt" + override val fileSuffix = "fullopt" + } + + val lib = new Lib(logger) + def dep(artifactId: String) = MavenResolver( cbtHasChanged, mavenCache, mavenCentral ).bindOne( + MavenDependency("org.scala-js", artifactId, scalaJsVersion) + ) + + def link( + mode: ScalaJsOutputMode, outputPath: File, + scalaJsOptions: Seq[String], entriesToLink: Seq[File] + ) = { + val scalaJsCliDep = dep( "scalajs-cli_"++lib.libMajorVersion(scalaVersion) ) + lib.runMain( + "org.scalajs.cli.Scalajsld", + Seq( + mode.option, + "--sourceMap", + "--stdlib", s"${scalaJsLibraryDependency.jar.getAbsolutePath}", + "--output", outputPath.string + ) ++ scalaJsOptions ++ entriesToLink.map(_.getAbsolutePath), + scalaJsCliDep.classLoader(classLoaderCache) + ) + } + + val scalaJsLibraryDependency = dep( "scalajs-library_"++lib.libMajorVersion(scalaVersion) ) + + // Has to be full Scala version because the compiler is incompatible between versions + val scalaJsCompilerDependency = dep( "scalajs-compiler_"++scalaVersion ) + val scalacOptions = Seq( + "-Xplugin:" ++ scalaJsCompilerDependency.jar.string, + "-Xplugin-require:scalajs" + ) +} diff --git a/plugins/scalajs/build/build.scala b/plugins/scalajs/build/build.scala new file mode 100644 index 0000000..0205cf8 --- /dev/null +++ b/plugins/scalajs/build/build.scala @@ -0,0 +1,3 @@ +import cbt._ + +class Build(val context: Context) extends Plugin diff --git a/plugins/scalariform/Scalariform.scala b/plugins/scalariform/Scalariform.scala new file mode 100644 index 0000000..0612469 --- /dev/null +++ b/plugins/scalariform/Scalariform.scala @@ -0,0 +1,61 @@ +package cbt + +import java.io.File +import java.nio.file.FileSystems +import java.nio.file.Files._ + +import scalariform.formatter.ScalaFormatter +import scalariform.formatter.preferences.FormattingPreferences +import scalariform.parser.ScalaParserException + +trait Scalariform extends BaseBuild { + final def scalariformFormat: ExitCode = { + Scalariform.format(sourceFiles, scalariformPreferences, scalaVersion) + ExitCode.Success + } + + def scalariformPreferences: FormattingPreferences = Scalariform.defaultPreferences +} + +object Scalariform { + + val defaultPreferences: FormattingPreferences = { + import scalariform.formatter.preferences._ + FormattingPreferences() + .setPreference(AlignParameters, true) + .setPreference(AlignArguments, true) + .setPreference(AlignSingleLineCaseStatements, true) + .setPreference(MultilineScaladocCommentsStartOnFirstLine, true) + .setPreference(SpaceInsideParentheses, true) + .setPreference(SpacesWithinPatternBinders, true) + .setPreference(SpacesAroundMultiImports, true) + .setPreference(DoubleIndentClassDeclaration, false) + } + + private val scalaFileMatcher = FileSystems.getDefault.getPathMatcher("glob:**.scala") + + def format(files: Seq[File], preferences: FormattingPreferences, scalaVersion: String): Unit = { + var reformattedCount: Int = 0 + for (file <- files if file.exists) { + val path = file.toPath + if(scalaFileMatcher.matches(path)) { + try { + val sourceCode = new String(readAllBytes(path)) + val formatted = ScalaFormatter.format( + sourceCode, + preferences, + Some(scalaVersion) + ) + if (sourceCode != formatted) { + write(path, formatted.getBytes) + reformattedCount += 1 + } + } catch { + case e: ScalaParserException => System.err.println(s"Scalariform parser error: ${e.getMessage} when formatting: $file") + } + } + } + if (reformattedCount > 0) System.err.println(s"Formatted $reformattedCount Scala sources") + } + +} diff --git a/plugins/scalariform/build/build.scala b/plugins/scalariform/build/build.scala new file mode 100644 index 0000000..5910b41 --- /dev/null +++ b/plugins/scalariform/build/build.scala @@ -0,0 +1,9 @@ +import cbt._ + +class Build(val context: Context) extends Plugin { + override def dependencies = + super.dependencies ++ + Resolver( mavenCentral ).bind( + ScalaDependency("org.scalariform", "scalariform", "0.1.8") + ) +} diff --git a/plugins/scalatest/ScalaTest.scala b/plugins/scalatest/ScalaTest.scala new file mode 100644 index 0000000..ee96431 --- /dev/null +++ b/plugins/scalatest/ScalaTest.scala @@ -0,0 +1,43 @@ +package cbt +import org.scalatest._ + + +trait ScalaTest extends BaseBuild{ + override def run: ExitCode = { + import ScalaTestLib._ + val _classLoader = classLoader(context.classLoaderCache) + val suiteNames = compile.map( d => discoverSuites(d, _classLoader) ).toVector.flatten + runSuites( suiteNames.map( loadSuite( _, _classLoader ) ) ) + ExitCode.Success + } + override def dependencies = super.dependencies ++ Resolver( mavenCentral ).bind( ScalaDependency("org.scalatest","scalatest","2.2.4") ) +} + +object ScalaTestLib{ + import java.io.File + def runSuites(suites: Seq[Suite]) = { + def color: Boolean = true + def durations: Boolean = true + def shortstacks: Boolean = true + def fullstacks: Boolean = true + def stats: Boolean = true + def testName: String = null + def configMap: ConfigMap = ConfigMap.empty + suites.foreach{ + _.execute(testName, configMap, color, durations, shortstacks, fullstacks, stats) + } + } + + def discoverSuites(discoveryPath: File, _classLoader: ClassLoader): Seq[String] = { + _classLoader + .loadClass("org.scalatest.tools.SuiteDiscoveryHelper") + .getMethod("discoverSuiteNames", classOf[List[_]], classOf[ClassLoader], classOf[Option[_]]) + .invoke(null, List(discoveryPath.string ++ "/"), _classLoader, None) + .asInstanceOf[Set[String]] + .to + } + def loadSuite(name: String, _classLoader: ClassLoader) = { + _classLoader.loadClass(name).getConstructor().newInstance().asInstanceOf[Suite] + } +} + diff --git a/plugins/scalatest/build/build.scala b/plugins/scalatest/build/build.scala new file mode 100644 index 0000000..dd21898 --- /dev/null +++ b/plugins/scalatest/build/build.scala @@ -0,0 +1,9 @@ +import cbt._ + +class Build(val context: Context) extends Plugin{ + override def dependencies = + super.dependencies ++ + Resolver( mavenCentral ).bind( + ScalaDependency("org.scalatest","scalatest","2.2.4") + ) +} diff --git a/plugins/sonatype-release/build/build.scala b/plugins/sonatype-release/build/build.scala new file mode 100644 index 0000000..0205cf8 --- /dev/null +++ b/plugins/sonatype-release/build/build.scala @@ -0,0 +1,3 @@ +import cbt._ + +class Build(val context: Context) extends Plugin diff --git a/plugins/sonatype-release/src/SonatypeRelease.scala b/plugins/sonatype-release/src/SonatypeRelease.scala new file mode 100644 index 0000000..cb32417 --- /dev/null +++ b/plugins/sonatype-release/src/SonatypeRelease.scala @@ -0,0 +1,51 @@ +package cbt + +import cbt.sonatype.SonatypeLib + +/** + * Sonatype release plugin. + * It provides ability to release your artifacts to Sonatype OSSRH + * and publish to Central repository (aka Maven Central). + * + * Release proccess is executed in two steps: + * • `sonatypePublishSigned` + * - creates staging repository to publish artifacts; + * - publishes signed artifacts(jars) to staging repository. + * • `sonatypeRelease` + * - closes staging repository; + * - promotes staging repository to Central repository; + * - drops staging repository after release. + */ +trait SonatypeRelease extends Publish { + + def profileName: String = groupId + + def sonatypeServiceURI: String = SonatypeLib.sonatypeServiceURI + + def sonatypeSnapshotsURI: String = SonatypeLib.sonatypeSnapshotsURI + + def sonatypeCredentials: String = SonatypeLib.sonatypeCredentials + + def sonatypePublishSigned: ExitCode = + sonatypeLib.sonatypePublishSigned( + sourceFiles, + `package` :+ pom, + groupId, + artifactId, + version, + isSnapshot, + scalaMajorVersion + ) + + def sonatypePublishSignedSnapshot: ExitCode = { + copy(context.copy(version = Some(version + "-SNAPSHOT"))).sonatypePublishSigned + } + + def sonatypeRelease: ExitCode = + sonatypeLib.sonatypeRelease(groupId, artifactId, version) + + private def sonatypeLib = + new SonatypeLib(sonatypeServiceURI, sonatypeSnapshotsURI, sonatypeCredentials, profileName)(lib) + + override def copy(context: Context) = super.copy(context).asInstanceOf[SonatypeRelease] +} diff --git a/plugins/sonatype-release/src/sonatype/HttpUtils.scala b/plugins/sonatype-release/src/sonatype/HttpUtils.scala new file mode 100644 index 0000000..9d23744 --- /dev/null +++ b/plugins/sonatype-release/src/sonatype/HttpUtils.scala @@ -0,0 +1,65 @@ +package cbt.sonatype + +import java.net.URL + +import cbt.Stage0Lib + +import scala.annotation.tailrec +import scala.util.{ Failure, Success, Try } + +private[sonatype] object HttpUtils { + // Make http GET. On failure request will be retried with exponential backoff. + def GET(uri: String, headers: Map[String, String]): (Int, String) = + withRetry(httpRequest("GET", uri, headers)) + + // Make http POST. On failure request will be retried with exponential backoff. + def POST(uri: String, body: Array[Byte], headers: Map[String, String]): (Int, String) = + withRetry(httpRequest("POST", uri, headers, body)) + + private def httpRequest(method: String, uri: String, headers: Map[String, String], body: Array[Byte] = Array.emptyByteArray): (Int, String) = { + val conn = Stage0Lib.openConnectionConsideringProxy(new URL(uri)) + conn.setReadTimeout(60000) // 1 minute + conn.setConnectTimeout(30000) // 30 seconds + + headers foreach { case (k,v) => + conn.setRequestProperty(k, v) + } + conn.setRequestMethod(method) + if(method == "POST" || method == "PUT") { // PATCH too? + conn.setDoOutput(true) + conn.getOutputStream.write(body) + } + + val arr = new Array[Byte](conn.getContentLength) + conn.getInputStream.read(arr) + + conn.getResponseCode -> new String(arr) + } + + // ============== General utilities + + def withRetry[T](f: => T): T = withRetry(4000, 5)(f) + + /** + * Retry execution of `f` `retriesLeft` times + * with `delay` doubled between attempts. + */ + @tailrec + def withRetry[T](delay: Int, retriesLeft: Int)(f: ⇒ T): T = { + Try(f) match { + case Success(result) ⇒ + result + case Failure(e) ⇒ + if (retriesLeft == 0) { + throw new Exception(e) + } else { + val newDelay = delay * 2 + val newRetries = retriesLeft - 1 +// log(s"Failed with exception: $e, will retry $newRetries times; waiting: $delay") + Thread.sleep(delay) + + withRetry(newDelay, newRetries)(f) + } + } + } +} diff --git a/plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala b/plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala new file mode 100644 index 0000000..e90b81d --- /dev/null +++ b/plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala @@ -0,0 +1,215 @@ +package cbt.sonatype + +import java.util.Base64 + +import scala.xml.XML + +/** + * Interface for Sonatype staging plugin HTTP API. + * All resources are described here: + * https://oss.sonatype.org/nexus-staging-plugin/default/docs/index.html + * + * Publish proccess via HTTP API described here: + * https://support.sonatype.com/hc/en-us/articles/213465868-Uploading-to-a-Staging-Repository-via-REST-API?page=1#comment_204178478 + */ +private final class SonatypeHttpApi(sonatypeURI: String, sonatypeCredentials: String, profileName: String)(log: String => Unit) { + import HttpUtils._ + + private val base64Credentials = new String(Base64.getEncoder.encode(sonatypeCredentials.getBytes)) + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles.html + def getStagingProfile: StagingProfile = { + log(s"Retrieving info for profile: $profileName") + val (_, response) = GET( + uri = s"$sonatypeURI/staging/profiles", + headers = Map("Authorization" -> s"Basic $base64Credentials") + ) + + val currentProfile = (XML.loadString(response) \\ "stagingProfile" find { profile => + (profile \ "name").headOption.exists(_.text == profileName) + }).getOrElse(throw new Exception(s"Failed to get profile with name: $profileName")) + + StagingProfile( + id = (currentProfile \ "id").head.text, + name = (currentProfile \ "name").head.text, + repositoryTargetId = (currentProfile \ "repositoryTargetId").head.text, + resourceURI = (currentProfile \ "resourceURI").head.text + ) + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profile_repositories_-profileIdKey-.html + def getStagingRepos(profile: StagingProfile): Seq[StagingRepository] = { + log(s"Retrieving staging repositories for profile: $profileName") + val (_, response) = GET( + uri = s"$sonatypeURI/staging/profile_repositories/${profile.id}", + headers = Map( + "Authorization" -> s"Basic $base64Credentials" + ) + ) + + (XML.loadString(response) \\ "stagingProfileRepository") map extractStagingRepository + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_repository_-repositoryIdKey-.html + private def getStagingRepoById(repoId: StagingRepositoryId): StagingRepository = { + log(s"Retrieving staging repo with id: ${repoId.repositoryId}") + val (_, response) = GET( + uri = s"$sonatypeURI/staging/repository/${repoId.repositoryId}", + headers = Map( + "Authorization" -> s"Basic $base64Credentials" + ) + ) + + extractStagingRepository(XML.loadString(response)) + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_start.html + def createStagingRepo(profile: StagingProfile): StagingRepositoryId = { + log(s"Creating staging repositories for profile: $profileName") + val (responseCode, response) = POST( + uri = profile.resourceURI + "/start", + body = createRequestBody("Create staging repository [CBT]").getBytes, + headers = Map( + "Authorization" -> s"Basic $base64Credentials", + "Content-Type" -> "application/xml" + ) + ) + + require(responseCode == 201, s"Create staging repo response code. Expected: 201, got: $responseCode") + + val optRepositoryId = (XML.loadString(response) \ "data" \ "stagedRepositoryId").headOption.map(e => StagingRepositoryId(e.text)) + + optRepositoryId.getOrElse(throw new Exception(s"Malformed response. Failed to get id of created staging repo")) + } + + def finishRelease(repo: StagingRepository, profile: StagingProfile): Unit = { + val repoId = StagingRepositoryId(repo.repositoryId) + repo.state match { + case Open => + closeStagingRepo(profile, repoId) + promoteStagingRepo(profile, repoId) + dropStagingRepo(profile, repoId) + case Closed => + promoteStagingRepo(profile, repoId) + dropStagingRepo(profile, repoId) + case Released => + dropStagingRepo(profile, repoId) + case Unknown(status) => + throw new Exception(s"Got repo in status: ${status}, can't finish release.") + } + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_finish.html + private def closeStagingRepo(profile: StagingProfile, repoId: StagingRepositoryId): Unit = { + log(s"Closing staging repo: ${repoId.repositoryId}") + val (responseCode, _) = POST( + uri = profile.resourceURI + "/finish", + body = promoteRequestBody( + repoId.repositoryId, + "Close staging repository [CBT]", + profile.repositoryTargetId + ).getBytes, + headers = Map( + "Authorization" -> s"Basic $base64Credentials", + "Content-Type" -> "application/xml" + ) + ) + + require(responseCode == 201, s"Close staging repo response code. Expected: 201, got: $responseCode") + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_promote.html + // You can promote repository only when it is in "closed" state. + private def promoteStagingRepo(profile: StagingProfile, repoId: StagingRepositoryId): Unit = { + log(s"Promoting staging repo: ${repoId.repositoryId}") + val responseCode = withRetry { + // need to get fresh info about this repo + val repoState = try getStagingRepoById(repoId) catch { + case e: Exception => + throw new Exception(s"Repository with id ${repoId.repositoryId} not found. Maybe it was dropped already", e) + } + + if(repoState.state == Closed) { + val (code, _) = POST( + uri = profile.resourceURI + "/promote", + body = promoteRequestBody( + repoId.repositoryId, + "Promote staging repository [CBT]", + profile.repositoryTargetId + ).getBytes, + headers = Map( + "Authorization" -> s"Basic $base64Credentials", + "Content-Type" -> "application/xml" + ) + ) + code + } else { + throw new Exception(s"Can't promote, repository ${repoId.repositoryId} is not in closed state yet!") + } + } + + require(responseCode == 201, s"Promote staging repo response code. Expected: 201, got: $responseCode") + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_drop.html + // It's safe to drop repository in "released" state. + private def dropStagingRepo(profile: StagingProfile, repoId: StagingRepositoryId): Unit = { + log(s"Dropping staging repo: ${repoId.repositoryId}") + val responseCode = withRetry { + // need to get fresh info about this repo + val repoState = try getStagingRepoById(repoId) catch { + case e: Exception => + throw new Exception(s"Repository with id ${repoId.repositoryId} not found. Maybe it was dropped already", e) + } + + if (repoState.state == Released) { + val (code, _) = POST( + uri = profile.resourceURI + "/drop", + body = promoteRequestBody( + repoId.repositoryId, + "Drop staging repository [CBT]", + profile.repositoryTargetId + ).getBytes, + headers = Map( + "Authorization" -> s"Basic $base64Credentials", + "Content-Type" -> "application/xml" + ) + ) + code + } else { + throw new Exception(s"Can't drop, repository ${repoId.repositoryId} is not in released state yet!") + } + } + require(responseCode == 201, s"Drop staging repo response code. Expected: 201, got: $responseCode") + } + + private def promoteRequestBody(repoId: String, description: String, targetRepoId: String) = + s""" + |<promoteRequest> + | <data> + | <stagedRepositoryId>$repoId</stagedRepositoryId> + | <description>$description</description> + | <targetRepositoryId>$targetRepoId</targetRepositoryId> + | </data> + |</promoteRequest> + """.stripMargin + + + private def createRequestBody(description: String) = + s""" + |<promoteRequest> + | <data> + | <description>$description</description> + | </data> + |</promoteRequest> + """.stripMargin + + private def extractStagingRepository(repo: xml.Node): StagingRepository = + StagingRepository( + (repo \ "profileId").head.text, + (repo \ "profileName").head.text, + (repo \ "repositoryId").head.text, + RepositoryState.fromString((repo \ "type").head.text) + ) +} + diff --git a/plugins/sonatype-release/src/sonatype/SonatypeLib.scala b/plugins/sonatype-release/src/sonatype/SonatypeLib.scala new file mode 100644 index 0000000..9aab9f5 --- /dev/null +++ b/plugins/sonatype-release/src/sonatype/SonatypeLib.scala @@ -0,0 +1,148 @@ +package cbt.sonatype + +import java.io.File +import java.net.URL +import java.nio.file.Files._ +import java.nio.file.Paths + +import cbt.{ ExitCode, Lib } + +/** + * Sonatype release process is: + * • get your profile info to publish artifacts + * • open staging repository to publish artifacts + * • publish signed artifacts and signatures to staging repository + * • close staging repository + * • promote staging repository + * • drop staging repository + */ + +object SonatypeLib { + + val sonatypeServiceURI: String = "https://oss.sonatype.org/service/local" + + val sonatypeSnapshotsURI: String = "https://oss.sonatype.org/content/repositories/snapshots" + + /** + * login:password for Sonatype access. + * Order of credentials lookup: + * • environment variables SONATYPE_USERNAME and SONATYPE_PASSWORD + * • ~/.cbt/sonatype-credentials + */ + def sonatypeCredentials: String = { + def fromEnv = for { + username <- Option(System.getenv("SONATYPE_USERNAME")) + password <- Option(System.getenv("SONATYPE_PASSWORD")) + } yield s"$username:$password" + + def fromFile = { + for { + home <- Option(System.getProperty("user.home")) + credsPath = Paths.get(home, ".cbt", "sonatype-credentials") + } yield new String(readAllBytes(credsPath)).trim + } + + fromEnv + .orElse(fromFile) + .getOrElse(throw new Exception( + "No Sonatype credentials found! You can provide them via SONATYPE_USERNAME, SONATYPE_PASSWORD env variables, " + + "or in ~/.cbt/sonatype-credentials file as login:password" + )) + } +} + +final class SonatypeLib( + sonatypeServiceURI: String, + sonatypeSnapshotsURI: String, + sonatypeCredentials: String, + profileName: String)(lib: Lib) { + + private val sonatypeApi = new SonatypeHttpApi(sonatypeServiceURI, sonatypeCredentials, profileName)(sonatypeLogger) + + /* + * Signed publish steps: + * • create new staging repo + * • create artifacts and sign them + * • publish jars to created repo + */ + def sonatypePublishSigned( + sourceFiles: Seq[File], + artifacts: Seq[File], + groupId: String, + artifactId: String, + version: String, + isSnapshot: Boolean, + scalaMajorVersion: String + ): ExitCode = { + if(sourceFiles.nonEmpty) { + System.err.println(lib.blue("Staring publishing to Sonatype.")) + + val profile = getStagingProfile() + + val deployURI = (if (isSnapshot) { + sonatypeSnapshotsURI + } else { + val repoId = sonatypeApi.createStagingRepo(profile) + s"${sonatypeServiceURI}/staging/deployByRepositoryId/${repoId.repositoryId}" + }) + s"/${groupId.replace(".", "/")}/${artifactId}_${scalaMajorVersion}/${version}" + + lib.publishSigned( + artifacts = artifacts, + url = new URL(deployURI), + credentials = Some(sonatypeCredentials) + ) + System.err.println(lib.green("Successfully published artifacts to Sonatype.")) + ExitCode.Success + } else { + System.err.println(lib.red("Sources are empty, won't publish empty jar.")) + ExitCode.Failure + } + } + + /** + * Release is: + * • find staging repo related to current profile; + * • close this staging repo; + * • wait until this repo is released; + * • drop this repo. + */ + def sonatypeRelease( + groupId: String, + artifactId: String, + version: String + ): ExitCode = { + val profile = getStagingProfile() + + sonatypeApi.getStagingRepos(profile).toList match { + case Nil => + System.err.println(lib.red("No staging repositories found, you need to publish artifacts first.")) + ExitCode.Failure + case repo :: Nil => + sonatypeApi.finishRelease(repo, profile) + System.err.println(lib.green(s"Successfully released ${groupId}/${artifactId} v:${version}")) + ExitCode.Success + case repos => + val showRepo = { r: StagingRepository => s"${r.repositoryId} in state: ${r.state}" } + val toRelease = lib.pickOne(lib.blue(s"More than one staging repo found. Select one of them:"), repos)(showRepo) + + toRelease map { repo => + sonatypeApi.finishRelease(repo, profile) + System.err.println(lib.green(s"Successfully released ${groupId}/${artifactId} v:${version}")) + ExitCode.Success + } getOrElse { + System.err.println(lib.red("Wrong repository number, try again please.")) + ExitCode.Failure + } + } + } + + private def getStagingProfile() = + try { + sonatypeApi.getStagingProfile + } catch { + case e: Exception => throw new Exception(s"Failed to get info for profile: $profileName", e) + } + + private def sonatypeLogger: String => Unit = lib.logger.log("Sonatype", _) + +} diff --git a/plugins/sonatype-release/src/sonatype/models.scala b/plugins/sonatype-release/src/sonatype/models.scala new file mode 100644 index 0000000..4446c53 --- /dev/null +++ b/plugins/sonatype-release/src/sonatype/models.scala @@ -0,0 +1,31 @@ +package cbt.sonatype + +case class StagingProfile( + id: String, + name: String, + repositoryTargetId: String, + resourceURI: String + ) + +case class StagingRepositoryId(repositoryId: String) + +object RepositoryState { + val fromString: String => RepositoryState = { + case "open" => Open + case "closed" => Closed + case "released" => Released + case other => Unknown(other) + } +} +sealed trait RepositoryState +case object Open extends RepositoryState +case object Closed extends RepositoryState +case object Released extends RepositoryState +case class Unknown(state: String) extends RepositoryState + +case class StagingRepository( + profileId: String, + profileName: String, + repositoryId: String, + state: RepositoryState // stands as `type` in XML response + ) diff --git a/plugins/uber-jar/build/build.scala b/plugins/uber-jar/build/build.scala new file mode 100644 index 0000000..0205cf8 --- /dev/null +++ b/plugins/uber-jar/build/build.scala @@ -0,0 +1,3 @@ +import cbt._ + +class Build(val context: Context) extends Plugin diff --git a/plugins/uber-jar/src/UberJar.scala b/plugins/uber-jar/src/UberJar.scala new file mode 100644 index 0000000..3783367 --- /dev/null +++ b/plugins/uber-jar/src/UberJar.scala @@ -0,0 +1,124 @@ +package cbt + +import java.io.File +import java.nio.file.{FileSystems, Files, Path} +import java.util.jar.JarFile + +trait UberJar extends BaseBuild { + + final def uberJar: ExitCode = { + System.err.println("Creating uber jar...") + new UberJarLib(logger).create(target, classpath, uberJarMainClass, uberJarName) + System.err.println(lib.green("Creating uber jar - DONE")) + ExitCode.Success + } + + def uberJarMainClass: Option[String] = runClass + + def uberJarName: String = projectName + ".jar" + +} + +class UberJarLib(logger: Logger) { + private val (jarFileMatcher, excludeFileMatcher) = { + val fs = FileSystems.getDefault + (fs.getPathMatcher("glob:**.jar"), fs.getPathMatcher("glob:**{.RSA,.DSA,.SF,.MF,META-INF}")) + } + private val log: String => Unit = logger.log("uber-jar", _) + private val lib = new cbt.Lib(logger) + + /** + * Creates uber jar for given build. + * + * @param target build's target directory + * @param classpath build's classpath + * @param mainClass optional main class + * @param jarName name of resulting jar file + */ + def create(target: File, + classpath: ClassPath, + mainClass: Option[String], + jarName: String): Unit = { + log(s"Classpath is: $classpath") + log(s"Target directory is: $target") + log(s"Jar name is: $jarName") + mainClass foreach (c => log(s"Main class is is: $c")) + + val (jars, dirs) = classpath.files partition (f => jarFileMatcher.matches(f.toPath)) + log(s"Found ${jars.length} jar dependencies: \n ${jars mkString "\n"}") + log(s"Found ${dirs.length} directories in classpath: \n ${dirs mkString "\n"}") + + log("Extracting jars...") + val extractedJarsRoot = extractJars(jars.distinct)(log).toFile + log("Extracting jars - DONE") + + log("Writing jar file...") + val uberJarPath = target.toPath.resolve(jarName) + val uberJar = lib.jarFile(uberJarPath.toFile, dirs :+ extractedJarsRoot, mainClass) getOrElse { + throw new Exception("Jar file wasn't created!") + } + log("Writing jar file - DONE") + + System.err.println(lib.green(s"Uber jar created. You can grab it at $uberJar")) + } + + /** + * Extracts jars, and writes them on disk. Returns root directory of extracted jars + * TODO: in future we probably should save extracted jars in target directory, to reuse them on second run + * + * @param jars list of *.jar files + * @param log logger + * @return root directory of extracted jars + */ + private def extractJars(jars: Seq[File])(log: String => Unit): Path = { + val destDir = { + val path = Files.createTempDirectory("unjars") + path.toFile.deleteOnExit() + log(s"Extracted jars directory: $path") + path + } + jars foreach { jar => extractJar(jar, destDir)(log) } + destDir + } + + /** + * Extracts content of single jar file to destination directory. + * When extracting jar, if same file already exists, we skip(don't write) this file. + * TODO: maybe skipping duplicates is not best strategy. Figure out duplicate strategy. + * + * @param jarFile jar file to extract + * @param destDir destination directory + * @param log logger + */ + private def extractJar(jarFile: File, destDir: Path)(log: String => Unit): Unit = { + log(s"Extracting jar: $jarFile") + val jar = new JarFile(jarFile) + val enumEntries = jar.entries + while (enumEntries.hasMoreElements) { + val entry = enumEntries.nextElement() + // log(s"Entry name: ${entry.getName}") + val entryPath = destDir.resolve(entry.getName) + if (excludeFileMatcher.matches(entryPath)) { + log(s"Excluded file ${entryPath.getFileName} from jar: $jarFile") + } else { + val exists = Files.exists(entryPath) + if (entry.isDirectory) { + if (!exists) { + Files.createDirectory(entryPath) + // log(s"Created directory: $entryPath") + } + } else { + if (exists) { + log(s"File $entryPath already exists, skipping.") + } else { + val is = jar.getInputStream(entry) + Files.copy(is, entryPath) + is.close() + // log(s"Wrote file: $entryPath") + } + } + } + } + } + +} diff --git a/plugins/wartremover/WartRemover.scala b/plugins/wartremover/WartRemover.scala new file mode 100644 index 0000000..d5bbcd0 --- /dev/null +++ b/plugins/wartremover/WartRemover.scala @@ -0,0 +1,51 @@ +package cbt + +import org.wartremover.WartTraverser +import java.io.File + +trait WartRemover extends BaseBuild { + + override def scalacOptions = + super.scalacOptions ++ wartremoverScalacOptions + + private[this] def wartremoverCompilerDependency: String = + MavenResolver( + context.cbtHasChanged, + context.paths.mavenCache, + mavenCentral).bindOne( + ScalaDependency("org.wartremover", "wartremover", "1.1.1") + ).jar.string + + private[this] def wartremoverScalacOptions: Seq[String] = + Seq("-Xplugin:" ++ wartremoverCompilerDependency) ++ + wartremoverErrorsScalacOptions ++ + wartremoverWarningsScalacOptions ++ + wartremoverExcludedScalacOptions ++ + wartremoverClasspathsScalacOptions + + private[this] def wartremoverErrorsScalacOptions: Seq[String] = + wartremoverErrors.distinct.map(w => s"-P:wartremover:traverser:${w.className}") + + private[this] def wartremoverWarningsScalacOptions: Seq[String] = + wartremoverWarnings.distinct + .filterNot(wartremoverErrors contains _) + .map(w => s"-P:wartremover:only-warn-traverser:${w.className}") + + private[this] def wartremoverExcludedScalacOptions: Seq[String] = + wartremoverExcluded.distinct.map(c => s"-P:wartremover:excluded:${c.getAbsolutePath}") + + private[this] def wartremoverClasspathsScalacOptions: Seq[String] = + wartremoverClasspaths.distinct.map(cp => s"-P:wartremover:cp:$cp") + + /** List of Warts that will be reported as compilation errors. */ + def wartremoverErrors: Seq[WartTraverser] = Seq.empty + + /** List of Warts that will be reported as compilation warnings. */ + def wartremoverWarnings: Seq[WartTraverser] = Seq.empty + + /** List of files to be excluded from all checks. */ + def wartremoverExcluded: Seq[File] = Seq.empty + + /** List of classpaths for custom Warts. */ + def wartremoverClasspaths: Seq[String] = Seq.empty +} diff --git a/plugins/wartremover/build/build.scala b/plugins/wartremover/build/build.scala new file mode 100644 index 0000000..d62c571 --- /dev/null +++ b/plugins/wartremover/build/build.scala @@ -0,0 +1,9 @@ +import cbt._ + +class Build(val context: Context) extends Plugin { + override def dependencies = + super.dependencies ++ + Resolver( mavenCentral ).bind( + ScalaDependency("org.wartremover", "wartremover", "1.1.1") + ) +} diff --git a/realpath/realpath.c b/realpath/realpath.c new file mode 100644 index 0000000..055dbcf --- /dev/null +++ b/realpath/realpath.c @@ -0,0 +1,35 @@ +// http://stackoverflow.com/questions/284662/how-do-you-normalize-a-file-path-in-bash +// realpath.c: display the absolute path to a file or directory. +// Adam Liss, August, 2007 +// This program is provided "as-is" to the public domain, without express or +// implied warranty, for any non-profit use, provided this notice is maintained. + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <libgen.h> +#include <limits.h> + +static char *s_pMyName; +void usage(void); + +int main(int argc, char *argv[]) +{ + char + sPath[PATH_MAX]; + + + s_pMyName = strdup(basename(argv[0])); + + if (argc < 2) + usage(); + + printf("%s\n", realpath(argv[1], sPath)); + return 0; +} + +void usage(void) +{ + fprintf(stderr, "usage: %s PATH\n", s_pMyName); + exit(1); +}
\ No newline at end of file diff --git a/realpath/realpath.sh b/realpath/realpath.sh new file mode 100755 index 0000000..de4d964 --- /dev/null +++ b/realpath/realpath.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# is there a realiable cross-platform was to do this without relying on compiling C code? + +DIR=$(dirname $(readlink "$0") 2>/dev/null || dirname "$0" 2>/dev/null ) +which realpath 2>&1 > /dev/null +REALPATH_INSTALLED=$? + +if [ ! $REALPATH_INSTALLED -eq 0 ]; then + if [ ! -f $DIR/realpath ]; then + >&2 echo "Compiling realpath" + gcc $DIR/realpath.c -o $DIR/realpath + chmod u+x $DIR/realpath + fi + $DIR/realpath $1 +else + realpath $1 +fi diff --git a/shell-integration/cbt-completions.bash b/shell-integration/cbt-completions.bash new file mode 100755 index 0000000..925ba4b --- /dev/null +++ b/shell-integration/cbt-completions.bash @@ -0,0 +1,13 @@ +#!/bin/bash +__cbt() +{ + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="$(cbt taskNames)" + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 +} + +complete -F __cbt cbt diff --git a/shell-integration/cbt-completions.fish b/shell-integration/cbt-completions.fish new file mode 100644 index 0000000..6e543fb --- /dev/null +++ b/shell-integration/cbt-completions.fish @@ -0,0 +1 @@ +complete -c cbt -a (cbt taskNames) diff --git a/shell-integration/cbt-completions.zsh b/shell-integration/cbt-completions.zsh new file mode 100755 index 0000000..d11307d --- /dev/null +++ b/shell-integration/cbt-completions.zsh @@ -0,0 +1,7 @@ +#! /usr/bin/env zsh + +_cbt() { + reply=( "${(ps:\n:)$(cbt taskNames)}" ) +} + +compctl -K _cbt cbt diff --git a/stage1/Cache.scala b/stage1/Cache.scala new file mode 100644 index 0000000..a8036e5 --- /dev/null +++ b/stage1/Cache.scala @@ -0,0 +1,14 @@ +package cbt +/** +Caches exactly one value. +Is there a less boiler-platy way to achieve this, that doesn't +require creating an instance for each thing you want to cache? +*/ +class Cache[T]{ + private var value: Option[T] = None + def apply(value: => T) = this.synchronized{ + if(!this.value.isDefined) + this.value = Some(value) + this.value.get + } +} diff --git a/stage1/CachingClassLoader.scala b/stage1/CachingClassLoader.scala new file mode 100644 index 0000000..4ddebda --- /dev/null +++ b/stage1/CachingClassLoader.scala @@ -0,0 +1,17 @@ +package cbt +import java.net._ +import java.util.concurrent.ConcurrentHashMap +import scala.util.Try + +trait CachingClassLoader extends ClassLoader{ + def logger: Logger + val cache = new KeyLockedLazyCache[String,Option[Class[_]]]( new ConcurrentHashMap, new ConcurrentHashMap, Some(logger) ) + override def loadClass(name: String, resolve: Boolean) = { + cache.get( name, Try(super.loadClass(name, resolve)).toOption ).getOrElse(null) + } + override def loadClass(name: String) = { + val _class = super.loadClass(name) + if(_class == null) throw new ClassNotFoundException(name) + else _class + } +} diff --git a/stage1/CbtPaths.scala b/stage1/CbtPaths.scala new file mode 100644 index 0000000..71c2ef1 --- /dev/null +++ b/stage1/CbtPaths.scala @@ -0,0 +1,15 @@ +package cbt +import java.io._ +case class CbtPaths(private val cbtHome: File, private val cache: File){ + val userHome: File = new File(Option(System.getProperty("user.home")).get) + val nailgun: File = cbtHome ++ "/nailgun_launcher" + val stage1: File = cbtHome ++ "/stage1" + val stage2: File = cbtHome ++ "/stage2" + val mavenCache: File = cache ++ "/maven" + private val target = NailgunLauncher.TARGET.stripSuffix("/") + val stage1Target: File = stage1 ++ ("/" ++ target) + val stage2Target: File = stage2 ++ ("/" ++ target) + val stage2StatusFile: File = stage2Target ++ ".last-success" + val compatibility: File = cbtHome ++ "/compatibility" + val nailgunTarget: File = nailgun ++ ("/" ++ target) +} diff --git a/stage1/ClassLoaderCache.scala b/stage1/ClassLoaderCache.scala new file mode 100644 index 0000000..e430ee1 --- /dev/null +++ b/stage1/ClassLoaderCache.scala @@ -0,0 +1,24 @@ +package cbt + +import java.net._ +import java.util.concurrent.ConcurrentHashMap +import collection.JavaConversions._ + +case class ClassLoaderCache( + logger: Logger, + private[cbt] permanentKeys: ConcurrentHashMap[String,AnyRef], + private[cbt] permanentClassLoaders: ConcurrentHashMap[AnyRef,ClassLoader] +){ + val persistent = new KeyLockedLazyCache( + permanentKeys, + permanentClassLoaders, + Some(logger) + ) + override def toString = ( + s"ClassLoaderCache(" + ++ + persistent.keys.keySet.toVector.map(_.toString.split(":").mkString("\n")).sorted.mkString("\n\n","\n\n","\n\n") + ++ + ")" + ) +} diff --git a/stage1/ClassPath.scala b/stage1/ClassPath.scala new file mode 100644 index 0000000..6e6f113 --- /dev/null +++ b/stage1/ClassPath.scala @@ -0,0 +1,28 @@ +package cbt +import java.io._ +import java.net._ + +object ClassPath{ + def flatten( classPaths: Seq[ClassPath] ): ClassPath = ClassPath( classPaths.map(_.files).flatten ) +} +case class ClassPath(files: Seq[File] = Seq()){ + private val duplicates = (files diff files.distinct).distinct + assert( + duplicates.isEmpty, + "Duplicate classpath entries found:\n" ++ duplicates.mkString("\n") ++ "\nin classpath:\n"++string + ) + private val nonExisting = files.distinct.filterNot(_.exists) + assert( + nonExisting.isEmpty, + "Classpath contains entires that don't exist on disk:\n" ++ nonExisting.mkString("\n") ++ "\nin classpath:\n"++string + ) + + def +:(file: File) = ClassPath(file +: files) + def :+(file: File) = ClassPath(files :+ file) + def ++(other: ClassPath) = ClassPath(files ++ other.files) + def string = strings.mkString( File.pathSeparator ) + def strings = files.map{ + f => f.string ++ ( if(f.isDirectory) "/" else "" ) + }.sorted + def toConsole = string +} diff --git a/stage1/ContextImplementation.scala b/stage1/ContextImplementation.scala new file mode 100644 index 0000000..91c54f4 --- /dev/null +++ b/stage1/ContextImplementation.scala @@ -0,0 +1,22 @@ +package cbt +import java.io._ +import java.util.concurrent.ConcurrentHashMap +import java.lang._ + +case class ContextImplementation( + projectDirectory: File, + cwd: File, + argsArray: Array[String], + enabledLoggersArray: Array[String], + startCompat: Long, + cbtHasChangedCompat: Boolean, + versionOrNull: String, + scalaVersionOrNull: String, + permanentKeys: ConcurrentHashMap[String,AnyRef], + permanentClassLoaders: ConcurrentHashMap[AnyRef,ClassLoader], + cache: File, + cbtHome: File, + cbtRootHome: File, + compatibilityTarget: File, + parentBuildOrNull: BuildInterface +) extends Context
\ No newline at end of file diff --git a/stage1/KeyLockedLazyCache.scala b/stage1/KeyLockedLazyCache.scala new file mode 100644 index 0000000..4eff5b2 --- /dev/null +++ b/stage1/KeyLockedLazyCache.scala @@ -0,0 +1,62 @@ +package cbt + +import java.util.concurrent.ConcurrentHashMap + +private[cbt] class LockableKey +/** +A cache that lazily computes values if needed during lookup. +Locking occurs on the key, so separate keys can be looked up +simultaneously without a deadlock. +*/ +final private[cbt] class KeyLockedLazyCache[Key <: AnyRef,Value <: AnyRef]( + val keys: ConcurrentHashMap[Key,AnyRef], + val values: ConcurrentHashMap[AnyRef,Value], + logger: Option[Logger] +){ + def get( key: Key, value: => Value ): Value = { + val lockableKey = keys.synchronized{ + if( ! (keys containsKey key) ){ + val lockableKey = new LockableKey + //logger.foreach(_.resolver("CACHE MISS: " ++ key.toString)) + keys.put( key, lockableKey ) + lockableKey + } else { + val lockableKey = keys get key + //logger.foreach(_.resolver("CACHE HIT: " ++ lockableKey.toString ++ " -> " ++ key.toString)) + lockableKey + } + } + import collection.JavaConversions._ + //logger.resolver("CACHE: \n" ++ keys.mkString("\n")) + // synchronizing on key only, so asking for a particular key does + // not block the whole cache, but just that cache entry + lockableKey.synchronized{ + if( ! (values containsKey lockableKey) ){ + values.put( lockableKey, value ) + } + values get lockableKey + } + } + def update( key: Key, value: Value ): Value = { + val lockableKey = keys get key + lockableKey.synchronized{ + values.put( lockableKey, value ) + value + } + } + def remove( key: Key ) = keys.synchronized{ + assert(keys containsKey key) + val lockableKey = keys get key + lockableKey.synchronized{ + if(values containsKey lockableKey){ + // this is so values in the process of being replaced (which mean they have a key but no value) + // are not being removed + keys.remove( key ) + values.remove( lockableKey ) + } + } + } + def containsKey( key: Key ) = keys.synchronized{ + keys containsKey key + } +} diff --git a/stage1/MavenRepository.scala b/stage1/MavenRepository.scala new file mode 100644 index 0000000..4184d2d --- /dev/null +++ b/stage1/MavenRepository.scala @@ -0,0 +1,9 @@ +package cbt +import java.io._ +import java.net._ +case class MavenResolver( cbtHasChanged: Boolean, mavenCache: File, urls: URL* ){ + def bind( dependencies: MavenDependency* )(implicit logger: Logger): Seq[BoundMavenDependency] + = dependencies.map( BoundMavenDependency(cbtHasChanged,mavenCache,_,urls.to) ).to + def bindOne( dependency: MavenDependency )(implicit logger: Logger): BoundMavenDependency + = BoundMavenDependency( cbtHasChanged, mavenCache, dependency, urls.to ) +} diff --git a/stage1/MultiClassLoader.scala b/stage1/MultiClassLoader.scala new file mode 100644 index 0000000..9546d47 --- /dev/null +++ b/stage1/MultiClassLoader.scala @@ -0,0 +1,42 @@ +package cbt +import java.net._ +import scala.collection.JavaConverters._ + +// do not make this a case class, required object identity equality +class MultiClassLoader(final val parents: Seq[ClassLoader])(implicit val logger: Logger) extends ClassLoader(null) with CachingClassLoader{ + override def findClass(name: String) = { + parents.find( parent => + try{ + null != parent.loadClass(name) // FIXME: is it correct to just ignore the resolve argument here? + } catch { + case _:ClassNotFoundException => false + } + ).map( + _.loadClass(name) + ).getOrElse( null ) + } + + // FIXME: is there more than findClass and findResource that needs to be dispatched? + override def findResource(name: String): URL = { + parents.foldLeft(null: URL)( + (acc, parent) => if( acc == null ) parent.getResource(name) else null + ) + } + override def findResources(name: String): java.util.Enumeration[URL] = { + java.util.Collections.enumeration( + parents.flatMap( _.getResources(name).asScala ).asJava + ) + } + + override def toString = ( + scala.Console.BLUE + ++ super.toString + ++ scala.Console.RESET + ++ "(" + ++ ( + if(parents.nonEmpty)( + "\n" ++ parents.map(_.toString).mkString(",\n").split("\n").map(" "++_).mkString("\n") ++ "\n" + ) else "" + ) ++")" + ) +} diff --git a/stage1/PoorMansProfiler.scala b/stage1/PoorMansProfiler.scala new file mode 100644 index 0000000..b7aa47d --- /dev/null +++ b/stage1/PoorMansProfiler.scala @@ -0,0 +1,23 @@ +/* +// temporary debugging tool +package cbt +import java.util.concurrent.ConcurrentHashMap +import collection.JavaConversions._ +object PoorMansProfiler{ + val entries = new ConcurrentHashMap[String, Long] + def profile[T](name: String)(code: => T): T = { + val before = System.currentTimeMillis + if(!(entries containsKey name)){ + entries.put( name, 0 ) + } + val res = code + entries.put( name, (entries get name) + (System.currentTimeMillis - before) ) + res + } + def summary: String = { + "Profiling Summary:\n" + entries.toSeq.sortBy(_._2).map{ + case (name, value) => name + ": " + (value / 1000.0) + }.mkString("\n") + } +} +*/
\ No newline at end of file diff --git a/stage1/Stage1.scala b/stage1/Stage1.scala new file mode 100644 index 0000000..c94d1a4 --- /dev/null +++ b/stage1/Stage1.scala @@ -0,0 +1,195 @@ +package cbt + +import java.io._ +import java.util.concurrent.ConcurrentHashMap + +import scala.collection.JavaConverters._ + +final case class Stage1ArgsParser(__args: Seq[String]) { + val _args = __args.drop(1) + /** + * Raw parameters including their `-D` flag. + **/ + val propsRaw: Seq[String] = _args.toVector.filter(_.startsWith("-D")) + + /** + * All arguments that weren't `-D` property declarations. + **/ + val args: Seq[String] = _args.toVector diff propsRaw + + /** + * Parsed properties, as a map of keys to values. + **/ + val props = propsRaw + .map(_.drop(2).split("=")).map({ + case Array(key, value) => + key -> value + }).toMap ++ System.getProperties.asScala + + val enabledLoggers = props.get("log") + + val tools = _args contains "tools" +} + + +abstract class Stage2Base{ + def run( context: Stage2Args ): Unit +} + +case class Stage2Args( + cwd: File, + args: Seq[String], + cbtHasChanged: Boolean, + classLoaderCache: ClassLoaderCache, + cache: File, + cbtHome: File, + compatibilityTarget: File +){ + val ClassLoaderCache( + logger, + permanentKeys, + permanentClassLoaders + ) = classLoaderCache +} +object Stage1{ + protected def newerThan( a: File, b: File ) ={ + a.lastModified > b.lastModified + } + + def getBuild( _context: java.lang.Object, _cbtChanged: java.lang.Boolean ) = { + val context = _context.asInstanceOf[Context] + val logger = new Logger( context.enabledLoggers, context.start ) + val (changed, classLoader) = buildStage2( + context.compatibilityTarget, + ClassLoaderCache( + logger, + context.permanentKeys, + context.permanentClassLoaders + ), + _cbtChanged, + context.cbtHome, + context.cache + ) + + classLoader + .loadClass("cbt.Stage2") + .getMethod( "getBuild", classOf[java.lang.Object], classOf[java.lang.Boolean] ) + .invoke(null, context, (_cbtChanged || changed): java.lang.Boolean) + } + + def buildStage2( + compatibilityTarget: File, classLoaderCache: ClassLoaderCache, _cbtChanged: Boolean, cbtHome: File, cache: File + ): (Boolean, ClassLoader) = { + import classLoaderCache.logger + + val lib = new Stage1Lib(logger) + import lib._ + val paths = CbtPaths(cbtHome, cache) + import paths._ + + val stage2sourceFiles = ( + stage2.listFiles ++ (stage2 ++ "/plugins").listFiles + ).toVector.filter(_.isFile).filter(_.toString.endsWith(".scala")) + + val cbtHasChanged = _cbtChanged || lib.needsUpdate(stage2sourceFiles, stage2StatusFile) + + val cls = this.getClass.getClassLoader.loadClass("cbt.NailgunLauncher") + + val cbtDependency = CbtDependency(cbtHasChanged, mavenCache, nailgunTarget, stage1Target, stage2Target, compatibilityTarget) + + logger.stage1("Compiling stage2 if necessary") + compile( + cbtHasChanged, + cbtHasChanged, + stage2sourceFiles, stage2Target, stage2StatusFile, + cbtDependency.dependencyClasspath, + mavenCache, + Seq("-deprecation","-feature","-unchecked"), classLoaderCache, + zincVersion = "0.3.9", scalaVersion = constants.scalaVersion + ) + + logger.stage1(s"calling CbtDependency.classLoader") + if( cbtHasChanged && classLoaderCache.persistent.containsKey( cbtDependency.classpath.string ) ) { + classLoaderCache.persistent.remove( cbtDependency.classpath.string ) + } + + val stage2ClassLoader = cbtDependency.classLoader(classLoaderCache) + + { + // a few classloader sanity checks + val compatibilityClassLoader = + cbtDependency.stage1Dependency.compatibilityDependency.classLoader(classLoaderCache) + assert( + classOf[BuildInterface].getClassLoader == compatibilityClassLoader, + classOf[BuildInterface].getClassLoader.toString ++ "\n\nis not the same as\n\n" ++ compatibilityClassLoader.toString + ) + //------------- + val stage1ClassLoader = + cbtDependency.stage1Dependency.classLoader(classLoaderCache) + assert( + classOf[Stage1Dependency].getClassLoader == stage1ClassLoader, + classOf[Stage1Dependency].getClassLoader.toString ++ "\n\nis not the same as\n\n" ++ stage1ClassLoader.toString + ) + //------------- + assert( + Stage0Lib.get(stage2ClassLoader.getParent,"parents").asInstanceOf[Seq[ClassLoader]].contains(stage1ClassLoader), + stage1ClassLoader.toString ++ "\n\nis not contained in parents of\n\n" ++ stage2ClassLoader.toString + ) + } + + ( cbtHasChanged, stage2ClassLoader ) + } + + def run( + _args: Array[String], + cache: File, + cbtHome: File, + _cbtChanged: java.lang.Boolean, + compatibilityTarget: File, + start: java.lang.Long, + classLoaderCacheKeys: ConcurrentHashMap[String,AnyRef], + classLoaderCacheValues: ConcurrentHashMap[AnyRef,ClassLoader] + ): Int = { + val args = Stage1ArgsParser(_args.toVector) + val logger = new Logger(args.enabledLoggers, start) + logger.stage1(s"Stage1 start") + + val classLoaderCache = ClassLoaderCache( + logger, + classLoaderCacheKeys, + classLoaderCacheValues + ) + + + val (cbtHasChanged, classLoader) = buildStage2( compatibilityTarget, classLoaderCache, _cbtChanged, cbtHome, cache ) + + val stage2Args = Stage2Args( + new File( args.args(0) ), + args.args.drop(1).toVector, + // launcher changes cause entire nailgun restart, so no need for them here + cbtHasChanged = cbtHasChanged, + classLoaderCache = classLoaderCache, + cache, + cbtHome, + compatibilityTarget + ) + + logger.stage1(s"Run Stage2") + val exitCode = ( + classLoader + .loadClass( + if(args.tools) "cbt.ToolsStage2" else "cbt.Stage2" + ) + .getMethod( "run", classOf[Stage2Args] ) + .invoke( + null, + stage2Args + ) match { + case code: ExitCode => code + case _ => ExitCode.Success + } + ).integer + logger.stage1(s"Stage1 end") + return exitCode; + } +} diff --git a/stage1/Stage1Lib.scala b/stage1/Stage1Lib.scala new file mode 100644 index 0000000..273b9af --- /dev/null +++ b/stage1/Stage1Lib.scala @@ -0,0 +1,447 @@ +package cbt + +import java.io._ +import java.lang.reflect.InvocationTargetException +import java.net._ +import java.nio.charset.StandardCharsets +import java.nio.file._ +import java.nio.file.attribute.FileTime +import javax.tools._ +import java.security._ +import java.util.{Set=>_,Map=>_,List=>_,_} +import java.util.concurrent.ConcurrentHashMap +import javax.xml.bind.annotation.adapters.HexBinaryAdapter + +// CLI interop +case class ExitCode(integer: Int) +object ExitCode{ + val Success = ExitCode(0) + val Failure = ExitCode(1) +} + +object CatchTrappedExitCode{ + def unapply(e: Throwable): Option[ExitCode] = { + Option(e) flatMap { + case i: InvocationTargetException => unapply(i.getTargetException) + case e if TrapSecurityManager.isTrappedExit(e) => Some( ExitCode(TrapSecurityManager.exitCode(e)) ) + case _ => None + } + } +} + +class BaseLib{ + def realpath(name: File) = new File(java.nio.file.Paths.get(name.getAbsolutePath).normalize.toString) +} + +class Stage1Lib( val logger: Logger ) extends BaseLib{ + lib => + implicit val implicitLogger: Logger = logger + + def libMajorVersion(libFullVersion: String) = libFullVersion.split("\\.").take(2).mkString(".") + + // ========== file system / net ========== + + def array2hex(padTo: Int, array: Array[Byte]): String = { + val hex = new java.math.BigInteger(1, array).toString(16) + ("0" * (padTo-hex.size)) ++ hex + } + def md5( bytes: Array[Byte] ): String = array2hex(32, MessageDigest.getInstance("MD5").digest(bytes)).toLowerCase + def sha1( bytes: Array[Byte] ): String = array2hex(40, MessageDigest.getInstance("SHA-1").digest(bytes)).toLowerCase + + def red(string: String) = scala.Console.RED++string++scala.Console.RESET + def blue(string: String) = scala.Console.BLUE++string++scala.Console.RESET + def green(string: String) = scala.Console.GREEN++string++scala.Console.RESET + + def write(file: File, content: String, options: OpenOption*): File = Stage0Lib.write(file, content, options:_*) + + def download(url: URL, target: File, sha1: Option[String]): Boolean = { + if( target.exists ){ + logger.resolver(green("found ") ++ url.string) + true + } else { + val incomplete = ( target ++ ".incomplete" ).toPath; + val connection = Stage0Lib.openConnectionConsideringProxy(url) + if(connection.getResponseCode != HttpURLConnection.HTTP_OK){ + logger.resolver(blue("not found: ") ++ url.string) + false + } else { + System.err.println(blue("downloading ") ++ url.string) + logger.resolver(blue("to ") ++ target.string) + target.getParentFile.mkdirs + val stream = connection.getInputStream + try{ + Files.copy(stream, incomplete, StandardCopyOption.REPLACE_EXISTING) + } finally { + stream.close() + } + sha1.foreach{ + hash => + val expected = hash.toLowerCase + val actual = this.sha1(Files.readAllBytes(incomplete)) + assert( expected == actual, s"$expected == $actual" ) + logger.resolver( green("verified") ++ " checksum for " ++ target.string) + } + Files.move(incomplete, target.toPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + true + } + } + } + + def listFilesRecursive(f: File): Seq[File] = { + f +: ( + if( f.isDirectory ) f.listFiles.flatMap(listFilesRecursive).toVector else Seq[File]() + ) + } + + // ========== compilation / execution ========== + + def runMain( cls: String, args: Seq[String], classLoader: ClassLoader, fakeInstance: Boolean = false ): ExitCode = { + import java.lang.reflect.Modifier + logger.lib(s"Running $cls.main($args) with classLoader: " ++ classLoader.toString) + trapExitCode{ + val c = classLoader.loadClass(cls) + val m = c.getMethod( "main", classOf[Array[String]] ) + val instance = + if(!fakeInstance) null else c.newInstance + assert( + fakeInstance || (m.getModifiers & java.lang.reflect.Modifier.STATIC) > 0, + "Cannot run non-static method " ++ cls+".main" + ) + m.invoke( instance, args.toArray.asInstanceOf[AnyRef] ) + ExitCode.Success + } + } + + /** shows an interactive dialogue in the shell asking the user to pick one of many choices */ + def pickOne[T]( msg: String, choices: Seq[T] )( show: T => String ): Option[T] = { + if(choices.size == 0) None else if(choices.size == 1) Some(choices.head) else { + Option(System.console).map{ + console => + val indexedChoices: Map[Int, T] = choices.zipWithIndex.toMap.mapValues(_+1).map(_.swap) + System.err.println( + indexedChoices.map{ case (index,choice) => s"[${index}] "++show(choice)}.mkString("\n") + ) + val range = s"1 - ${indexedChoices.size}" + System.err.println() + System.err.println( msg ++ " [" ++ range ++ "] " ) + val answer = console.readLine() + val choice = try{ + Some(Integer.parseInt(answer)) + }catch{ + case e:java.lang.NumberFormatException => None + } + + choice.flatMap(indexedChoices.get).orElse{ + System.err.println("Not in range "++range) + None + } + }.getOrElse{ + System.err.println("System.console() == null. Use `cbt direct <task>` or see https://github.com/cvogt/cbt/issues/236") + None + } + } + } + + /** interactively pick one main class */ + def runClass( mainClasses: Seq[Class[_]] ): Option[Class[_]] = { + pickOne( "Which one do you want to run?", mainClasses )( _.toString ) + } + + def mainClasses( targetDirectory: File, classLoader : ClassLoader ): Seq[Class[_]] = { + val arrayClass = classOf[Array[String]] + val unitClass = classOf[Unit] + + listFilesRecursive(targetDirectory) + .filter(_.isFile) + .map(_.getPath) + .collect{ + // no $ to avoid inner classes + case path if !path.contains("$") && path.endsWith(".class") => + classLoader.loadClass( + path + .stripSuffix(".class") + .stripPrefix(targetDirectory.getPath) + .stripPrefix(File.separator) // 1 for the slash + .replace(File.separator, ".") + ) + }.filter( + _.getDeclaredMethods().exists( m => + m.getName == "main" + && m.getParameterTypes.toList == List(arrayClass) + && m.getReturnType == unitClass + ) + ) + } + + implicit class ClassLoaderExtensions(classLoader: ClassLoader){ + def canLoad(className: String) = { + try{ + classLoader.loadClass(className) + true + } catch { + case e: ClassNotFoundException => false + } + } + } + + def needsUpdate( sourceFiles: Seq[File], statusFile: File ) = { + val lastCompile = statusFile.lastModified + sourceFiles.filter(_.lastModified > lastCompile).nonEmpty + } + + def compile( + cbtHasChanged: Boolean, + needsRecompile: Boolean, + files: Seq[File], + compileTarget: File, + statusFile: File, + classpath: ClassPath, + mavenCache: File, + scalacOptions: Seq[String] = Seq(), + classLoaderCache: ClassLoaderCache, + zincVersion: String, + scalaVersion: String + ): Option[File] = { + + val cp = classpath.string + if(classpath.files.isEmpty) + throw new Exception("Trying to compile with empty classpath. Source files: " ++ files.toString) + + if( files.isEmpty ){ + None + }else{ + if( needsRecompile ){ + def Resolver(urls: URL*) = MavenResolver(cbtHasChanged, mavenCache, urls: _*) + val zinc = Resolver(mavenCentral).bindOne(MavenDependency("com.typesafe.zinc","zinc", zincVersion)) + val zincDeps = zinc.transitiveDependencies + + val sbtInterface = + zincDeps + .collect{ case d @ + BoundMavenDependency( + _, _, MavenDependency( "com.typesafe.sbt", "sbt-interface", _, Classifier.none), _ + ) => d + } + .headOption + .getOrElse( throw new Exception(s"cannot find sbt-interface in zinc $zincVersion dependencies: "++zincDeps.toString) ) + .jar + + val compilerInterface = + zincDeps + .collect{ case d @ + BoundMavenDependency( + _, _, MavenDependency( "com.typesafe.sbt", "compiler-interface", _, Classifier.sources), _ + ) => d + } + .headOption + .getOrElse( throw new Exception(s"cannot find compiler-interface in zinc $zincVersion dependencies: "++zincDeps.toString) ) + .jar + + val scalaLibrary = Resolver(mavenCentral).bindOne(MavenDependency("org.scala-lang","scala-library",scalaVersion)).jar + val scalaReflect = Resolver(mavenCentral).bindOne(MavenDependency("org.scala-lang","scala-reflect",scalaVersion)).jar + val scalaCompiler = Resolver(mavenCentral).bindOne(MavenDependency("org.scala-lang","scala-compiler",scalaVersion)).jar + + val start = System.currentTimeMillis + + val _class = "com.typesafe.zinc.Main" + val dualArgs = + Seq( + "-scala-compiler", scalaCompiler.toString, + "-scala-library", scalaLibrary.toString, + "-sbt-interface", sbtInterface.toString, + "-compiler-interface", compilerInterface.toString, + "-scala-extra", scalaReflect.toString, + "-d", compileTarget.toString + ) + val singleArgs = scalacOptions.map( "-S" ++ _ ) + + val code = + redirectOutToErr{ + System.err.println("Compiling to " ++ compileTarget.toString) + try{ + lib.runMain( + _class, + dualArgs ++ singleArgs ++ Seq( + "-cp", cp // let's put cp last. It so long + ) ++ files.map(_.toString), + zinc.classLoader(classLoaderCache) + ) + } catch { + case e: Exception => + System.err.println(red("The Scala compiler crashed. Try running it by hand:")) + System.out.println(s""" + java -cp \\ + ${zinc.classpath.strings.mkString(":\\\n")} \\ + \\ + ${_class} \\ + \\ + ${dualArgs.grouped(2).map(_.mkString(" ")).mkString(" \\\n")} \\ + \\ + ${singleArgs.mkString(" \\\n")} \\ + \\ + -cp \\ + ${classpath.strings.mkString(":\\\n")} \\ + \\ + ${files.sorted.mkString(" \\\n")} + """ + ) + ExitCode.Failure + } + } + + if(code == ExitCode.Success){ + // write version and when last compilation started so we can trigger + // recompile if cbt version changed or newer source files are seen + write(statusFile, "")//cbtVersion.getBytes) + Files.setLastModifiedTime(statusFile.toPath, FileTime.fromMillis(start) ) + } else { + System.exit(code.integer) // FIXME: let's find a better solution for error handling. Maybe a monad after all. + } + } + Some( compileTarget ) + } + } + def redirectOutToErr[T](code: => T): T = { + val ( out, err ) = try{ + // trying nailgun's System.our/err wrapper + val field = System.out.getClass.getDeclaredField("streams") + assert(System.out.getClass.getName == "com.martiansoftware.nailgun.ThreadLocalPrintStream") + assert(System.err.getClass.getName == "com.martiansoftware.nailgun.ThreadLocalPrintStream") + field.setAccessible(true) + val out = field.get(System.out).asInstanceOf[ThreadLocal[PrintStream]] + val err = field.get(System.err).asInstanceOf[ThreadLocal[PrintStream]] + ( out, err ) + } catch { + case e: NoSuchFieldException => + // trying cbt's System.our/err wrapper + val field = classOf[FilterOutputStream].getDeclaredField("out") + field.setAccessible(true) + val outStream = field.get(System.out) + val errStream = field.get(System.err) + assert(outStream.getClass.getName == "cbt.ThreadLocalOutputStream") + assert(errStream.getClass.getName == "cbt.ThreadLocalOutputStream") + val field2 = outStream.getClass.getDeclaredField("threadLocal") + field2.setAccessible(true) + val out = field2.get(outStream).asInstanceOf[ThreadLocal[PrintStream]] + val err = field2.get(errStream).asInstanceOf[ThreadLocal[PrintStream]] + ( out, err ) + } + + val oldOut: PrintStream = out.get + out.set( err.get: PrintStream ) + val res = code + out.set( oldOut ) + res + } + + def trapExitCode( code: => ExitCode ): ExitCode = { + val trapExitCodeBefore = TrapSecurityManager.trapExitCode().get + try{ + TrapSecurityManager.trapExitCode().set(true) + code + } catch { + case CatchTrappedExitCode(exitCode) => + logger.stage1(s"caught exit code $exitCode") + exitCode + } finally { + TrapSecurityManager.trapExitCode().set(trapExitCodeBefore) + } + } + + def ScalaDependency( + groupId: String, artifactId: String, version: String, classifier: Classifier = Classifier.none, + scalaMajorVersion: String + ) = + MavenDependency( + groupId, artifactId ++ "_" ++ scalaMajorVersion, version, classifier + ) + + def cacheOnDisk[T] + ( cbtHasChanged: Boolean, cacheFile: File ) + ( deserialize: String => T ) + ( serialize: T => String ) + ( compute: => Seq[T] ) = { + if(!cbtHasChanged && cacheFile.exists){ + import collection.JavaConversions._ + Files + .readAllLines( cacheFile.toPath, StandardCharsets.UTF_8 ) + .toStream + .map(deserialize) + } else { + val result = compute + val string = result.map(serialize).mkString("\n") + write(cacheFile, string) + result + } + } + + def dependencyTreeRecursion(root: Dependency, indent: Int = 0): String = ( + ( " " * indent ) + ++ (if(root.needsUpdate) red(root.show) else root.show) + ++ root.dependencies.map( d => + "\n" ++ dependencyTreeRecursion(d,indent + 1) + ).mkString + ) + + def transitiveDependencies(dependency: Dependency): Seq[Dependency] = { + def linearize(deps: Seq[Dependency]): Seq[Dependency] = { + // Order is important here in order to generate the correct lineraized dependency order for EarlyDependencies + // (and maybe this as well in case we want to get rid of MultiClassLoader) + try{ + if(deps.isEmpty) deps else ( deps ++ linearize(deps.flatMap(_.dependencies)) ) + } catch{ + case e: Exception => throw new Exception(dependency.show, e) + } + } + + // FIXME: this is probably wrong too eager. + // We should consider replacing versions during traversals already + // not just replace after traversals, because that could mean we + // pulled down dependencies current versions don't even rely + // on anymore. + + val deps: Seq[Dependency] = linearize(dependency.dependencies).reverse.distinct.reverse + val hasInfo: Seq[Dependency with ArtifactInfo] = deps.collect{ case d:Dependency with ArtifactInfo => d } + val noInfo: Seq[Dependency] = deps.filter{ + case _:Dependency with ArtifactInfo => false + case _ => true + } + noInfo ++ BoundMavenDependency.updateOutdated( hasInfo ).reverse.distinct + } + + + def actual(current: Dependency, latest: Map[(String,String),Dependency]) = current match { + case d: ArtifactInfo => latest((d.groupId,d.artifactId)) + case d => d + } + + def classLoaderRecursion( dependency: Dependency, latest: Map[(String,String),Dependency], cache: ClassLoaderCache ): ClassLoader = { + val d = dependency + val dependencies = dependency.dependencies + def dependencyClassLoader( latest: Map[(String,String),Dependency], cache: ClassLoaderCache ): ClassLoader = { + if( dependency.dependencies.isEmpty ){ + // wrap for caching + new cbt.URLClassLoader( ClassPath(), ClassLoader.getSystemClassLoader().getParent() ) + } else if( dependencies.size == 1 ){ + classLoaderRecursion( dependencies.head, latest, cache ) + } else{ + val cp = d.dependencyClasspath.string + if( dependencies.exists(_.needsUpdate) && cache.persistent.containsKey(cp) ){ + cache.persistent.remove(cp) + } + def cl = new MultiClassLoader( dependencies.map( classLoaderRecursion(_, latest, cache) ) ) + if(d.isInstanceOf[BuildInterface]) + cl // Don't cache builds right now. We need to fix invalidation first. + else + cache.persistent.get( cp, cl ) + } + } + + val a = actual( dependency, latest ) + def cl = new cbt.URLClassLoader( a.exportedClasspath, dependencyClassLoader(latest, cache) ) + if(d.isInstanceOf[BuildInterface]) + cl + else + cache.persistent.get( a.classpath.string, cl ) + } +} diff --git a/stage1/URLClassLoader.scala b/stage1/URLClassLoader.scala new file mode 100644 index 0000000..ff8d2a1 --- /dev/null +++ b/stage1/URLClassLoader.scala @@ -0,0 +1,47 @@ +package cbt + +import java.net._ +import scala.util.Try + +case class URLClassLoader( classPath: ClassPath, parent: ClassLoader )( implicit val logger: Logger ) + extends java.net.URLClassLoader( + classPath.strings.map( p => new URL("file:" ++ p) ).toArray, + parent + ) with CachingClassLoader{ + val id = Math.abs( new java.util.Random().nextInt ) + override def toString = ( + scala.Console.BLUE + ++ getClass.getSimpleName ++ ":" ++ id.toString + ++ scala.Console.RESET + ++ "(\n" + ++ ( + getURLs.map(_.toString).sorted.mkString(",\n") + ++ ( + if(getParent() != ClassLoader.getSystemClassLoader().getParent()) + ",\n" ++ Option(getParent()).map(_.toString).getOrElse("null") + else "" + ) + ).split("\n").map(" "++_).mkString("\n") + ++ "\n)" + ) +} + +/* +trait ClassLoaderLogging extends ClassLoader{ + def logger: Logger + val prefix = s"[${getClass.getSimpleName}] " + val postfix = " in \name" ++ this.toString + override def loadClass(name: String, resolve: Boolean): Class[_] = { + //logger.resolver(prefix ++ s"loadClass($name, $resolve)" ++ postfix ) + super.loadClass(name, resolve) + } + override def loadClass(name: String): Class[_] = { + //logger.resolver(prefix ++ s"loadClass($name)" ++ postfix ) + super.loadClass(name) + } + override def findClass(name: String): Class[_] = { + //logger.resolver(prefix ++ s"findClass($name)" ++ postfix ) + super.findClass(name) + } +} +*/ diff --git a/stage1/cbt.scala b/stage1/cbt.scala new file mode 100644 index 0000000..7a239a1 --- /dev/null +++ b/stage1/cbt.scala @@ -0,0 +1,99 @@ +package cbt +import java.io._ +import java.nio.file._ +import java.net._ +import java.util.concurrent.ConcurrentHashMap + +object `package`{ + val mavenCentral = new URL("https://repo1.maven.org/maven2") + val jcenter = new URL("https://jcenter.bintray.com") + def bintray(owner: String) = new URL(s"https://dl.bintray.com/$owner/maven") // FIXME: url encode owner + private val sonatypeBase = new URL("https://oss.sonatype.org/content/repositories/") + val sonatypeReleases = sonatypeBase ++ "releases" + val sonatypeSnapshots = sonatypeBase ++ "snapshots" + + private val lib = new BaseLib + implicit class FileExtensionMethods( file: File ){ + def ++( s: String ): File = { + if(s endsWith "/") throw new Exception( + """Trying to append a String that ends in "/" to a File would loose the trailing "/". Use .stripSuffix("/") if you need to.""" + ) + new File( file.toString ++ s ) + } + def /(s: String): File = new File(file.getAbsolutePath + File.separator + s) + def parent = lib.realpath(file ++ "/..") + def string = file.toString + } + implicit class URLExtensionMethods( url: URL ){ + def ++( s: String ): URL = new URL( url.toString ++ s ) + def string = url.toString + } + implicit class BuildInterfaceExtensions(build: BuildInterface){ + import build._ + def triggerLoopFiles: Seq[File] = triggerLoopFilesArray.to + def crossScalaVersions: Seq[String] = crossScalaVersionsArray.to + } + implicit class ArtifactInfoExtensions(subject: ArtifactInfo){ + import subject._ + def str = s"$groupId:$artifactId:$version" + def show = this.getClass.getSimpleName ++ s"($str)" + } + implicit class DependencyExtensions(subject: Dependency){ + import subject._ + def dependencyClasspath: ClassPath = ClassPath(dependencyClasspathArray.to) + def exportedClasspath: ClassPath = ClassPath(exportedClasspathArray.to) + def classpath = exportedClasspath ++ dependencyClasspath + def dependencies: Seq[Dependency] = dependenciesArray.to + def needsUpdate: Boolean = needsUpdateCompat + } + implicit class ContextExtensions(subject: Context){ + import subject._ + val paths = CbtPaths(cbtHome, cache) + implicit def logger: Logger = new Logger(enabledLoggers, start) + def classLoaderCache: ClassLoaderCache = new ClassLoaderCache( + logger, + permanentKeys, + permanentClassLoaders + ) + def cbtDependency = { + import paths._ + CbtDependency(cbtHasChanged, mavenCache, nailgunTarget, stage1Target, stage2Target, compatibilityTarget) + } + def args: Seq[String] = argsArray.to + def enabledLoggers: Set[String] = enabledLoggersArray.to + def scalaVersion = Option(scalaVersionOrNull) + def version = Option(versionOrNull) + def parentBuild = Option(parentBuildOrNull) + def start: scala.Long = startCompat + def cbtHasChanged: scala.Boolean = cbtHasChangedCompat + + def copy( + projectDirectory: File = projectDirectory, + args: Seq[String] = args, + enabledLoggers: Set[String] = enabledLoggers, + cbtHasChanged: Boolean = cbtHasChanged, + version: Option[String] = version, + scalaVersion: Option[String] = scalaVersion, + cache: File = cache, + cbtHome: File = cbtHome, + parentBuild: Option[BuildInterface] = None + ): Context = ContextImplementation( + projectDirectory, + cwd, + args.to, + enabledLoggers.to, + startCompat, + cbtHasChangedCompat, + version.getOrElse(null), + scalaVersion.getOrElse(null), + permanentKeys, + permanentClassLoaders, + cache, + cbtHome, + cbtRootHome, + compatibilityTarget, + parentBuild.getOrElse(null) + ) + } +} + diff --git a/stage1/constants.scala b/stage1/constants.scala new file mode 100644 index 0000000..437cf19 --- /dev/null +++ b/stage1/constants.scala @@ -0,0 +1,7 @@ +package cbt +object constants{ + val scalaXmlVersion = "1.0.5" + val scalaVersion = "2.11.8" + val zincVersion = "0.3.9" + val scalaMajorVersion = scalaVersion.split("\\.").take(2).mkString(".") +} diff --git a/stage1/logger.scala b/stage1/logger.scala new file mode 100644 index 0000000..57f0cfa --- /dev/null +++ b/stage1/logger.scala @@ -0,0 +1,61 @@ +package cbt + +/** + * This represents a logger with namespaces that can be enabled or disabled as needed. The + * namespaces are defined using {{enabledLoggers}}. Possible values are defined in the subobject + * "names". + * + * We can replace this with something more sophisticated eventually. + */ +case class Logger(enabledLoggers: Set[String], start: Long) { + def this(enabledLoggers: Option[String], start: Long) = { + this( + enabledLoggers.toVector.flatMap( _.split(",") ).toSet, + start + ) + } + + val disabledLoggers: Set[String] = enabledLoggers.filter(_.startsWith("-")).map(_.drop(1)) + + def log(name: String, msg: => String) = { + if( + ( + (enabledLoggers contains name) + || (enabledLoggers contains "all") + ) && !(disabledLoggers contains name) + ){ + logUnguarded(name, msg) + } + } + + def showInvocation(method: String, args: Any) = method ++ "( " ++ args.toString ++ " )" + + final def stage1(msg: => String) = log(names.stage1, msg) + final def stage2(msg: => String) = log(names.stage2, msg) + final def loop(msg: => String) = log(names.loop, msg) + final def task(msg: => String) = log(names.task, msg) + final def composition(msg: => String) = log(names.composition, msg) + final def resolver(msg: => String) = log(names.resolver, msg) + final def lib(msg: => String) = log(names.lib, msg) + final def test(msg: => String) = log(names.test, msg) + final def git(msg: => String) = log(names.git, msg) + final def pom(msg: => String) = log(names.pom, msg) + + private object names{ + val stage1 = "stage1" + val stage2 = "stage2" + val loop = "loop" + val task = "task" + val resolver = "resolver" + val composition = "composition" + val lib = "lib" + val test = "test" + val pom = "pom" + val git = "git" + } + + private def logUnguarded(name: String, msg: => String) = { + val timeTaken = ((System.currentTimeMillis.toDouble - start) / 1000).toString + System.err.println( s"[$timeTaken][$name] $msg" ) + } +} diff --git a/stage1/resolver.scala b/stage1/resolver.scala new file mode 100644 index 0000000..13e8e52 --- /dev/null +++ b/stage1/resolver.scala @@ -0,0 +1,401 @@ +package cbt +import java.nio.file._ +import java.nio.charset.StandardCharsets +import java.net._ +import java.io._ +import scala.xml._ +import scala.concurrent._ +import scala.concurrent.duration._ + +trait DependencyImplementation extends Dependency{ + implicit protected def logger: Logger + protected def lib = new Stage1Lib(logger) + + def needsUpdate: Boolean + //def cacheClassLoader: Boolean = false + private[cbt] def targetClasspath: ClassPath + def dependencyClasspathArray: Array[File] = dependencyClasspath.files.toArray + def exportedClasspathArray: Array[File] = exportedClasspath.files.toArray + def exportedClasspath: ClassPath + def dependenciesArray: Array[Dependency] = dependencies.to + + def needsUpdateCompat: java.lang.Boolean = needsUpdate + + /* + //private type BuildCache = KeyLockedLazyCache[Dependency, Future[ClassPath]] + def exportClasspathConcurrently: ClassPath = { + // FIXME: this should separate a blocking and a non-blocking EC + import scala.concurrent.ExecutionContext.Implicits.global + Await.result( + exportClasspathConcurrently( + transitiveDependencies + .collect{ case d: ArtifactInfo => d } + .groupBy( d => (d.groupId,d.artifactId) ) + .mapValues( _.head ) + //, new BuildCache + ), // FIXME + Duration.Inf + ) + } + + def concurrencyEnabled = false + + /** + The implementation of this method is untested and likely buggy + at this stage. + */ + def exportClasspathConcurrently( + latest: Map[(String, String),Dependency with ArtifactInfo]//, cache: BuildCache + )( implicit ec: ExecutionContext ): Future[AnyRef] = { + Future.sequence( // trigger compilation / download of all dependencies first + this.dependencies.map{ + d => + // find out latest version of the required dependency + val l = d match { + case m: BoundMavenDependency => latest( (m.groupId,m.artifactId) ) + case _ => d + } + // // trigger compilation if not already triggered + // cache.get( l, l.exportClasspathConcurrently( latest, cache ) ) + l.exportClasspathConcurrently( latest ) // FIXME + } + ).map( + // merge dependency classpaths into one + ClassPath.flatten(_) + ).map( + _ => + // now that all dependencies are done, compile the code of this + exportedClasspath + ) + } + */ + + def classLoader( cache: ClassLoaderCache ): ClassLoader = { + /* + if( concurrencyEnabled ){ + // trigger concurrent building / downloading dependencies + exportClasspathConcurrently + } + */ + lib.classLoaderRecursion( + this, + (this +: transitiveDependencies).collect{ + case d: ArtifactInfo => d + }.groupBy( + d => (d.groupId,d.artifactId) + ).mapValues(_.head), + cache // FIXME + ) + } + // FIXME: these probably need to update outdated as well + def classpath : ClassPath = exportedClasspath ++ dependencyClasspath + def dependencyClasspath : ClassPath = ClassPath( + transitiveDependencies + .flatMap(_.exportedClasspath.files) + .distinct // <- currently needed here to handle diamond dependencies on builds (duplicate in classpath) + ) + def dependencies: Seq[Dependency] + + private object transitiveDependenciesCache extends Cache[Seq[Dependency]] + /** return dependencies in order of linearized dependence. this is a bit tricky. */ + def transitiveDependencies: Seq[Dependency] = transitiveDependenciesCache{ + lib.transitiveDependencies(this) + } + + override def show: String = this.getClass.getSimpleName + // ========== debug ========== + def dependencyTree: String = lib.dependencyTreeRecursion(this) +} + +// TODO: all this hard codes the scala version, needs more flexibility +class ScalaCompilerDependency(cbtHasChanged: Boolean, mavenCache: File, version: String)(implicit logger: Logger) extends BoundMavenDependency(cbtHasChanged, mavenCache, MavenDependency("org.scala-lang","scala-compiler",version, Classifier.none), Seq(mavenCentral)) +class ScalaLibraryDependency (cbtHasChanged: Boolean, mavenCache: File, version: String)(implicit logger: Logger) extends BoundMavenDependency(cbtHasChanged, mavenCache, MavenDependency("org.scala-lang","scala-library",version, Classifier.none), Seq(mavenCentral)) +class ScalaReflectDependency (cbtHasChanged: Boolean, mavenCache: File, version: String)(implicit logger: Logger) extends BoundMavenDependency(cbtHasChanged, mavenCache, MavenDependency("org.scala-lang","scala-reflect",version, Classifier.none), Seq(mavenCentral)) + +case class ScalaDependencies(cbtHasChanged: Boolean, mavenCache: File, version: String)(implicit val logger: Logger) extends DependencyImplementation{ sd => + override final val needsUpdate = false + def targetClasspath = ClassPath() + def exportedClasspath = ClassPath() + def dependencies = Seq( + new ScalaCompilerDependency(cbtHasChanged, mavenCache, version), + new ScalaLibraryDependency(cbtHasChanged, mavenCache, version), + new ScalaReflectDependency(cbtHasChanged, mavenCache, version) + ) +} + +case class BinaryDependency( path: File, dependencies: Seq[Dependency] )(implicit val logger: Logger) extends DependencyImplementation{ + def exportedClasspath = ClassPath(Seq(path)) + override def needsUpdate = false + def targetClasspath = exportedClasspath +} + +/** Allows to easily assemble a bunch of dependencies */ +case class Dependencies( dependencies: Seq[Dependency] )(implicit val logger: Logger) extends DependencyImplementation{ + override def needsUpdate = dependencies.exists(_.needsUpdate) + override def exportedClasspath = ClassPath() + override def targetClasspath = ClassPath() +} + +case class Stage1Dependency(cbtHasChanged: Boolean, mavenCache: File, nailgunTarget: File, stage1Target: File, compatibilityTarget: File)(implicit val logger: Logger) extends DependencyImplementation{ + override def needsUpdate = cbtHasChanged + override def targetClasspath = exportedClasspath + override def exportedClasspath = ClassPath( Seq(nailgunTarget, stage1Target) ) + val compatibilityDependency = CompatibilityDependency(cbtHasChanged, compatibilityTarget) + override def dependencies = Seq( + compatibilityDependency + ) ++ + MavenResolver(cbtHasChanged,mavenCache,mavenCentral).bind( + MavenDependency("org.scala-lang","scala-library",constants.scalaVersion), + MavenDependency("org.scala-lang.modules","scala-xml_"+constants.scalaMajorVersion,constants.scalaXmlVersion) + ) +} +case class CompatibilityDependency(cbtHasChanged: Boolean, compatibilityTarget: File)(implicit val logger: Logger) extends DependencyImplementation{ + override def needsUpdate = cbtHasChanged + override def targetClasspath = exportedClasspath + override def exportedClasspath = ClassPath( Seq(compatibilityTarget) ) + override def dependencies = Seq() +} +case class CbtDependency(cbtHasChanged: Boolean, mavenCache: File, nailgunTarget: File, stage1Target: File, stage2Target: File, compatibilityTarget: File)(implicit val logger: Logger) extends DependencyImplementation{ + override def needsUpdate = cbtHasChanged + override def targetClasspath = exportedClasspath + override def exportedClasspath = ClassPath( Seq( stage2Target ) ) + val stage1Dependency = Stage1Dependency(cbtHasChanged, mavenCache, nailgunTarget, stage1Target, compatibilityTarget) + override def dependencies = Seq( + stage1Dependency + ) ++ + MavenResolver(cbtHasChanged, mavenCache,mavenCentral).bind( + MavenDependency("net.incongru.watchservice","barbary-watchservice","1.0"), + MavenDependency("org.eclipse.jgit", "org.eclipse.jgit", "4.2.0.201601211800-r") + ) +} + +case class Classifier(name: Option[String]) +object Classifier{ + object none extends Classifier(None) + object javadoc extends Classifier(Some("javadoc")) + object sources extends Classifier(Some("sources")) +} +abstract class DependenciesProxy{ + +} +class BoundMavenDependencies( + cbtHasChanged: Boolean, mavenCache: File, urls: Seq[URL], mavenDependencies: Seq[MavenDependency] +)(implicit logger: Logger) extends Dependencies( + mavenDependencies.map( BoundMavenDependency(cbtHasChanged,mavenCache,_,urls) ) +) +case class MavenDependency( + groupId: String, artifactId: String, version: String, classifier: Classifier = Classifier.none +){ + private[cbt] def serialize = groupId ++ ":" ++ artifactId ++ ":"++ version ++ classifier.name.map(":" ++ _).getOrElse("") +} +object MavenDependency{ + private[cbt] def deserialize = (_:String).split(":") match { + case col => MavenDependency( col(0), col(1), col(2), Classifier(col.lift(3)) ) + } +} +// FIXME: take MavenResolver instead of mavenCache and repositories separately +case class BoundMavenDependency( + cbtHasChanged: Boolean, mavenCache: File, mavenDependency: MavenDependency, repositories: Seq[URL] +)(implicit val logger: Logger) extends ArtifactInfo with DependencyImplementation{ + val MavenDependency( groupId, artifactId, version, classifier ) = mavenDependency + assert( + Option(groupId).collect{ + case BoundMavenDependency.ValidIdentifier(_) => + }.nonEmpty, + s"not a valid groupId: '$groupId'" + ) + assert( + Option(artifactId).collect{ + case BoundMavenDependency.ValidIdentifier(_) => + }.nonEmpty, + s"not a valid artifactId: '$artifactId'" + ) + assert( + version != "" && version != null && !version.startsWith(" ") && !version.endsWith(" "), + s"not a valid version: '$version'" + ) + override def show: String = this.getClass.getSimpleName ++ "(" ++ mavenDependency.serialize ++ ")" + + override def needsUpdate = false + + private val groupPath = groupId.split("\\.").mkString("/") + protected[cbt] def basePath = s"/$groupPath/$artifactId/$version/$artifactId-$version" ++ classifier.name.map("-"++_).getOrElse("") + + //private def coursierJarFile = userHome++"/.coursier/cache/v1/https/repo1.maven.org/maven2"++basePath++".jar" + + def exportedJars = Seq( jar ) + override def exportedClasspath = ClassPath( exportedJars ) + override def targetClasspath = exportedClasspath + import scala.collection.JavaConversions._ + + private def resolve(suffix: String, hash: Option[String]): File = { + logger.resolver("Resolving "+this) + val file = mavenCache ++ basePath ++ "." ++ suffix + val urls = repositories.map(_ ++ basePath ++ "." ++ suffix) + urls.find( + lib.download(_, file, hash) + ).getOrElse( + throw new Exception(s"\nCannot resolve\n$this\nCan't find any of\n"++urls.mkString("\n")) + ) + file + } + + private def resolveHash(suffix: String) = { + Files.readAllLines( + resolve( suffix ++ ".sha1", None ).toPath, + StandardCharsets.UTF_8 + ).mkString("\n").split(" ").head.trim + } + + private object jarSha1Cache extends Cache[String] + def jarSha1: String = jarSha1Cache{ resolveHash("jar") } + + private object pomSha1Cache extends Cache[String] + def pomSha1: String = pomSha1Cache{ resolveHash("pom") } + + private object jarCache extends Cache[File] + def jar: File = jarCache{ resolve("jar", Some(jarSha1)) } + + private object pomCache extends Cache[File] + def pom: File = pomCache{ resolve("pom", Some(pomSha1)) } + + private def pomXml = XML.loadFile(pom.string) + // ========== pom traversal ========== + + private lazy val transitivePom: Seq[BoundMavenDependency] = { + (pomXml \ "parent").collect{ + case parent => + BoundMavenDependency( + cbtHasChanged: Boolean, + mavenCache, + MavenDependency( + (parent \ "groupId").text, + (parent \ "artifactId").text, + (parent \ "version").text + ), + repositories + )(logger) + }.flatMap(_.transitivePom) :+ this + } + + private lazy val properties: Map[String, String] = ( + transitivePom.flatMap{ d => + val props = (d.pomXml \ "properties").flatMap(_.child).map{ + tag => tag.label -> tag.text + } + logger.pom(s"Found properties in $pom: $props") + props + } + ).toMap + + private lazy val dependencyVersions: Map[String, (String,String)] = + transitivePom.flatMap( + p => + (p.pomXml \ "dependencyManagement" \ "dependencies" \ "dependency").map{ + xml => + val groupId = p.lookup(xml,_ \ "groupId").get + val artifactId = p.lookup(xml,_ \ "artifactId").get + val version = p.lookup(xml,_ \ "version").get + artifactId -> (groupId, version) + } + ).toMap + + def dependencies: Seq[BoundMavenDependency] = { + if(classifier == Classifier.sources) Seq() + else { + lib.cacheOnDisk( + cbtHasChanged, mavenCache ++ basePath ++ ".pom.dependencies" + )( MavenDependency.deserialize )( _.serialize ){ + (pomXml \ "dependencies" \ "dependency").collect{ + case xml if ( (xml \ "scope").text == "" || (xml \ "scope").text == "compile" ) && (xml \ "optional").text != "true" => + val artifactId = lookup(xml,_ \ "artifactId").get + val groupId = + lookup(xml,_ \ "groupId").getOrElse( + dependencyVersions + .get(artifactId).map(_._1) + .getOrElse( + throw new Exception(s"$artifactId not found in \n$dependencyVersions") + ) + ) + val version = + lookup(xml,_ \ "version").getOrElse( + dependencyVersions + .get(artifactId).map(_._2) + .getOrElse( + throw new Exception(s"$artifactId not found in \n$dependencyVersions") + ) + ) + val classifier = Classifier( Some( (xml \ "classifier").text ).filterNot(_ == "").filterNot(_ == null) ) + MavenDependency( groupId, artifactId, version, classifier ) + }.toVector + }.map( + BoundMavenDependency( cbtHasChanged, mavenCache, _, repositories ) + ).to + } + } + def lookup( xml: Node, accessor: Node => NodeSeq ): Option[String] = { + //println("lookup in "++pomUrl) + val Substitution = "\\$\\{([^\\}]+)\\}".r + accessor(xml).headOption.map{v => + //println("found: "++v.text) + Substitution.replaceAllIn( + v.text, + matcher => { + val path = matcher.group(1) + properties.get(path).orElse( + transitivePom.reverse.flatMap{ d => + Some(path.split("\\.").toList).collect{ + case "project" :: path => + path.foldLeft(d.pomXml:NodeSeq){ case (xml,tag) => xml \ tag }.text + }.filter(_ != "") + }.headOption + ) + .getOrElse( + throw new Exception(s"Can't find $path in \n$properties.\n\npomParents: $transitivePom\n\n pomXml:\n$pomXml" ) + ) + //println("lookup "++path ++ ": "++(pomXml\path).text) + } + ) + } + } +} +object BoundMavenDependency{ + def ValidIdentifier = "^([A-Za-z0-9_\\-.]+)$".r // according to maven's DefaultModelValidator.java + def semanticVersionLessThan(left: Array[Either[Int,String]], right: Array[Either[Int,String]]) = { + // FIXME: this ignores ends when different size + val zipped = left zip right + val res = zipped.map { + case (Left(i),Left(j)) => i compare j + case (Right(i),Right(j)) => i compare j + case (Left(i),Right(j)) => i.toString compare j + case (Right(i),Left(j)) => i compare j.toString + } + res.find(_ != 0).map(_ < 0).getOrElse(false) + } + def toInt(str: String): Either[Int,String] = try { + Left(str.toInt) + } catch { + case e: NumberFormatException => Right(str) + } + /* this obviously should be overridable somehow */ + def updateOutdated( + deps: Seq[Dependency with ArtifactInfo], + versionLessThan: (Array[Either[Int,String]], Array[Either[Int,String]]) => Boolean = semanticVersionLessThan + )(implicit logger: Logger): Seq[Dependency with ArtifactInfo] = { + val latest = deps + .groupBy( d => (d.groupId, d.artifactId) ) + .mapValues( + _.groupBy(_.version) // remove duplicates + .map( _._2.head ) + .toVector + .sortBy( _.version.split("\\.|\\-").map(toInt) )( Ordering.fromLessThan(versionLessThan) ) + .last + ) + deps.map{ + d => + val l = latest((d.groupId,d.artifactId)) + if(d != l) logger.resolver("outdated: "++d.show) + l + } + } +} diff --git a/stage2/BasicBuild.scala b/stage2/BasicBuild.scala new file mode 100644 index 0000000..128d2f8 --- /dev/null +++ b/stage2/BasicBuild.scala @@ -0,0 +1,247 @@ +package cbt + +import java.io._ +import java.net._ + +class BasicBuild(val context: Context) extends BaseBuild +trait BaseBuild extends BuildInterface with DependencyImplementation with TriggerLoop with SbtDependencyDsl{ + def context: Context + + // library available to builds + implicit protected final val logger: Logger = context.logger + implicit protected final val classLoaderCache: ClassLoaderCache = context.classLoaderCache + implicit protected final val _context = context + override protected final val lib: Lib = new Lib(logger) + + // ========== general stuff ========== + + def enableConcurrency = false + final def projectDirectory: File = lib.realpath(context.projectDirectory) + assert( projectDirectory.exists, "projectDirectory does not exist: " ++ projectDirectory.string ) + final def usage: String = lib.usage(this.getClass, show) + + final def taskNames: String = lib.taskNames(this.getClass).sorted.mkString("\n") + + // ========== meta data ========== + + def defaultScalaVersion: String = constants.scalaVersion + final def scalaVersion = context.scalaVersion getOrElse defaultScalaVersion + final def scalaMajorVersion: String = lib.libMajorVersion(scalaVersion) + def crossScalaVersions: Seq[String] = Seq(scalaVersion, "2.10.6") + final def crossScalaVersionsArray: Array[String] = crossScalaVersions.to + def projectName = "default" + + // TODO: this should probably provide a nice error message if class has constructor signature + def copy(context: Context): BuildInterface = lib.copy(this.getClass, context).asInstanceOf[BuildInterface] + def zincVersion = "0.3.9" + + def dependencies: Seq[Dependency] = + // FIXME: this should probably be removed + Resolver( mavenCentral ).bind( + "org.scala-lang" % "scala-library" % scalaVersion + ) + + // ========== paths ========== + final private val defaultSourceDirectory = projectDirectory ++ "/src" + + /** base directory where stuff should be generated */ + def target: File = projectDirectory ++ "/target" + /** base directory where stuff should be generated for this scala version*/ + def scalaTarget: File = target ++ s"/scala-$scalaMajorVersion" + /** directory where jars (and the pom file) should be put */ + def jarTarget: File = scalaTarget + /** directory where the scaladoc should be put */ + def docTarget: File = scalaTarget ++ "/api" + /** directory where the class files should be put (in package directories) */ + def compileTarget: File = scalaTarget ++ "/classes" + /** + File which cbt uses to determine if it needs to trigger an incremental re-compile. + Last modified date is the time when the last successful compilation started. + Contents is the cbt version git hash. + */ + def compileStatusFile: File = compileTarget ++ ".last-success" + + /** Source directories and files. Defaults to .scala and .java files in src/ and top-level. */ + def sources: Seq[File] = Seq(defaultSourceDirectory) ++ projectDirectory.listFiles.toVector.filter(lib.sourceFileFilter) + + /** Absolute path names for all individual files found in sources directly or contained in directories. */ + final def sourceFiles: Seq[File] = lib.sourceFiles(sources) + + protected def logEmptySourceDirectories(): Unit = { + val nonExisting = + sources + .filterNot( _.exists ) + .diff( Seq(defaultSourceDirectory) ) + if(!nonExisting.isEmpty) logger.stage2("Some sources do not exist: \n"++nonExisting.mkString("\n")) + } + logEmptySourceDirectories() + + def Resolver( urls: URL* ) = MavenResolver( context.cbtHasChanged, context.paths.mavenCache, urls: _* ) + + def ScalaDependency( + groupId: String, artifactId: String, version: String, classifier: Classifier = Classifier.none, + scalaVersion: String = scalaMajorVersion + ) = lib.ScalaDependency( groupId, artifactId, version, classifier, scalaVersion ) + + final def DirectoryDependency(path: File) = cbt.DirectoryDependency( + context.copy( projectDirectory = path, args = Seq() ) + ) + + def triggerLoopFiles: Seq[File] = sources ++ transitiveDependencies.collect{ case b: TriggerLoop => b.triggerLoopFiles }.flatten + + def localJars : Seq[File] = + Seq(projectDirectory ++ "/lib") + .filter(_.exists) + .flatMap(_.listFiles) + .filter(_.toString.endsWith(".jar")) + + override def dependencyClasspath : ClassPath = ClassPath(localJars) ++ super.dependencyClasspath + + protected def compileDependencies: Seq[Dependency] = Nil + final def compileClasspath : ClassPath = + dependencyClasspath ++ ClassPath( compileDependencies.flatMap(_.exportedClasspath.files).distinct ) + + def resourceClasspath: ClassPath = { + val resourcesDirectory = projectDirectory ++ "/resources" + ClassPath( if(resourcesDirectory.exists) Seq(resourcesDirectory) else Nil ) + } + def exportedClasspath : ClassPath = ClassPath(compile.toSeq) ++ resourceClasspath + def targetClasspath = ClassPath(Seq(compileTarget)) + // ========== compile, run, test ========== + + /** scalac options used for zinc and scaladoc */ + def scalacOptions: Seq[String] = Seq( + "-feature", + "-deprecation", + "-unchecked" + ) + + private object needsUpdateCache extends Cache[Boolean] + def needsUpdate: Boolean = needsUpdateCache( + context.cbtHasChanged + || lib.needsUpdate( sourceFiles, compileStatusFile ) + || transitiveDependencies.filterNot(_ == context.parentBuild).exists(_.needsUpdate) + ) + + private object compileCache extends Cache[Option[File]] + def compile: Option[File] = compileCache{ + lib.compile( + context.cbtHasChanged, + needsUpdate || context.parentBuild.map(_.needsUpdate).getOrElse(false), + sourceFiles, compileTarget, compileStatusFile, compileClasspath, + context.paths.mavenCache, scalacOptions, context.classLoaderCache, + zincVersion = zincVersion, scalaVersion = scalaVersion + ) + } + + + def mainClasses: Seq[Class[_]] = compile.toSeq.flatMap( lib.mainClasses( _, classLoader(classLoaderCache) ) ) + + def runClass: Option[String] = lib.runClass( mainClasses ).map( _.getName ) + + def run: ExitCode = runClass.map( lib.runMain( _, context.args, classLoader(context.classLoaderCache) ) ).getOrElse{ + logger.task( "No main class found for " ++ projectDirectory.string ) + ExitCode.Success + } + + def clean: ExitCode = { + lib.clean( + target, + context.args.contains("force"), + context.args.contains("dry-run"), + context.args.contains("list"), + context.args.contains("help") + ) + } + + def repl: ExitCode = { + lib.consoleOrFail("Use `cbt direct repl` instead") + + val colorized = "scala.color" + if(Option(System.getProperty(colorized)).isEmpty) { + // set colorized REPL, if user didn't pass own value + System.setProperty(colorized, "true") + } + + val scalac = new ScalaCompilerDependency(context.cbtHasChanged, context.paths.mavenCache, scalaVersion) + lib.runMain( + "scala.tools.nsc.MainGenericRunner", + Seq( + "-bootclasspath", + scalac.classpath.string, + "-classpath", + classpath.string + ) ++ context.args, + scalac.classLoader(classLoaderCache) + ) + } + + def test: Option[ExitCode] = + Some(new lib.ReflectBuild( + DirectoryDependency(projectDirectory++"/test").build + ).callNullary(Some("run"))) + def t = test + def rt = recursiveUnsafe(Some("test")) + + def recursiveSafe(_run: BuildInterface => Any): ExitCode = { + val builds = (this +: transitiveDependencies).collect{ + case b: BuildInterface => b + } + val results = builds.map(_run) + if( + results.forall{ + case Some(_:ExitCode) => true + case None => true + case _:ExitCode => true + case other => false + } + ){ + if( + results.collect{ + case Some(c:ExitCode) => c + case c:ExitCode => c + }.filter(_ != 0) + .nonEmpty + ) ExitCode.Failure + else ExitCode.Success + } else ExitCode.Success + } + + def recursive: ExitCode = { + recursiveUnsafe(context.args.lift(1)) + } + + def recursiveUnsafe(taskName: Option[String]): ExitCode = { + recursiveSafe{ + b => + System.err.println(b.show) + lib.trapExitCode{ // FIXME: trapExitCode does not seem to work here + try{ + new lib.ReflectBuild(b).callNullary(taskName) + ExitCode.Success + } catch { + case e: Throwable => println(e.getClass); throw e + } + } + ExitCode.Success + } + } + + def c = compile + def r = run + + /* + context.logger.composition(">"*80) + context.logger.composition("class " ++ this.getClass.toString) + context.logger.composition("dir " ++ projectDirectory.string) + context.logger.composition("sources " ++ sources.toList.mkString(" ")) + context.logger.composition("target " ++ target.string) + context.logger.composition("context " ++ context.toString) + context.logger.composition("dependencyTree\n" ++ dependencyTree) + context.logger.composition("<"*80) + */ + + // ========== cbt internals ========== + def finalBuild: BuildInterface = this + override def show = this.getClass.getSimpleName ++ "(" ++ projectDirectory.string ++ ")" +} diff --git a/stage2/BuildBuild.scala b/stage2/BuildBuild.scala new file mode 100644 index 0000000..b183745 --- /dev/null +++ b/stage2/BuildBuild.scala @@ -0,0 +1,81 @@ +package cbt +import java.nio.file._ + +trait BuildBuild extends BaseBuild{ + private final val managedContext = context.copy( + projectDirectory = managedBuildDirectory, + parentBuild=Some(this) + ) + + object plugins{ + final lazy val scalaTest = DirectoryDependency( managedContext.cbtHome ++ "/plugins/scalatest" ) + final lazy val sbtLayout = DirectoryDependency( managedContext.cbtHome ++ "/plugins/sbt_layout" ) + final lazy val scalaJs = DirectoryDependency( managedContext.cbtHome ++ "/plugins/scalajs" ) + final lazy val scalariform = DirectoryDependency( managedContext.cbtHome ++ "/plugins/scalariform" ) + final lazy val scalafmt = DirectoryDependency( managedContext.cbtHome ++ "/plugins/scalafmt" ) + final lazy val wartremover = DirectoryDependency( managedContext.cbtHome ++ "/plugins/wartremover" ) + final lazy val uberJar = DirectoryDependency( managedContext.cbtHome ++ "/plugins/uber-jar" ) + final lazy val sonatypeRelease = DirectoryDependency( managedContext.cbtHome ++ "/plugins/sonatype-release" ) + } + + override def dependencies = + super.dependencies :+ context.cbtDependency + def managedBuildDirectory: java.io.File = lib.realpath( projectDirectory.parent ) + private object managedBuildCache extends Cache[BuildInterface] + def managedBuild = managedBuildCache{ + val managedBuildFile = projectDirectory++"/build.scala" + logger.composition("Loading build at "++managedContext.projectDirectory.toString) + val build = ( + if(managedBuildFile.exists){ + val contents = new String(Files.readAllBytes(managedBuildFile.toPath)) + val cbtUrl = ("cbt:"++GitDependency.GitUrl.regex++"#[a-z0-9A-Z]+").r + cbtUrl + .findFirstIn(contents) + .flatMap{ + url => + val Array(base,hash) = url.drop(4).split("#") + if(context.cbtHome.string.contains(hash)) + None + else Some{ + // Note: cbt can't use an old version of itself for building, + // otherwise we'd have to recursively build all versions since + // the beginning. Instead CBT always needs to build the pure Java + // Launcher in the checkout with itself and then run it via reflection. + val dep = new GitDependency(base, hash, Some("nailgun_launcher")) + val ctx = managedContext.copy( cbtHome = dep.checkout ) + dep.classLoader(classLoaderCache) + .loadClass( "cbt.NailgunLauncher" ) + .getMethod( "getBuild", classOf[AnyRef] ) + .invoke( null, ctx ) + } + }.getOrElse{ + try{ + classLoader(context.classLoaderCache) + .loadClass(lib.buildClassName) + .getConstructors.head + .newInstance(managedContext) + } catch { + case e: ClassNotFoundException if e.getMessage == lib.buildClassName => + throw new Exception("You need to define a class Build in build.scala in: "+context.projectDirectory) + } + } + } else if( projectDirectory.listFiles.exists( _.getName.endsWith(".scala") ) ){ + throw new Exception( + "No file build.scala (lower case) found in " ++ projectDirectory.getPath + ) + } else if( projectDirectory.getParentFile.getName == "build" ){ + new BasicBuild( managedContext ) with BuildBuild + } else { + new BasicBuild( managedContext ) + } + ) + try{ + build.asInstanceOf[BuildInterface] + } catch { + case e: ClassCastException if e.getMessage.contains("Build cannot be cast to cbt.BuildInterface") => + throw new Exception("Your Build class needs to extend BaseBuild in: "+context.projectDirectory, e) + } + } + override def triggerLoopFiles = super.triggerLoopFiles ++ managedBuild.triggerLoopFiles + override def finalBuild: BuildInterface = if( context.projectDirectory == context.cwd ) this else managedBuild.finalBuild +} diff --git a/stage2/BuildDependency.scala b/stage2/BuildDependency.scala new file mode 100644 index 0000000..a834435 --- /dev/null +++ b/stage2/BuildDependency.scala @@ -0,0 +1,36 @@ +package cbt +import java.io.File +/* +sealed abstract class ProjectProxy extends Ha{ + protected def delegate: ProjectMetaData + def artifactId: String = delegate.artifactId + def groupId: String = delegate.groupId + def version: String = delegate.version + def exportedClasspath = delegate.exportedClasspath + def dependencies = Seq(delegate) +} +*/ +trait TriggerLoop extends DependencyImplementation{ + final def triggerLoopFilesArray = triggerLoopFiles.toArray + def triggerLoopFiles: Seq[File] +} +/** You likely want to use the factory method in the BasicBuild class instead of this. */ +final case class DirectoryDependency(context: Context) extends TriggerLoop{ + override def show = this.getClass.getSimpleName ++ "(" ++ context.projectDirectory.string ++ ")" + lazy val logger = context.logger + override lazy val lib: Lib = new Lib(logger) + private lazy val root = lib.loadRoot( context.copy(args=Seq()) ) + lazy val build = root.finalBuild + def exportedClasspath = ClassPath() + def dependencies = Seq(build) + def triggerLoopFiles = root.triggerLoopFiles + def needsUpdate = build.needsUpdate + def targetClasspath = ClassPath() +} +/* +case class DependencyOr(first: DirectoryDependency, second: JavaDependency) extends ProjectProxy with DirectoryDependencyBase{ + val isFirst = new File(first.context.projectDirectory).exists + def triggerLoopFiles = if(isFirst) first.triggerLoopFiles else Seq() + protected val delegate = if(isFirst) first else second +} +*/
\ No newline at end of file diff --git a/stage2/GitDependency.scala b/stage2/GitDependency.scala new file mode 100644 index 0000000..650fd09 --- /dev/null +++ b/stage2/GitDependency.scala @@ -0,0 +1,82 @@ +package cbt +import java.io._ +import java.nio.file.Files.readAllBytes +import java.net._ +import org.eclipse.jgit.api._ +import org.eclipse.jgit.internal.storage.file._ +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider +import org.eclipse.jgit.lib.Ref + +object GitDependency{ + val GitUrl = "(git:|https:|file:/)//([^/]+)/(.+)".r +} +case class GitDependency( + url: String, ref: String, subDirectory: Option[String] = None // example: git://github.com/cvogt/cbt.git#<some-hash> +)(implicit val logger: Logger, classLoaderCache: ClassLoaderCache, context: Context ) extends DependencyImplementation{ + import GitDependency._ + override def lib = new Lib(logger) + + // TODO: add support for authentication via ssh and/or https + // See http://www.codeaffine.com/2014/12/09/jgit-authentication/ + private val GitUrl( _, domain, path ) = url + + private val credentialsFile = context.projectDirectory ++ "/git.login" + + private object checkoutCache extends Cache[File] + + private def authenticate(_git: CloneCommand) = + if(!credentialsFile.exists){ + _git + } else { + val (user, password) = { + // TODO: implement safer method than reading credentials from plain text file + val c = new String(readAllBytes(credentialsFile.toPath)).split("\n").head.trim.split(":") + (c(0), c.drop(1).mkString(":")) + } + _git.setCredentialsProvider( new UsernamePasswordCredentialsProvider(user, password) ) + } + + def checkout: File = checkoutCache{ + val checkoutDirectory = context.cache ++ s"/git/$domain/$path/$ref" + val _git = if(checkoutDirectory.exists){ + logger.git(s"Found existing checkout of $url#$ref in $checkoutDirectory") + val _git = new Git(new FileRepository(checkoutDirectory ++ "/.git")) + val actualRef = _git.getRepository.getBranch + if(actualRef != ref){ + logger.git(s"actual ref '$actualRef' does not match expected ref '$ref' - fetching and checking out") + _git.fetch().call() + _git.checkout().setName(ref).call + } + _git + } else { + logger.git(s"Cloning $url into $checkoutDirectory") + val _git = authenticate( + Git + .cloneRepository() + .setURI(url) + .setDirectory(checkoutDirectory) + ).call() + + logger.git(s"Checking out ref $ref") + _git.checkout().setName(ref).call() + _git + } + val actualRef = _git.getRepository.getBranch + assert( actualRef == ref, s"actual ref '$actualRef' does not match expected ref '$ref'") + checkoutDirectory + } + private object dependencyCache extends Cache[DependencyImplementation] + def dependency = dependencyCache{ + DirectoryDependency( + context.copy( + projectDirectory = checkout ++ subDirectory.map("/" ++ _).getOrElse("") + ) + ) + } + + def dependencies = Seq(dependency) + + def exportedClasspath = ClassPath() + private[cbt] def targetClasspath = exportedClasspath + def needsUpdate: Boolean = false +} diff --git a/stage2/Lib.scala b/stage2/Lib.scala new file mode 100644 index 0000000..25183a3 --- /dev/null +++ b/stage2/Lib.scala @@ -0,0 +1,521 @@ +package cbt + +import java.io._ +import java.net._ +import java.lang.reflect.InvocationTargetException +import java.nio.file.{Path =>_,_} +import java.nio.file.Files.{readAllBytes, deleteIfExists, delete} +import java.security.MessageDigest +import java.util.jar._ +import java.lang.reflect.Method + +import scala.util._ + +// pom model +case class Developer(id: String, name: String, timezone: String, url: URL) + +/** Don't extend. Create your own libs :). */ +final class Lib(logger: Logger) extends Stage1Lib(logger) with Scaffold{ + lib => + + val buildClassName = "Build" + val buildBuildClassName = "BuildBuild" + + def copy(cls: Class[_], context: Context) = + cls + .getConstructor(classOf[Context]) + .newInstance(context) + + /** Loads Build for given Context */ + def loadDynamic(context: Context, default: Context => BuildInterface = new BasicBuild(_)): BuildInterface = { + context.logger.composition( context.logger.showInvocation("Build.loadDynamic",context) ) + loadRoot(context, default).finalBuild + } + /** + Loads whatever Build needs to be executed first in order to eventually build the build for the given context. + This can either the Build itself, of if exists a BuildBuild or a BuildBuild for a BuildBuild and so on. + */ + def loadRoot(context: Context, default: Context => BuildInterface = new BasicBuild(_)): BuildInterface = { + context.logger.composition( context.logger.showInvocation("Build.loadRoot",context.projectDirectory) ) + def findStartDir(projectDirectory: File): File = { + val buildDir = realpath( projectDirectory ++ "/build" ) + if(buildDir.exists) findStartDir(buildDir) else projectDirectory + } + + val start = findStartDir(context.projectDirectory) + + val useBasicBuildBuild = context.projectDirectory == start + + val rootBuildClassName = if( useBasicBuildBuild ) buildBuildClassName else buildClassName + try{ + if(useBasicBuildBuild) default( context ) else new cbt.BasicBuild( context.copy( projectDirectory = start ) ) with BuildBuild + } catch { + case e:ClassNotFoundException if e.getMessage == rootBuildClassName => + throw new Exception(s"no class $rootBuildClassName found in " ++ start.string) + } + } + + def srcJar(sourceFiles: Seq[File], artifactId: String, scalaMajorVersion: String, version: String, jarTarget: File): Option[File] = { + lib.jarFile( + jarTarget ++ ("/"++artifactId++"_"++scalaMajorVersion++"-"++version++"-sources.jar"), + sourceFiles + ) + } + + def jar(artifactId: String, scalaMajorVersion: String, version: String, compileTarget: File, jarTarget: File): Option[File] = { + lib.jarFile( + jarTarget ++ ("/"++artifactId++"_"++scalaMajorVersion++"-"++version++".jar"), + Seq(compileTarget) + ) + } + + def docJar( + cbtHasChanged: Boolean, + scalaVersion: String, + sourceFiles: Seq[File], + dependencyClasspath: ClassPath, + docTarget: File, + jarTarget: File, + artifactId: String, + scalaMajorVersion: String, + version: String, + compileArgs: Seq[String], + classLoaderCache: ClassLoaderCache, + mavenCache: File + ): Option[File] = { + if(sourceFiles.isEmpty){ + None + } else { + docTarget.mkdirs + val args = Seq( + // FIXME: can we use compiler dependency here? + "-cp", dependencyClasspath.string, // FIXME: does this break for builds that don't have scalac dependencies? + "-d", docTarget.toString + ) ++ compileArgs ++ sourceFiles.map(_.toString) + logger.lib("creating docs for source files "+args.mkString(", ")) + redirectOutToErr{ + runMain( + "scala.tools.nsc.ScalaDoc", + args, + ScalaDependencies(cbtHasChanged,mavenCache,scalaVersion)(logger).classLoader(classLoaderCache) + ) + } + lib.jarFile( + jarTarget ++ ("/"++artifactId++"_"++scalaMajorVersion++"-"++version++"-javadoc.jar"), + Vector(docTarget) + ) + } + } + + // task reflection helpers + def tasks(cls:Class[_]): Map[String, Method] = + Stream + .iterate(cls.asInstanceOf[Class[Any]])(_.getSuperclass) + .takeWhile(_ != null) + .toVector + .dropRight(1) // drop Object + .reverse + .flatMap( + c => + c + .getDeclaredMethods + .filterNot( _.getName contains "$" ) + .filter{ m => + java.lang.reflect.Modifier.isPublic(m.getModifiers) + } + .filter( _.getParameterTypes.length == 0 ) + .map(m => NameTransformer.decode(m.getName) -> m) + ).toMap + + def taskNames(cls: Class[_]): Seq[String] = tasks(cls).keys.toVector.sorted + + def usage(buildClass: Class[_], show: String): String = { + val baseTasks = Seq( + classOf[BasicBuild], + classOf[PackageJars], + classOf[Publish] + ).flatMap(lib.taskNames).distinct.sorted + val thisTasks = lib.taskNames(buildClass) diff baseTasks + ( + ( + if( thisTasks.nonEmpty ){ + s"""Methods provided by Build ${show} + + ${thisTasks.mkString(" ")} + +""" + } else "" + ) ++ s"""Methods provided by CBT (but possibly overwritten) + + ${baseTasks.mkString(" ")}""" + ) ++ "\n" + } + + class ReflectBuild[T:scala.reflect.ClassTag](build: BuildInterface) extends ReflectObject(build){ + def usage = lib.usage(build.getClass, build.show) + } + abstract class ReflectObject[T](obj: T){ + def usage: String + def callNullary( taskName: Option[String] ): ExitCode = { + logger.lib("Calling task " ++ taskName.toString) + val ts = tasks(obj.getClass) + taskName.map( NameTransformer.encode ).flatMap(ts.get).map{ method => + val result: Option[Any] = Option(method.invoke(obj)) // null in case of Unit + result.flatMap{ + case v: Option[_] => v + case other => Some(other) + }.map{ + value => + // Try to render console representation. Probably not the best way to do this. + scala.util.Try( value.getClass.getDeclaredMethod("toConsole") ) match { + case scala.util.Success(toConsole) => + println(toConsole.invoke(value)) + ExitCode.Success + + case scala.util.Failure(e) if Option(e.getMessage).getOrElse("") contains "toConsole" => + value match { + case code if code.getClass.getSimpleName == "ExitCode" => + // FIXME: ExitCode needs to be part of the compatibility interfaces + ExitCode(Stage0Lib.get(code,"integer").asInstanceOf[Int]) + case other => + println( other.toString ) // no method .toConsole, using to String + ExitCode.Success + } + + case scala.util.Failure(e) => + throw e + } + }.getOrElse(ExitCode.Success) + }.getOrElse{ + taskName.foreach{ n => + System.err.println(s"Method not found: $n") + System.err.println("") + } + System.err.println(usage) + taskName.map{ _ => + ExitCode.Failure + }.getOrElse( ExitCode.Success ) + } + } + } + + def consoleOrFail(msg: String) = { + Option(System.console).getOrElse( + throw new Exception(msg + ". System.console() == null. See https://github.com/cvogt/cbt/issues/236") + ) + } + + def clean(target: File, force: Boolean, dryRun: Boolean, list: Boolean, help: Boolean): ExitCode = { + def depthFirstFileStream(file: File): Vector[File] = { + ( + if (file.isDirectory) { + file.listFiles.toVector.flatMap(depthFirstFileStream(_)) + } else Vector() + ) :+ file + } + lazy val files = depthFirstFileStream( target ) + + if( help ){ + System.err.println( s""" + list lists files to be delete + force does not ask for confirmation + dry-run does not actually delete files +""" ) + ExitCode.Success + } else if (!target.exists){ + System.err.println( "Nothing to clean. Does not exist: " ++ target.string ) + ExitCode.Success + } else if( list ){ + files.map(_.string).foreach( println ) + ExitCode.Success + } else { + val performDelete = ( + force || { + val console = consoleOrFail("Use `cbt direct clean` or `cbt clean help`") + System.err.println("Files to be deleted:\n\n") + files.foreach( System.err.println ) + System.err.println("") + System.err.print("To delete the above files type 'delete': ") + console.readLine() == "delete" + } + ) + + if( !performDelete ) { + System.err.println( "Ok, not cleaning." ) + ExitCode.Failure + } else { + // use same Vector[File] that was displayed earlier as a safety measure + files.foreach{ file => + System.err.println( red("Deleting") ++ " " ++ file.string ) + if(!dryRun){ + delete( file.toPath ) + } + } + System.err.println( "Done." ) + ExitCode.Success + } + } + } + + // file system helpers + def basename(path: File): String = path.toString.stripSuffix("/").split("/").last + def dirname(path: File): File = new File(realpath(path).string.stripSuffix("/").split("/").dropRight(1).mkString("/")) + def nameAndContents(file: File) = basename(file) -> readAllBytes(file.toPath) + + /** Which file endings to consider being source files. */ + def sourceFileFilter(file: File): Boolean = file.toString.endsWith(".scala") || file.toString.endsWith(".java") + + def sourceFiles( sources: Seq[File], sourceFileFilter: File => Boolean = sourceFileFilter ): Seq[File] = { + for { + base <- sources.filter(_.exists).map(lib.realpath) + file <- lib.listFilesRecursive(base) if file.isFile && sourceFileFilter(file) + } yield file + } + + // FIXME: for some reason it includes full path in docs + def jarFile( jarFile: File, files: Seq[File], mainClass: Option[String] = None ): Option[File] = { + Files.deleteIfExists(jarFile.toPath) + if( files.isEmpty ){ + None + } else { + jarFile.getParentFile.mkdirs + logger.lib("Start packaging "++jarFile.string) + val manifest = new Manifest() + manifest.getMainAttributes.put( Attributes.Name.MANIFEST_VERSION, "1.0" ) + manifest.getMainAttributes.putValue( "Created-By", "Chris' Build Tool" ) + mainClass foreach { className => + manifest.getMainAttributes.put(Attributes.Name.MAIN_CLASS, className) + } + val jar = new JarOutputStream(new FileOutputStream(jarFile), manifest) + try{ + val names = for { + base <- files.filter(_.exists).map(realpath) + file <- listFilesRecursive(base) if file.isFile + } yield { + val name = if(base.isDirectory){ + file.toString stripPrefix (base.toString ++ File.separator) + } else file.toString + val entry = new JarEntry( name ) + entry.setTime(file.lastModified) + jar.putNextEntry(entry) + jar.write( readAllBytes( file.toPath ) ) + jar.closeEntry() + name + } + + val duplicateFiles = (names diff names.distinct).distinct + assert( + duplicateFiles.isEmpty, + s"Conflicting file names when trying to create $jarFile: "++duplicateFiles.mkString(", ") + ) + } finally { + jar.close() + } + + logger.lib("Done packaging " ++ jarFile.toString) + + Some(jarFile) + } + } + + lazy val passphrase = + consoleOrFail( "Use `cbt direct <task>`" ).readPassword( "GPG Passphrase please:" ).mkString + + def sign(file: File): File = { + //http://stackoverflow.com/questions/16662408/correct-way-to-sign-and-verify-signature-using-bouncycastle + val statusCode = + new ProcessBuilder( "gpg", "--batch", "--yes", "-a", "-b", "-s", "--passphrase", passphrase, file.toString ) + .inheritIO.start.waitFor + + if( 0 != statusCode ) throw new Exception("gpg exited with status code " ++ statusCode.toString) + + file ++ ".asc" + } + + //def requiredForPom[T](name: String): T = throw new Exception(s"You need to override `def $name` in order to generate a valid pom.") + + def pom( + groupId: String, + artifactId: String, + version: String, + scalaMajorVersion: String, + name: String, + description: String, + url: URL, + developers: Seq[Developer], + licenses: Seq[License], + scmUrl: String, // seems like invalid URLs are used here in pom files + scmConnection: String, + inceptionYear: Int, + organization: Option[Organization], + dependencies: Seq[Dependency], + jarTarget: File + ): File = { + val xml = +<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"> + <modelVersion>4.0.0</modelVersion> + <groupId>{groupId}</groupId> + <artifactId>{artifactId ++ "_" ++ scalaMajorVersion}</artifactId> + <version>{version}</version> + <packaging>jar</packaging> + <name>{name}</name> + <description>{description}</description> + <url>{url}</url> + <licenses> + {licenses.map{ license => + <license> + <name>{license.name}</name> + {license.url.map(url => <url>url</url>).getOrElse( scala.xml.NodeSeq.Empty )} + <distribution>repo</distribution> + </license> + }} + </licenses> + <developers> + {developers.map{ developer => + <developer> + <id>{developer.id}</id> + <name>{developer.name}</name> + <timezone>{developer.timezone}</timezone> + <url>{developer.url}</url> + </developer> + }} + </developers> + <scm> + <url>{scmUrl}</url> + <connection>{scmConnection}</connection> + </scm> + <inceptionYear>{inceptionYear}</inceptionYear> + {organization.map{ org => + <organization> + <name>{org.name}</name> + {org.url.map( url => <url>url</url> ).getOrElse( scala.xml.NodeSeq.Empty )} + </organization> + }.getOrElse(scala.xml.NodeSeq.Empty)} + <dependencies> + {dependencies.map{ + case d:ArtifactInfo => + <dependency> + <groupId>{d.groupId}</groupId> + <artifactId>{d.artifactId}</artifactId> + <version>{d.version}</version> + </dependency> + }} + </dependencies> +</project> + // FIXME: do not build this file name including scalaMajorVersion in multiple places + val path = jarTarget.toString ++ ( "/" ++ artifactId++ "_" ++ scalaMajorVersion ++ "-" ++ version ++ ".pom" ) + val file = new File(path) + write(file, "<?xml version='1.0' encoding='UTF-8'?>\n" ++ xml.toString) + } + + def concurrently[T,R]( concurrencyEnabled: Boolean )( items: Seq[T] )( projection: T => R ): Seq[R] = { + if(concurrencyEnabled) items.par.map(projection).seq + else items.map(projection) + } + + def publishUnsigned( sourceFiles: Seq[File], artifacts: Seq[File], url: URL, credentials: Option[String] = None ): Unit = { + if(sourceFiles.nonEmpty){ + publish( artifacts, url, credentials ) + } + } + + def publishLocal( sourceFiles: Seq[File], artifacts: Seq[File], mavenCache: File, releaseFolder: String ): Unit = { + if(sourceFiles.nonEmpty){ + val targetDir = mavenCache ++ releaseFolder.stripSuffix("/") + targetDir.mkdirs + artifacts.foreach{ a => + val target = targetDir ++ ("/" ++ a.getName) + System.err.println(blue("publishing ") ++ target.getPath) + Files.copy( a.toPath, target.toPath, StandardCopyOption.REPLACE_EXISTING ) + } + } + } + + def publishSigned( artifacts: Seq[File], url: URL, credentials: Option[String] = None ): Unit = { + // TODO: make concurrency configurable here + publish( artifacts ++ artifacts.map(sign), url, credentials ) + } + + private def publish(artifacts: Seq[File], url: URL, credentials: Option[String]): Unit = { + val files = artifacts.map(nameAndContents) + lazy val checksums = files.flatMap{ + case (name, content) => Seq( + name++".md5" -> md5(content).toArray.map(_.toByte), + name++".sha1" -> sha1(content).toArray.map(_.toByte) + ) + } + val all = (files ++ checksums) + uploadAll(url, all, credentials) + } + + def uploadAll(url: URL, nameAndContents: Seq[(String, Array[Byte])], credentials: Option[String] = None ): Unit = + nameAndContents.foreach { case (name, content) => upload(name, content, url, credentials ) } + + def upload(fileName: String, fileContents: Array[Byte], baseUrl: URL, credentials: Option[String] = None): Unit = { + import java.net._ + import java.io._ + val url = baseUrl ++ "/" ++ fileName + System.err.println(blue("uploading ") ++ url.toString) + val httpCon = Stage0Lib.openConnectionConsideringProxy(url) + httpCon.setDoOutput(true) + httpCon.setRequestMethod("PUT") + credentials.foreach( + c => { + val encoding = new sun.misc.BASE64Encoder().encode(c.getBytes) + httpCon.setRequestProperty("Authorization", "Basic " ++ encoding) + } + ) + httpCon.setRequestProperty("Content-Type", "application/binary") + httpCon.getOutputStream.write( + fileContents + ) + httpCon.getInputStream + } + + + // code for continuous compile + def watch(files: Seq[File])(action: PartialFunction[File, Unit]): Unit = { + import com.barbarysoftware.watchservice._ + import scala.collection.JavaConversions._ + val watcher = WatchService.newWatchService + + val realFiles = files.map(realpath) + + realFiles.map{ + // WatchService can only watch folders + case file if file.isFile => dirname(file) + case file => file + }.distinct.map{ file => + val watchableFile = new WatchableFile(file) + val key = watchableFile.register( + watcher, + StandardWatchEventKind.ENTRY_CREATE, + StandardWatchEventKind.ENTRY_DELETE, + StandardWatchEventKind.ENTRY_MODIFY + ) + } + + scala.util.control.Breaks.breakable{ + while(true){ + logger.loop("Waiting for file changes...") + logger.loop("Waiting for file changes...2") + Option(watcher.take).map{ + key => + val changedFiles = key + .pollEvents + .toVector + .filterNot(_.kind == StandardWatchEventKind.OVERFLOW) + .map(_.context.toString) + // make sure we don't react on other files changed + // in the same folder like the files we care about + .filter{ name => realFiles.exists(name startsWith _.toString) } + .map(new File(_)) + + changedFiles.foreach( f => logger.loop( "Changed: " ++ f.toString ) ) + changedFiles.collect(action) + key.reset + } + } + } + } +} diff --git a/stage2/License.scala b/stage2/License.scala new file mode 100644 index 0000000..a35a922 --- /dev/null +++ b/stage2/License.scala @@ -0,0 +1,57 @@ +package cbt +import java.net.URL +case class License(name: String, shortName: String, url: Option[String]) +object License{ + val PublicDomain = License("Public Domain", "Public Domain", None) + val Scala = License ("Scala License", "Scala License", Some("http://www.scala-lang.org/license.html")) + val TypesafeSubscriptionAgreement = License( + "Typesafe Subscription Agreement", "Typesafe Subscription Agreement", + Some("http://downloads.typesafe.com/website/legal/TypesafeSubscriptionAgreement.pdf") + ) + + private def spdx(id: String, name: String) = License(name, id, Some(s"https://spdx.org/licenses/$id.html")) + val Academic = spdx("AFL-3.0", "Academic Free License") + val Affero = spdx("AGPL-3.0", "GNU Affero General Public License v3.0") + val Apache2 = spdx("Apache-2.0", "Apache License 2.0") + val Apple2_0 = spdx("APSL-2.0", "Apple Public Source License 2.0") + val Beerware = spdx("Beerware", "Beerware License") + val Bsd2Clause = spdx("BSD-2-Clause", """BSD 2-clause "Simplified" License""") + val Bsd3Clause = spdx("BSD-3-Clause", """BSD 3-clause "New" or "Revised" License""") + val BsdOriginal = spdx("BSD-4-Clause", """BSD 4-clause "Original" or "Old" License""") + val CreativeCommonsZeroUniversal = spdx("CC0-1.0", "Creative Commons Zero v1.0 Universal") + val CreativeCommonsAttributionNonCommercialShareAlike_2_0 = spdx("CC-BY-NC-SA-2.0", "Creative Commons Attribution Non Commercial Share Alike 2.0") + val CreativeCommonsAttributionNonCommercialShareAlike_2_5 = spdx("CC-BY-NC-SA-2.5", "Creative Commons Attribution Non Commercial Share Alike 2.5") + val CreativeCommonsAttributionNonCommercialShareAlike_3_0 = spdx("CC-BY-NC-SA-3.0", "Creative Commons Attribution Non Commercial Share Alike 3.0") + val CreativeCommonsAttributionNonCommercialShareAlike_4_0 = spdx("CC-BY-NC-SA-4.0", "Creative Commons Attribution Non Commercial Share Alike 4.0") + val CreativeCommonsAttributionShareAlike_2_5 = spdx("CC-BY-SA-2.5", "Creative Commons Attribution Share Alike 2.5") + val CreativeCommonsAttribution_3_0 = spdx("CC-BY-3.0", "Creative Commons Attribution 3.0") + val CreativeCommonsAttributionShareAlike_3_0 = spdx("CC-BY-SA-3.0", "Creative Commons Attribution Share Alike 3.0") + val CreativeCommonsAttribution_4_0 = spdx("CC-BY-4.0", "Creative Commons Attribution 4.0") + val CreativeCommonsAttributionShareAlike_4_0 = spdx("CC-BY-SA-4.0", "Creative Commons Attribution Share Alike 4.0") + val Eclipse = spdx("EPL-1.0", "Eclipse Public License 1.0") + val GPL1 = spdx("GPL-1.0", "GNU General Public License v1.0 only") + val GPL1Plus = spdx("GPL-1.0+", "GNU General Public License v1.0 or later") + val GPL2 = spdx("GPL-2.0", "GNU General Public License v2.0 only") + val GPL2Plus = spdx("GPL-2.0+", "GNU General Public License v2.0 or later") + val GPl3 = spdx("GPL-3.0", "GNU General Public License v3.0 only") + val GPL3Plus = spdx("GPL-3.0+", "GNU General Public License v3.0 or later") + val ISC = spdx("ISC", "ISC License") + val LGPL2 = spdx("LGPL-2.0", "GNU Library General Public License v2 only") + // @deprecated("-", "-") + val LGPL2_Plus = spdx("LGPL-2.0+", "GNU Library General Public License v2 or later") + val LGPL2_1 = spdx("LGPL-2.1", "GNU Library General Public License v2.1 only") + // @deprecated("-", "-") + val LGPL2_1_Plus = spdx("LGPL-2.1+", "GNU Library General Public License v2.1 or later") + val LGPL3 = spdx("LGPL-3.0", "GNU Lesser General Public License v3.0 only") + // @deprecated("use LGPL3", "2.0rc2") + val LGPL3_Plus = spdx("LGPL-3.0+", "GNU Lesser General Public License v3.0 or later") + /** Spdx.org does not (yet) differentiate between the X11 and Expat versions + for details see http://en.wikipedia.org/wiki/MIT_License#Various_versions */ + val MIT = spdx("MIT", "MIT License") + val MPL_1_0 = spdx("MPL-1.0", "Mozilla Public License 1.0") + val MPL_1_1 = spdx("MPL-1.1", "Mozilla Public License 1.1") + val MPL2 = spdx("MPL-2.0", "Mozilla Public License 2.0") + val Unlicense = spdx("Unlicense", "The Unlicense") + val W3C = spdx("W3C", "W3C Software Notice and License") + val WTFPL = spdx("WTFPL", "Do What The F*ck You Want To Public License") +} diff --git a/stage2/NameTransformer.scala b/stage2/NameTransformer.scala new file mode 100644 index 0000000..33489ca --- /dev/null +++ b/stage2/NameTransformer.scala @@ -0,0 +1,161 @@ +// Adapted from https://github.com/scala/scala/blob/5cb3d4ec14488ce2fc5a1cc8ebdd12845859c57d/src/library/scala/reflect/NameTransformer.scala +/* __ *\ +** ________ ___ / / ___ Scala API ** +** / __/ __// _ | / / / _ | (c) 2003-2013, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** +** /____/\___/_/ |_/____/_/ | | ** +** |/ ** +\* */ + +package cbt + +/** Provides functions to encode and decode Scala symbolic names. + * Also provides some constants. + */ +object NameTransformer { + // XXX Short term: providing a way to alter these without having to recompile + // the compiler before recompiling the compiler. + val MODULE_SUFFIX_STRING = sys.props.getOrElse("SCALA_MODULE_SUFFIX_STRING", "$") + val NAME_JOIN_STRING = sys.props.getOrElse("SCALA_NAME_JOIN_STRING", "$") + val MODULE_INSTANCE_NAME = "MODULE$" + val LOCAL_SUFFIX_STRING = " " + val SETTER_SUFFIX_STRING = "_$eq" + val TRAIT_SETTER_SEPARATOR_STRING = "$_setter_$" + + private val nops = 128 + private val ncodes = 26 * 26 + + private class OpCodes(val op: Char, val code: String, val next: OpCodes) + + private val op2code = new Array[String](nops) + private val code2op = new Array[OpCodes](ncodes) + private def enterOp(op: Char, code: String) = { + op2code(op.toInt) = code + val c = (code.charAt(1) - 'a') * 26 + code.charAt(2) - 'a' + code2op(c.toInt) = new OpCodes(op, code, code2op(c)) + } + + /* Note: decoding assumes opcodes are only ever lowercase. */ + enterOp('~', "$tilde") + enterOp('=', "$eq") + enterOp('<', "$less") + enterOp('>', "$greater") + enterOp('!', "$bang") + enterOp('#', "$hash") + enterOp('%', "$percent") + enterOp('^', "$up") + enterOp('&', "$amp") + enterOp('|', "$bar") + enterOp('*', "$times") + enterOp('/', "$div") + enterOp('+', "$plus") + enterOp('-', "$minus") + enterOp(':', "$colon") + enterOp('\\', "$bslash") + enterOp('?', "$qmark") + enterOp('@', "$at") + + /** Replace operator symbols by corresponding `\$opname`. + * + * @param name the string to encode + * @return the string with all recognized opchars replaced with their encoding + */ + def encode(name: String): String = { + var buf: StringBuilder = null + val len = name.length() + var i = 0 + while (i < len) { + val c = name charAt i + if (c < nops && (op2code(c.toInt) ne null)) { + if (buf eq null) { + buf = new StringBuilder() + buf.append(name.substring(0, i)) + } + buf.append(op2code(c.toInt)) + /* Handle glyphs that are not valid Java/JVM identifiers */ + } + else if (!Character.isJavaIdentifierPart(c)) { + if (buf eq null) { + buf = new StringBuilder() + buf.append(name.substring(0, i)) + } + buf.append("$u%04X".format(c.toInt)) + } + else if (buf ne null) { + buf.append(c) + } + i += 1 + } + if (buf eq null) name else buf.toString() + } + + /** Replace `\$opname` by corresponding operator symbol. + * + * @param name0 the string to decode + * @return the string with all recognized operator symbol encodings replaced with their name + */ + def decode(name0: String): String = { + //System.out.println("decode: " + name);//DEBUG + val name = if (name0.endsWith("<init>")) name0.stripSuffix("<init>") + "this" + else name0 + var buf: StringBuilder = null + val len = name.length() + var i = 0 + while (i < len) { + var ops: OpCodes = null + var unicode = false + val c = name charAt i + if (c == '$' && i + 2 < len) { + val ch1 = name.charAt(i+1) + if ('a' <= ch1 && ch1 <= 'z') { + val ch2 = name.charAt(i+2) + if ('a' <= ch2 && ch2 <= 'z') { + ops = code2op((ch1 - 'a') * 26 + ch2 - 'a') + while ((ops ne null) && !name.startsWith(ops.code, i)) ops = ops.next + if (ops ne null) { + if (buf eq null) { + buf = new StringBuilder() + buf.append(name.substring(0, i)) + } + buf.append(ops.op) + i += ops.code.length() + } + /* Handle the decoding of Unicode glyphs that are + * not valid Java/JVM identifiers */ + } else if ((len - i) >= 6 && // Check that there are enough characters left + ch1 == 'u' && + ((Character.isDigit(ch2)) || + ('A' <= ch2 && ch2 <= 'F'))) { + /* Skip past "$u", next four should be hexadecimal */ + val hex = name.substring(i+2, i+6) + try { + val str = Integer.parseInt(hex, 16).toChar + if (buf eq null) { + buf = new StringBuilder() + buf.append(name.substring(0, i)) + } + buf.append(str) + /* 2 for "$u", 4 for hexadecimal number */ + i += 6 + unicode = true + } catch { + case _:NumberFormatException => + /* `hex` did not decode to a hexadecimal number, so + * do nothing. */ + } + } + } + } + /* If we didn't see an opcode or encoded Unicode glyph, and the + buffer is non-empty, write the current character and advance + one */ + if ((ops eq null) && !unicode) { + if (buf ne null) + buf.append(c) + i += 1 + } + } + //System.out.println("= " + (if (buf == null) name else buf.toString()));//DEBUG + if (buf eq null) name else buf.toString() + } +} diff --git a/stage2/PackageJars.scala b/stage2/PackageJars.scala new file mode 100644 index 0000000..a101993 --- /dev/null +++ b/stage2/PackageJars.scala @@ -0,0 +1,33 @@ +package cbt +import java.io.File + +// would love to call this just `Package` but that conflicts with scala package objects. +trait PackageJars extends BaseBuild with ArtifactInfo{ + def name: String + def artifactId = name + def defaultVersion: String + final def version = context.version getOrElse defaultVersion + def `package`: Seq[File] = lib.concurrently( enableConcurrency )( + Seq(() => jar, () => docJar, () => srcJar) + )( _() ).flatten + + private object cacheJarBasicBuild extends Cache[Option[File]] + def jar: Option[File] = cacheJarBasicBuild{ + compile.flatMap( lib.jar( artifactId, scalaMajorVersion, version, _, jarTarget ) ) + } + + private object cacheSrcJarBasicBuild extends Cache[Option[File]] + def srcJar: Option[File] = cacheSrcJarBasicBuild{ + lib.srcJar( sourceFiles, artifactId, scalaMajorVersion, version, scalaTarget ) + } + + private object cacheDocBasicBuild extends Cache[Option[File]] + def docJar: Option[File] = cacheDocBasicBuild{ + lib.docJar( + context.cbtHasChanged, + scalaVersion, sourceFiles, compileClasspath, docTarget, + jarTarget, artifactId, scalaMajorVersion, version, + scalacOptions, context.classLoaderCache, context.paths.mavenCache + ) + } +} diff --git a/stage2/Plugin.scala b/stage2/Plugin.scala new file mode 100644 index 0000000..94a8749 --- /dev/null +++ b/stage2/Plugin.scala @@ -0,0 +1,4 @@ +package cbt +trait Plugin extends BaseBuild{ + override def dependencies = super.dependencies :+ context.cbtDependency +} diff --git a/stage2/Publish.scala b/stage2/Publish.scala new file mode 100644 index 0000000..7e00620 --- /dev/null +++ b/stage2/Publish.scala @@ -0,0 +1,49 @@ +package cbt +import java.io.File +import java.net.URL +import java.nio.file.Files.readAllBytes + +trait Publish extends PackageJars{ + def description: String + def url: URL + def developers: Seq[Developer] + def licenses: Seq[License] + def scmUrl: String + def scmConnection: String + def inceptionYear: Int + def organization: Option[Organization] + + // ========== package ========== + + /** put additional xml that should go into the POM file in here */ + def pom: File = lib.pom( + groupId = groupId, + artifactId = artifactId, + version = version, + scalaMajorVersion = scalaMajorVersion, + name = name, + description = description, + url = url, + developers = developers, + licenses = licenses, + scmUrl = scmUrl, + scmConnection = scmConnection, + inceptionYear, + organization, + dependencies = dependencies, + jarTarget = jarTarget + ) + + // ========== publish ========== + private val releaseFolder = s"/${groupId.replace(".","/")}/${artifactId}_$scalaMajorVersion/$version/" + + def publishLocal: Unit = + lib.publishLocal( sourceFiles, `package` :+ pom, context.paths.mavenCache, releaseFolder ) + + def publishSnapshotLocal: Unit = + copy( context.copy(version = Some(version+"-SNAPSHOT")) ).publishLocal + + def isSnapshot: Boolean = version.endsWith("-SNAPSHOT") + + override def copy(context: Context) = super.copy(context).asInstanceOf[Publish] +} diff --git a/stage2/SbtDependencyDsl.scala b/stage2/SbtDependencyDsl.scala new file mode 100644 index 0000000..05cb709 --- /dev/null +++ b/stage2/SbtDependencyDsl.scala @@ -0,0 +1,15 @@ +package cbt +trait SbtDependencyDsl{ self: BaseBuild => + /** SBT-like dependency builder DSL for syntax compatibility */ + class DependencyBuilder2( groupId: String, artifactId: String, scalaVersion: Option[String] ){ + def %(version: String) = scalaVersion.map( + v => ScalaDependency(groupId, artifactId, version, scalaVersion = v) + ).getOrElse( + MavenDependency(groupId, artifactId, version) + ) + } + implicit class DependencyBuilder(groupId: String){ + def %%(artifactId: String) = new DependencyBuilder2( groupId, artifactId, Some(scalaMajorVersion) ) + def %(artifactId: String) = new DependencyBuilder2( groupId, artifactId, None ) + } +}
\ No newline at end of file diff --git a/stage2/Scaffold.scala b/stage2/Scaffold.scala new file mode 100644 index 0000000..866e5da --- /dev/null +++ b/stage2/Scaffold.scala @@ -0,0 +1,52 @@ +package cbt +import java.io._ +import java.nio.file._ +import java.net._ +trait Scaffold{ + def logger: Logger + + private def createFile( projectDirectory: File, fileName: String, code: String ){ + val outputFile = projectDirectory ++ ("/" ++ fileName) + Stage0Lib.write( outputFile, code, StandardOpenOption.CREATE_NEW ) + import scala.Console._ + println( GREEN ++ "Created " ++ fileName ++ RESET ) + } + + def createMain( + projectDirectory: File + ): Unit = { + createFile(projectDirectory, "Main.scala", s"""object Main{ + def main( args: Array[String] ): Unit = { + println( Console.GREEN ++ "Hello World" ++ Console.RESET ) + } +} +""" + ) + } + + def createBuild( + projectDirectory: File + ): Unit = { + createFile(projectDirectory, "build/build.scala", s"""import cbt._ +class Build(val context: Context) extends BaseBuild{ + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here for scala-library, etc. + Seq( + // source dependency + // DirectoryDependency( projectDirectory ++ "/subProject" ) + ) ++ + // pick resolvers explicitly for individual dependencies (and their transitive dependencies) + Resolver( mavenCentral, sonatypeReleases ).bind( + // CBT-style Scala dependencies + // ScalaDependency( "com.lihaoyi", "ammonite-ops", "0.5.5" ) + // MavenDependency( "com.lihaoyi", "ammonite-ops_2.11", "0.5.5" ) + + // SBT-style dependencies + // "com.lihaoyi" %% "ammonite-ops" % "0.5.5" + // "com.lihaoyi" % "ammonite-ops_2.11" % "0.5.5" + ) +} +""" + ) + } +} diff --git a/stage2/Stage2.scala b/stage2/Stage2.scala new file mode 100644 index 0000000..3d5c244 --- /dev/null +++ b/stage2/Stage2.scala @@ -0,0 +1,90 @@ +package cbt +import java.io._ + +object Stage2 extends Stage2Base{ + def getBuild(__context: java.lang.Object, _cbtChanged: java.lang.Boolean) = { + val _context = __context.asInstanceOf[Context] + val context = _context.copy( + cbtHasChanged = _context.cbtHasChanged || _cbtChanged + ) + val first = new Lib(context.logger).loadRoot( context ) + first.finalBuild + } + + def run( args: Stage2Args ): Unit = { + import args.logger + val paths = CbtPaths(args.cbtHome,args.cache) + import paths._ + val lib = new Lib(args.logger) + + logger.stage2(s"Stage2 start") + val loop = args.args.lift(0) == Some("loop") + val direct = args.args.lift(0) == Some("direct") + val cross = args.args.lift(0) == Some("cross") + + val taskIndex = if (loop || direct || cross) { + 1 + } else { + 0 + } + val task = args.args.lift( taskIndex ) + + val context: Context = ContextImplementation( + args.cwd, + args.cwd, + args.args.drop( taskIndex +1 ).toArray, + logger.enabledLoggers.toArray, + logger.start, + args.cbtHasChanged, + null, + null, + args.permanentKeys, + args.permanentClassLoaders, + args.cache, + args.cbtHome, + args.cbtHome, + args.compatibilityTarget, + null + ) + val first = lib.loadRoot( context ) + val build = first.finalBuild + + def call(build: BuildInterface): ExitCode = { + if(cross){ + build.crossScalaVersions.map{ + v => new lib.ReflectBuild( + build.copy(context.copy(scalaVersion = Some(v))) + ).callNullary(task) + }.filter(_ != ExitCode.Success).headOption getOrElse ExitCode.Success + } else { + new lib.ReflectBuild(build).callNullary(task) + } + } + + val res = + if (loop) { + // TODO: this should allow looping over task specific files, like test files as well + val triggerFiles = first.triggerLoopFiles.map(lib.realpath) + val triggerCbtFiles = Seq( nailgun, stage1, stage2 ).map(lib.realpath _) + val allTriggerFiles = triggerFiles ++ triggerCbtFiles + + logger.loop("Looping change detection over:\n - "++allTriggerFiles.mkString("\n - ")) + + lib.watch(allTriggerFiles){ + case file if triggerCbtFiles.exists(file.toString startsWith _.toString) => + logger.loop("Change is in CBT's own source code.") + logger.loop("Restarting CBT.") + scala.util.control.Breaks.break + + case file if triggerFiles.exists(file.toString startsWith _.toString) => + val build = lib.loadDynamic(context) + logger.loop(s"Re-running $task for " ++ build.show) + call(build) + } + } else { + val code = call(build) + logger.stage2(s"Stage2 end") + System.exit(code.integer) + } + } +} diff --git a/stage2/ToolsStage2.scala b/stage2/ToolsStage2.scala new file mode 100644 index 0000000..df615fc --- /dev/null +++ b/stage2/ToolsStage2.scala @@ -0,0 +1,12 @@ +package cbt +import java.io._ +object ToolsStage2 extends Stage2Base{ + def run( _args: Stage2Args ): Unit = { + val args = _args.args.dropWhile(Seq("tools","direct") contains _) + val lib = new Lib(_args.logger) + val toolsTasks = new ToolsTasks(lib, args, _args.cwd, _args.classLoaderCache, _args.cache, _args.cbtHome, _args.cbtHasChanged) + new lib.ReflectObject(toolsTasks){ + def usage: String = "Available methods: " ++ lib.taskNames(toolsTasks.getClass).mkString(" ") + }.callNullary(args.lift(0)) + } +} diff --git a/stage2/ToolsTasks.scala b/stage2/ToolsTasks.scala new file mode 100644 index 0000000..324b7d8 --- /dev/null +++ b/stage2/ToolsTasks.scala @@ -0,0 +1,150 @@ +package cbt +import java.net._ +import java.io.{Console=>_,_} +import java.nio.file._ +class ToolsTasks( + lib: Lib, + args: Seq[String], + cwd: File, + classLoaderCache: ClassLoaderCache, + cache: File, + cbtHome: File, + cbtHasChanged: Boolean +){ + private val paths = CbtPaths(cbtHome, cache) + import paths._ + private def Resolver( urls: URL* ) = MavenResolver(cbtHasChanged,mavenCache,urls: _*) + implicit val logger: Logger = lib.logger + def createMain: Unit = lib.createMain( cwd ) + def createBuild: Unit = lib.createBuild( cwd ) + def gui = NailgunLauncher.main(Array( + "0.0", + (cbtHome / "tools" / "gui").getAbsolutePath, + "run", + cwd.getAbsolutePath, + constants.scalaMajorVersion + )) + def resolve = { + ClassPath.flatten( + args(1).split(",").toVector.map{ + d => + val v = d.split(":") + Resolver(mavenCentral).bindOne(MavenDependency(v(0),v(1),v(2))).classpath + } + ) + } + def dependencyTree = { + args(1).split(",").toVector.map{ + d => + val v = d.split(":") + Resolver(mavenCentral).bindOne(MavenDependency(v(0),v(1),v(2))).dependencyTree + }.mkString("\n\n") + } + def amm = ammonite + def ammonite = { + val version = args.lift(1).getOrElse(constants.scalaVersion) + val classLoader = Resolver(mavenCentral).bindOne( + MavenDependency( + "com.lihaoyi","ammonite-repl_2.11.8",args.lift(1).getOrElse("0.5.8") + ) + ).classLoader(classLoaderCache) + // FIXME: this does not work quite yet, throws NoSuchFileException: /ammonite/repl/frontend/ReplBridge$.class + lib.runMain( + "ammonite.repl.Main", args.drop(2), classLoader + ) + } + def scala = { + val version = args.lift(1).getOrElse(constants.scalaVersion) + val scalac = new ScalaCompilerDependency( cbtHasChanged, mavenCache, version ) + val _args = Seq("-cp", scalac.classpath.string) ++ args.drop(2) + lib.runMain( + "scala.tools.nsc.MainGenericRunner", _args, scalac.classLoader(classLoaderCache) + ) + } + def cbtEarlyDependencies = { + val scalaVersion = args.lift(1).getOrElse(constants.scalaVersion) + val scalaMajorVersion = scalaVersion.split("\\.").take(2).mkString(".") + val scalaXmlVersion = args.lift(2).getOrElse(constants.scalaXmlVersion) + val zincVersion = args.lift(3).getOrElse(constants.zincVersion) + val scalaDeps = Seq( + Resolver(mavenCentral).bindOne(MavenDependency("org.scala-lang","scala-reflect",scalaVersion)), + Resolver(mavenCentral).bindOne(MavenDependency("org.scala-lang","scala-compiler",scalaVersion)) + ) + + val scalaXml = Dependencies( + Resolver(mavenCentral).bind( + MavenDependency("org.scala-lang.modules","scala-xml_"+scalaMajorVersion,scalaXmlVersion), + MavenDependency("org.scala-lang","scala-library",scalaVersion) + ) + ) + + val zinc = Resolver(mavenCentral).bindOne(MavenDependency("com.typesafe.zinc","zinc",zincVersion)) + + def valName(dep: BoundMavenDependency) = { + val words = dep.artifactId.split("_").head.split("-") + words(0) ++ words.drop(1).map(s => s(0).toString.toUpperCase ++ s.drop(1)).mkString ++ "_" ++ dep.version.replace(".","_") ++ "_" + } + + def jarVal(dep: BoundMavenDependency) = "_" + valName(dep) +"Jar" + def transitive(dep: Dependency) = (dep +: lib.transitiveDependencies(dep).reverse).collect{case d: BoundMavenDependency => d} + def codeEach(dep: Dependency) = { + transitive(dep).tails.map(_.reverse).toVector.reverse.drop(1).map{ + deps => + val d = deps.last + val parents = deps.dropRight(1) + val parentString = if(parents.isEmpty) "rootClassLoader" else ( valName(parents.last) ) + val n = valName(d) + s""" + // ${d.groupId}:${d.artifactId}:${d.version} + download(new URL(mavenUrl + "${d.basePath}.jar"), Paths.get(${n}File), "${d.jarSha1}"); + + String[] ${n}ClasspathArray = new String[]{${deps.sortBy(_.jar).map(valName(_)+"File").mkString(", ")}}; + String ${n}Classpath = classpath( ${n}ClasspathArray ); + ClassLoader $n = + classLoaderCache.contains( ${n}Classpath ) + ? classLoaderCache.get( ${n}Classpath ) + : classLoaderCache.put( classLoader( ${n}File, $parentString ), ${n}Classpath );""" + } + } + val assignments = codeEach(zinc) ++ codeEach(scalaXml) + val files = scalaDeps ++ transitive(scalaXml) ++ transitive(zinc) + //{ case (name, dep) => s"$name =\n ${tree(dep, 4)};" }.mkString("\n\n ") + val code = s"""// This file was auto-generated using `cbt tools cbtEarlyDependencies` +package cbt; +import java.io.*; +import java.nio.file.*; +import java.net.*; +import java.security.*; +import static cbt.Stage0Lib.*; +import static cbt.NailgunLauncher.*; + +class EarlyDependencies{ + + /** ClassLoader for stage1 */ + ClassLoader classLoader; + String[] classpathArray; + /** ClassLoader for zinc */ + ClassLoader zinc; + +${files.map(d => s""" String ${valName(d)}File;""").mkString("\n")} + + public EarlyDependencies( + String mavenCache, String mavenUrl, ClassLoaderCache2<ClassLoader> classLoaderCache, ClassLoader rootClassLoader + ) throws Exception { +${files.map(d => s""" ${valName(d)}File = mavenCache + "${d.basePath}.jar";""").mkString("\n")} + +${scalaDeps.map(d => s""" download(new URL(mavenUrl + "${d.basePath}.jar"), Paths.get(${valName(d)}File), "${d.jarSha1}");""").mkString("\n")} +${assignments.mkString("\n")} + + classLoader = scalaXml_${scalaXmlVersion.replace(".","_")}_; + classpathArray = scalaXml_${scalaXmlVersion.replace(".","_")}_ClasspathArray; + + zinc = zinc_${zincVersion.replace(".","_")}_; + } +} +""" + val file = nailgun ++ ("/" ++ "EarlyDependencies.java") + lib.write( file, code ) + println( Console.GREEN ++ "Wrote " ++ file.string ++ Console.RESET ) + } +} diff --git a/stage2/plugins/AdvancedFlags.scala b/stage2/plugins/AdvancedFlags.scala new file mode 100644 index 0000000..4ff701d --- /dev/null +++ b/stage2/plugins/AdvancedFlags.scala @@ -0,0 +1,10 @@ +package cbt +trait AdvancedScala extends BaseBuild{ + override def scalacOptions = super.scalacOptions ++ Seq( + "-language:postfixOps", + "-language:implicitConversions", + "-language:higherKinds", + "-language:existentials", + "-language:experimental.macros" + ) +} diff --git a/stage2/plugins/Dotty.scala b/stage2/plugins/Dotty.scala new file mode 100644 index 0000000..8671fb6 --- /dev/null +++ b/stage2/plugins/Dotty.scala @@ -0,0 +1,172 @@ +package cbt +import java.io.File +import java.net.URL +import java.nio.file.Files +import java.nio.file.attribute.FileTime + +trait Dotty extends BaseBuild{ + def dottyVersion: String = "0.1-20160926-ec28ea1-NIGHTLY" + def dottyOptions: Seq[String] = Seq() + override def scalaTarget: File = target ++ s"/dotty-$dottyVersion" + + private lazy val dottyLib = new DottyLib( + logger, context.cbtHasChanged, context.paths.mavenCache, + context.classLoaderCache, dottyVersion = dottyVersion + ) + + private object compileCache extends Cache[Option[File]] + override def compile: Option[File] = compileCache{ + dottyLib.compile( + needsUpdate || context.parentBuild.map(_.needsUpdate).getOrElse(false), + sourceFiles, compileTarget, compileStatusFile, compileClasspath, + dottyOptions + ) + } + + def doc: ExitCode = + dottyLib.doc( + sourceFiles, compileClasspath, docTarget, dottyOptions + ) + + override def repl = dottyLib.repl(context.args, classpath) + + override def dependencies = Resolver(mavenCentral).bind( + ScalaDependency( "org.scala-lang.modules", "scala-java8-compat", "0.8.0-RC7" ) + ) +} + +class DottyLib( + logger: Logger, + cbtHasChanged: Boolean, + mavenCache: File, + classLoaderCache: ClassLoaderCache, + dottyVersion: String +){ + val lib = new Lib(logger) + import lib._ + + private def Resolver(urls: URL*) = MavenResolver(cbtHasChanged, mavenCache, urls: _*) + private lazy val dottyDependency = Resolver(mavenCentral).bindOne( + MavenDependency("ch.epfl.lamp","dotty_2.11",dottyVersion) + ) + + def repl(args: Seq[String], classpath: ClassPath) = { + consoleOrFail("Use `cbt direct repl` instead") + lib.runMain( + "dotty.tools.dotc.repl.Main", + Seq( + "-bootclasspath", + dottyDependency.classpath.string, + "-classpath", + classpath.string + ) ++ args, + dottyDependency.classLoader(classLoaderCache) + ) + } + + def doc( + sourceFiles: Seq[File], + dependencyClasspath: ClassPath, + docTarget: File, + compileArgs: Seq[String] + ): ExitCode = { + if(sourceFiles.isEmpty){ + ExitCode.Success + } else { + docTarget.mkdirs + val args = Seq( + // FIXME: can we use compiler dependency here? + "-bootclasspath", dottyDependency.classpath.string, // FIXME: does this break for builds that don't have scalac dependencies? + "-classpath", dependencyClasspath.string, // FIXME: does this break for builds that don't have scalac dependencies? + "-d", docTarget.toString + ) ++ compileArgs ++ sourceFiles.map(_.toString) + logger.lib("creating docs for source files "+args.mkString(", ")) + val exitCode = redirectOutToErr{ + runMain( + "dotty.tools.dottydoc.api.java.Dottydoc", + args, + dottyDependency.classLoader(classLoaderCache), + fakeInstance = true // this is a hack as Dottydoc's main method is not static + ) + } + System.err.println("done") + exitCode + } + } + + def compile( + needsRecompile: Boolean, + files: Seq[File], + compileTarget: File, + statusFile: File, + classpath: ClassPath, + dottyOptions: Seq[String] + ): Option[File] = { + + if(classpath.files.isEmpty) + throw new Exception("Trying to compile with empty classpath. Source files: " ++ files.toString) + + if( files.isEmpty ){ + None + }else{ + if( needsRecompile ){ + val start = System.currentTimeMillis + + val _class = "dotty.tools.dotc.Main" + val dualArgs = + Seq( + "-d", compileTarget.toString + ) + val singleArgs = dottyOptions.map( "-S" ++ _ ) + + val code = + try{ + System.err.println("Compiling with Dotty to " ++ compileTarget.toString) + compileTarget.mkdirs + redirectOutToErr{ + lib.runMain( + _class, + dualArgs ++ singleArgs ++ Seq( + "-bootclasspath", dottyDependency.classpath.string, // let's put cp last. It so long + "-classpath", classpath.string // let's put cp last. It so long + ) ++ files.map(_.toString), + dottyDependency.classLoader(classLoaderCache) + ) + } + } catch { + case e: Exception => + System.err.println(red("Dotty crashed. See https://github.com/lampepfl/dotty/issues. To reproduce run:")) + System.out.println(s""" +java -cp \\ +${dottyDependency.classpath.strings.mkString(":\\\n")} \\ +\\ +${_class} \\ +\\ +${dualArgs.grouped(2).map(_.mkString(" ")).mkString(" \\\n")} \\ +\\ +${singleArgs.mkString(" \\\n")} \\ +\\ +-bootclasspath \\ +${dottyDependency.classpath.strings.mkString(":\\\n")} \\ +-classpath \\ +${classpath.strings.mkString(":\\\n")} \\ +\\ +${files.sorted.mkString(" \\\n")} +""" + ) + ExitCode.Failure + } + + if(code == ExitCode.Success){ + // write version and when last compilation started so we can trigger + // recompile if cbt version changed or newer source files are seen + write(statusFile, "")//cbtVersion.getBytes) + Files.setLastModifiedTime(statusFile.toPath, FileTime.fromMillis(start) ) + } else { + System.exit(code.integer) // FIXME: let's find a better solution for error handling. Maybe a monad after all. + } + } + Some( compileTarget ) + } + } +} diff --git a/stage2/plugins/GithubPom.scala b/stage2/plugins/GithubPom.scala new file mode 100644 index 0000000..8b11385 --- /dev/null +++ b/stage2/plugins/GithubPom.scala @@ -0,0 +1,11 @@ +package cbt +import java.net.URL +trait GithubPom extends Publish{ + def user: String + def githubProject = name + def githubUser = user + final def githubUserProject = githubUser ++ "/" ++ githubProject + override def url = new URL(s"http://github.com/$githubUserProject") + override def scmUrl = s"git@github.com:$githubUserProject.git" + override def scmConnection = s"scm:git:$scmUrl" +} diff --git a/stage2/plugins/ScalaParadise.scala b/stage2/plugins/ScalaParadise.scala new file mode 100644 index 0000000..28ee934 --- /dev/null +++ b/stage2/plugins/ScalaParadise.scala @@ -0,0 +1,29 @@ +package cbt +trait ScalaParadise extends BaseBuild{ + def scalaParadiseVersion = "2.1.0" + + private def scalaParadiseDependency = + Resolver( mavenCentral ).bindOne( + "org.scalamacros" % ("paradise_" ++ scalaVersion) % scalaParadiseVersion + ) + + override def dependencies = ( + super.dependencies // don't forget super.dependencies here + ++ ( + if(scalaVersion.startsWith("2.10.")) + Seq(scalaParadiseDependency) + else + Seq() + ) + ) + + override def scalacOptions = ( + super.scalacOptions + ++ ( + if(scalaVersion.startsWith("2.10.")) + Seq("-Xplugin:"++scalaParadiseDependency.jar.string) + else + Seq() + ) + ) +} diff --git a/stage2/plugins/readme.txt b/stage2/plugins/readme.txt new file mode 100644 index 0000000..d959c72 --- /dev/null +++ b/stage2/plugins/readme.txt @@ -0,0 +1,4 @@ +This directory is for built-in plugins which are always available. +This is nice for plugins, which themselves do not have any +compile-time dependencies except for CBT itself. +See <cbt home>/plugins/ for plugins, that require an explicit dependency in the BuildBuild. diff --git a/stage2/pom.scala b/stage2/pom.scala new file mode 100644 index 0000000..b521d51 --- /dev/null +++ b/stage2/pom.scala @@ -0,0 +1,6 @@ +package cbt +import java.net.URL +case class Organization( + name: String, + url: Option[URL] +) diff --git a/test/build/build.scala b/test/build/build.scala new file mode 100644 index 0000000..5a138fb --- /dev/null +++ b/test/build/build.scala @@ -0,0 +1,4 @@ +import cbt._ +class Build(val context: cbt.Context) extends BaseBuild{ + override def dependencies = super.dependencies :+ context.cbtDependency +} diff --git a/test/empty-build-file/Main.scala b/test/empty-build-file/Main.scala new file mode 100644 index 0000000..19d4beb --- /dev/null +++ b/test/empty-build-file/Main.scala @@ -0,0 +1,5 @@ +object Main{ + def main( args: Array[String] ): Unit = { + println( Console.GREEN ++ "Hello World" ++ Console.RESET ) + } +} diff --git a/test/empty-build-file/build/build.scala b/test/empty-build-file/build/build.scala new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/empty-build-file/build/build.scala diff --git a/test/empty-build/Main.scala b/test/empty-build/Main.scala new file mode 100644 index 0000000..19d4beb --- /dev/null +++ b/test/empty-build/Main.scala @@ -0,0 +1,5 @@ +object Main{ + def main( args: Array[String] ): Unit = { + println( Console.GREEN ++ "Hello World" ++ Console.RESET ) + } +} diff --git a/test/empty-build/build/build/dummy b/test/empty-build/build/build/dummy new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/empty-build/build/build/dummy diff --git a/test/forgot-extend/build/build.scala b/test/forgot-extend/build/build.scala new file mode 100644 index 0000000..9181a5d --- /dev/null +++ b/test/forgot-extend/build/build.scala @@ -0,0 +1,2 @@ +import cbt._ +class Build(val context: Context) diff --git a/test/library-test/Foo.scala b/test/library-test/Foo.scala new file mode 100644 index 0000000..75c0780 --- /dev/null +++ b/test/library-test/Foo.scala @@ -0,0 +1,4 @@ +package lib_test +object Foo{ + def bar = "Hello, Foo Bar" +} diff --git a/test/library-test/build/build.scala b/test/library-test/build/build.scala new file mode 100644 index 0000000..d07e58e --- /dev/null +++ b/test/library-test/build/build.scala @@ -0,0 +1,25 @@ +import cbt._ + +// cbt:https://github.com/cvogt/cbt.git#bf4ea112fe668fb7e2e95a2baca4989b16384783 +class Build(val context: Context) extends BaseBuild with PackageJars{ + def groupId = "cbt.test" + def defaultVersion = "0.1" + def name = "library-test" + + override def dependencies = + super.dependencies ++ // don't forget super.dependencies here for scala-library, etc. + Seq( + // source dependency + // DirectoryDependency( projectDirectory ++ "/subProject" ) + ) ++ + // pick resolvers explicitly for individual dependencies (and their transitive dependencies) + Resolver( mavenCentral, sonatypeReleases ).bind( + // CBT-style Scala dependencies + // ScalaDependency( "com.lihaoyi", "ammonite-ops", "0.5.5" ) + // MavenDependency( "com.lihaoyi", "ammonite-ops_2.11", "0.5.5" ) + + // SBT-style dependencies + // "com.lihaoyi" %% "ammonite-ops" % "0.5.5" + // "com.lihaoyi" % "ammonite-ops_2.11" % "0.5.5" + ) +} diff --git a/test/multi-build/build/build.scala b/test/multi-build/build/build.scala new file mode 100644 index 0000000..5576a3d --- /dev/null +++ b/test/multi-build/build/build.scala @@ -0,0 +1,7 @@ +import cbt._ +class Build(val context: Context) extends BaseBuild{ + override def dependencies = Seq( + DirectoryDependency(projectDirectory++"/sub1"), + DirectoryDependency(projectDirectory++"/sub2") + ) ++ super.dependencies +} diff --git a/test/multi-build/code.scala b/test/multi-build/code.scala new file mode 100644 index 0000000..3fe85ad --- /dev/null +++ b/test/multi-build/code.scala @@ -0,0 +1,5 @@ +object Main extends App{ + println("root here") + println(Foo(5)) + println(Bar("test")) +} diff --git a/test/multi-build/sub1/code.scala b/test/multi-build/sub1/code.scala new file mode 100644 index 0000000..b2d5deb --- /dev/null +++ b/test/multi-build/sub1/code.scala @@ -0,0 +1,4 @@ +case class Foo(i: Int) +object Main extends App{ + println("sub1 here") +}
\ No newline at end of file diff --git a/test/multi-build/sub2/code.scala b/test/multi-build/sub2/code.scala new file mode 100644 index 0000000..1ec6ebf --- /dev/null +++ b/test/multi-build/sub2/code.scala @@ -0,0 +1,4 @@ +case class Bar(s: String) +object Main extends App{ + println("sub1 here") +} diff --git a/test/no-build-file/Main.scala b/test/no-build-file/Main.scala new file mode 100644 index 0000000..19d4beb --- /dev/null +++ b/test/no-build-file/Main.scala @@ -0,0 +1,5 @@ +object Main{ + def main( args: Array[String] ): Unit = { + println( Console.GREEN ++ "Hello World" ++ Console.RESET ) + } +} diff --git a/test/no-build-file/build/foo.scala b/test/no-build-file/build/foo.scala new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/no-build-file/build/foo.scala diff --git a/test/nothing/placeholder b/test/nothing/placeholder new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/nothing/placeholder diff --git a/test/simple-fixed-cbt/Main.scala b/test/simple-fixed-cbt/Main.scala new file mode 100644 index 0000000..75f9349 --- /dev/null +++ b/test/simple-fixed-cbt/Main.scala @@ -0,0 +1,6 @@ +import lib_test.Foo +import org.eclipse.jgit.lib.Ref +import com.spotify.missinglink.ArtifactLoader +object Main extends App{ + println(Foo.bar) +} diff --git a/test/simple-fixed-cbt/build/build.scala b/test/simple-fixed-cbt/build/build.scala new file mode 100644 index 0000000..1d0640d --- /dev/null +++ b/test/simple-fixed-cbt/build/build.scala @@ -0,0 +1,14 @@ +import cbt._ + +// cbt:https://github.com/cvogt/cbt.git#bf4ea112fe668fb7e2e95a2baca4989b16384783 +class Build(val context: cbt.Context) extends PackageJars{ + override def dependencies = super.dependencies ++ Seq( + DirectoryDependency( context.cbtHome ++ "/test/library-test" ) + ) ++ Resolver( mavenCentral ).bind( + MavenDependency("org.eclipse.jgit", "org.eclipse.jgit", "4.2.0.201601211800-r"), + MavenDependency("com.spotify", "missinglink-core", "0.1.1") + ) + def groupId: String = "cbt.test" + def defaultVersion: String = "0.1" + def name: String = "simple-fixed-cbt" +}
\ No newline at end of file diff --git a/test/simple-fixed/Main.scala b/test/simple-fixed/Main.scala new file mode 100644 index 0000000..75f9349 --- /dev/null +++ b/test/simple-fixed/Main.scala @@ -0,0 +1,6 @@ +import lib_test.Foo +import org.eclipse.jgit.lib.Ref +import com.spotify.missinglink.ArtifactLoader +object Main extends App{ + println(Foo.bar) +} diff --git a/test/simple-fixed/build/build.scala b/test/simple-fixed/build/build.scala new file mode 100644 index 0000000..a2bd584 --- /dev/null +++ b/test/simple-fixed/build/build.scala @@ -0,0 +1,29 @@ +import cbt._ + +class Build(context: cbt.Context) extends BasicBuild(context){ + override def dependencies = ( + super.dependencies + ++ + Seq( + GitDependency("https://github.com/cvogt/cbt.git", "908e05e296974fe67d8aaf9f094d97ff986905af", Some("test/library-test")) + ) + ++ + Resolver(mavenCentral).bind( + ScalaDependency("com.typesafe.play", "play-json", "2.4.4"), + MavenDependency("joda-time", "joda-time", "2.9.2"), + // the below tests pom inheritance with dependencyManagement and variable substitution for pom properties + MavenDependency("org.eclipse.jgit", "org.eclipse.jgit", "4.2.0.201601211800-r"), + // the below tests pom inheritance with variable substitution for pom xml tag contents + MavenDependency("com.spotify", "missinglink-core", "0.1.1") + ) + ++ + Resolver( + mavenCentral, + bintray("tpolecat"), + sonatypeSnapshots + ).bind( + "org.cvogt" %% "play-json-extensions" % "0.8.0", + "ai.x" %% "lens" % "1.0.0" + ) + ) +} diff --git a/test/simple/Main.scala b/test/simple/Main.scala new file mode 100644 index 0000000..75f9349 --- /dev/null +++ b/test/simple/Main.scala @@ -0,0 +1,6 @@ +import lib_test.Foo +import org.eclipse.jgit.lib.Ref +import com.spotify.missinglink.ArtifactLoader +object Main extends App{ + println(Foo.bar) +} diff --git a/test/simple/build/build.scala b/test/simple/build/build.scala new file mode 100644 index 0000000..586daca --- /dev/null +++ b/test/simple/build/build.scala @@ -0,0 +1,43 @@ +import cbt._ + +class Build(val context: cbt.Context) extends BaseBuild{ + override def dependencies = ( + super.dependencies + ++ + Seq( + GitDependency("https://github.com/cvogt/cbt.git", "908e05e296974fe67d8aaf9f094d97ff986905af", Some("test/library-test")) + ) ++ + // FIXME: make the below less verbose + Resolver( mavenCentral ).bind( + ScalaDependency("com.typesafe.play", "play-json", "2.4.4"), + MavenDependency("joda-time", "joda-time", "2.9.2"), + // the below tests pom inheritance with dependencyManagement and variable substitution for pom properties + MavenDependency("org.eclipse.jgit", "org.eclipse.jgit", "4.2.0.201601211800-r"), + // the below tests pom inheritance with variable substitution for pom xml tag contents + MavenDependency("com.spotify", "missinglink-core", "0.1.1"), + // the below tests pom inheritance with variable substitution being parts of strings + MavenDependency("cc.factorie","factorie_2.11","1.2") + // the dependency below uses a maven version range. Currently not supported. + // TODO: put in a proper error message for version range not supported + //MavenDependency("com.github.nikita-volkov", "sext", "0.2.4") + // currently breaks with can't find https://repo1.maven.org/maven2/org/apache/avro/avro-mapred/1.7.7/avro-mapred-1.7.7-hadoop2.pom.sha1 + // org.apache.spark:spark-sql_2.11:1.6.1 + // currently fails, let's see if because of a bug + // io.spray:spray-http:1.3.3 + ) ++ + Resolver( new java.net.URL("http://maven.spikemark.net/roundeights") ).bind( + // Check that lower case checksums work + ScalaDependency("com.roundeights","hasher","1.2.0") + ) ++ + Resolver( + mavenCentral, + bintray("tpolecat"), + sonatypeSnapshots + ).bind( + "org.cvogt" %% "play-json-extensions" % "0.8.0", + "ai.x" %% "lens" % "1.0.0" + ) + ) + + def printArgs = context.args.mkString(" ") +} diff --git a/test/test.scala b/test/test.scala new file mode 100644 index 0000000..5b4a4af --- /dev/null +++ b/test/test.scala @@ -0,0 +1,262 @@ +package cbt +package test +import java.util.concurrent.ConcurrentHashMap +import java.io.File +import java.nio.file._ +import java.net.URL + +// micro framework +object Main{ + def main(_args: Array[String]): Unit = { + val start = System.currentTimeMillis + val args = new Stage1ArgsParser(_args.toVector) + implicit val logger: Logger = new Logger(args.enabledLoggers, System.currentTimeMillis) + val lib = new Lib(logger) + val cbtHome = new File(System.getenv("CBT_HOME")) + + var successes = 0 + var failures = 0 + def assertException[T:scala.reflect.ClassTag](msg: String = "")(code: => Unit)(implicit logger: Logger) = { + try{ + code + assert(false, msg) + }catch{ case _:AssertionError => } + } + def assert(condition: Boolean, msg: String = "")(implicit logger: Logger) = { + scala.util.Try{ + Predef.assert(condition, "["++msg++"]") + }.map{ _ => + print(".") + successes += 1 + }.recover{ + case e: AssertionError => + println("FAILED") + e.printStackTrace + failures += 1 + }.get + } + + def runCbt(path: String, _args: Seq[String])(implicit logger: Logger): Result = { + import java.io._ + val allArgs: Seq[String] = ((cbtHome.string ++ "/cbt") +: "direct" +: (_args ++ args.propsRaw)) + logger.test(allArgs.toString) + val pb = new ProcessBuilder( allArgs :_* ) + pb.directory(cbtHome ++ ("/test/" ++ path)) + val p = pb.start + val berr = new BufferedReader(new InputStreamReader(p.getErrorStream)); + val bout = new BufferedReader(new InputStreamReader(p.getInputStream)); + import collection.JavaConversions._ + val err = Stream.continually(berr.readLine()).takeWhile(_ != null).mkString("\n") + val out = Stream.continually(bout.readLine()).takeWhile(_ != null).mkString("\n") + p.waitFor + Result(p.exitValue == 0, out, err) + } + case class Result(exit0: Boolean, out: String, err: String) + def assertSuccess(res: Result, msg: => String)(implicit logger: Logger) = { + assert(res.exit0, msg ++ res.toString) + } + + // tests + def usage(path: String)(implicit logger: Logger) = { + val usageString = "Methods provided by CBT" + val res = runCbt(path, Seq()) + logger.test(res.toString) + val debugToken = "usage " ++ path ++ " " + assertSuccess(res,debugToken) + assert(res.out == "", debugToken ++ res.toString) + assert(res.err contains usageString, debugToken ++ res.toString) + } + def compile(path: String)(implicit logger: Logger) = task("compile", path) + def task(name: String, path: String)(implicit logger: Logger) = { + val res = runCbt(path, Seq(name)) + val debugToken = name ++ " " ++ path ++ " " + assertSuccess(res,debugToken) + res + // assert(res.err == "", res.err) // FIXME: enable this + } + + def clean(path: String)(implicit logger: Logger) = { + val res = runCbt(path, Seq("clean", "dry-run", "force")) + val debugToken = "\n"++lib.red("Deleting") ++ " " ++ (cbtHome ++("/test/"++path++"/target")).toPath.toAbsolutePath.toString++"\n" + val debugToken2 = "\n"++lib.red("Deleting") ++ " " ++ (cbtHome ++("/test/"++path)).toPath.toAbsolutePath.toString++"\n" + assertSuccess(res,debugToken) + assert(res.out == "", debugToken ++ " " ++ res.toString) + assert(res.err.contains(debugToken), debugToken ++ " " ++ res.toString) + assert( + !res.err.contains(debugToken2), + "Tried to delete too much: " ++ debugToken2 ++ " " ++ res.toString + ) + res.err.split("\n").filter(_.startsWith(lib.red("Deleting"))).foreach{ line => + assert( + line.size >= debugToken2.trim.size, + "Tried to delete too much: " ++ line ++" debugToken2: " ++ debugToken2 + ) + } + } + + logger.test( "Running tests " ++ _args.toList.toString ) + + val cache = cbtHome ++ "/cache" + val mavenCache = cache ++ "/maven" + val cbtHasChanged = true + def Resolver(urls: URL*) = MavenResolver(cbtHasChanged, mavenCache, urls: _*) + + { + val noContext = ContextImplementation( + cbtHome ++ "/test/nothing", + cbtHome, + Array(), + Array(), + start, + cbtHasChanged, + null, + null, + new ConcurrentHashMap[String,AnyRef], + new ConcurrentHashMap[AnyRef,ClassLoader], + cache, + cbtHome, + cbtHome, + cbtHome ++ "/compatibilityTarget", + null + ) + + val b = new BasicBuild(noContext){ + override def dependencies = + Resolver(mavenCentral).bind( + MavenDependency("net.incongru.watchservice","barbary-watchservice","1.0"), + MavenDependency("net.incongru.watchservice","barbary-watchservice","1.0") + ) + } + val cp = b.classpath + assert(cp.strings.distinct == cp.strings, "duplicates in classpath: " ++ cp.string) + } + + // test that messed up artifacts crash with an assertion (which should tell the user what's up) + assertException[AssertionError](){ + Resolver(mavenCentral).bindOne( MavenDependency("com.jcraft", "jsch", " 0.1.53") ).classpath + } + assertException[AssertionError](){ + Resolver(mavenCentral).bindOne( MavenDependency("com.jcraft", null, "0.1.53") ).classpath + } + assertException[AssertionError](){ + Resolver(mavenCentral).bindOne( MavenDependency("com.jcraft", "", " 0.1.53") ).classpath + } + assertException[AssertionError](){ + Resolver(mavenCentral).bindOne( MavenDependency("com.jcraft%", "jsch", " 0.1.53") ).classpath + } + assertException[AssertionError](){ + Resolver(mavenCentral).bindOne( MavenDependency("", "jsch", " 0.1.53") ).classpath + } + + ( + ( + if(System.getenv("CIRCLECI") == null){ + // tenporarily disable on circleci as it seems to have trouble reliably + // downloading from bintray + Dependencies( + Resolver( bintray("tpolecat") ).bind( + lib.ScalaDependency("org.tpolecat","tut-core","0.4.2", scalaMajorVersion="2.11") + ) + ).classpath.strings + } else Nil + ) ++ + Dependencies( + Resolver( sonatypeReleases ).bind( + MavenDependency("org.cvogt","scala-extensions_2.11","0.5.1") + ) + ).classpath.strings + ++ + Dependencies( + Resolver( mavenCentral ).bind( + MavenDependency("ai.x","lens_2.11","1.0.0") + ) + ).classpath.strings + ).foreach{ + path => assert(new File(path).exists, path) + } + + usage("nothing") + compile("nothing") + //clean("nothing") + usage("multi-build") + compile("multi-build") + clean("multi-build") + usage("simple") + compile("simple") + clean("simple") + usage("simple-fixed") + compile("simple-fixed") + + compile("../plugins/sbt_layout") + compile("../plugins/scalafmt") + compile("../plugins/scalajs") + compile("../plugins/scalariform") + compile("../plugins/scalatest") + compile("../plugins/wartremover") + compile("../plugins/uber-jar") + compile("../examples/scalafmt-example") + compile("../examples/scalariform-example") + compile("../examples/scalatest-example") + compile("../examples/scalajs-react-example/js") + compile("../examples/scalajs-react-example/jvm") + compile("../examples/multi-project-example") + if(sys.props("java.version").startsWith("1.7")){ + System.err.println("\nskipping dotty tests on Java 7") + } else { + compile("../examples/dotty-example") + task("run","../examples/dotty-example") + task("doc","../examples/dotty-example") + } + task("fastOptJS","../examples/scalajs-react-example/js") + task("fullOptJS","../examples/scalajs-react-example/js") + compile("../examples/uber-jar-example") + + { + val res = task("docJar","simple-fixed-cbt") + assert( res.out endsWith "simple-fixed-cbt_2.11-0.1-javadoc.jar", res.out ) + assert( res.err contains "model contains", res.err ) + assert( res.err endsWith "documentable templates", res.err ) + } + + { + val res = runCbt("simple", Seq("printArgs","1","2","3")) + assert(res.exit0) + assert(res.out == "1 2 3", res.out) + } + + { + val res = runCbt("../examples/build-info-example", Seq("run")) + assert(res.exit0) + assert(res.out contains "version: 0.1", res.out) + } + + { + val res = runCbt("forgot-extend", Seq("run")) + assert(!res.exit0) + assert(res.err contains "Build cannot be cast to cbt.BuildInterface", res.err) + } + + { + val res = runCbt("no-build-file", Seq("run")) + assert(!res.exit0) + assert(res.err contains "No file build.scala (lower case) found in", res.err) + } + + { + val res = runCbt("empty-build-file", Seq("run")) + assert(!res.exit0) + assert(res.err contains "You need to define a class Build in build.scala in", res.err) + } + + { + val res = runCbt("../examples/wartremover-example", Seq("compile")) + assert(!res.exit0) + assert(res.err.contains("var is disabled"), res.out) + assert(res.err.contains("null is disabled"), res.out) + } + + System.err.println(" DONE!") + System.err.println( successes.toString ++ " succeeded, "++ failures.toString ++ " failed" ) + if(failures > 0) System.exit(1) else System.exit(0) + } +} diff --git a/tools/gui/build/build.scala b/tools/gui/build/build.scala new file mode 100644 index 0000000..5312e55 --- /dev/null +++ b/tools/gui/build/build.scala @@ -0,0 +1,12 @@ +import cbt._ + +class Build(val context: Context) extends BaseBuild { + + override def dependencies = { + super.dependencies ++ Resolver(mavenCentral).bind( + MavenDependency("org.eclipse.jetty", "jetty-server", "9.3.12.v20160915"), + MavenDependency("org.scalaj", "scalaj-http_" + constants.scalaMajorVersion, "2.3.0") + ) + } + +} diff --git a/tools/gui/resources/template-project/build/build.scala b/tools/gui/resources/template-project/build/build.scala new file mode 100644 index 0000000..3ec5db9 --- /dev/null +++ b/tools/gui/resources/template-project/build/build.scala @@ -0,0 +1,5 @@ +import cbt._ + +class Build(val context: Context) extends BaseBuild##with## { + +##projectName####dependencies##} diff --git a/tools/gui/resources/template-project/build/build/build.scala b/tools/gui/resources/template-project/build/build/build.scala new file mode 100644 index 0000000..1a59657 --- /dev/null +++ b/tools/gui/resources/template-project/build/build/build.scala @@ -0,0 +1,7 @@ +import cbt._ + +class Build(val context: Context) extends BuildBuild { + + override def dependencies = super.dependencies##plus## + +} diff --git a/tools/gui/resources/template-project/src/main/scala/Main.scala b/tools/gui/resources/template-project/src/main/scala/Main.scala new file mode 100644 index 0000000..48f8174 --- /dev/null +++ b/tools/gui/resources/template-project/src/main/scala/Main.scala @@ -0,0 +1,5 @@ +##package##object Main { + def main(args: Array[String]): Unit = { + println("Hello World") + } +} diff --git a/tools/gui/resources/web/favicon.ico b/tools/gui/resources/web/favicon.ico Binary files differnew file mode 100644 index 0000000..2ec8115 --- /dev/null +++ b/tools/gui/resources/web/favicon.ico diff --git a/tools/gui/resources/web/index.html b/tools/gui/resources/web/index.html new file mode 100644 index 0000000..9ad45a5 --- /dev/null +++ b/tools/gui/resources/web/index.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="author" content="tim-zh"> + <title>CBT</title> + <link rel="stylesheet" href="styles.css"> +</head> +<body> +<div id="popup"> + <div onclick="hidePopup()">×</div> + <table id="popup-table"></table> +</div> +<div class="container"> + + <h1>CBT bootstrap</h1> + + <div id="cwd" class="entry"><span>current dir:</span> </div> + <div class="entry"><input type="text" id="name" placeholder="project name"></div> + <div class="entry"><input type="text" id="package" placeholder="default package"></div> + <hr> + + <div class="entry"><input type="text" id="query" placeholder="add maven dependency" onkeyup="handleSearchInput(event)"></div> + <button class="small-btn" onclick="search()">search</button> + <div id="dependencies"></div> + <hr> + + <input type="checkbox" id="readme-flag"><label for="readme-flag">readme.md</label> + <input type="checkbox" id="dotty-flag"><label for="dotty-flag">dotty</label> + <input type="checkbox" id="uberJar-flag"><label for="uberJar-flag">uber jar</label> + <input type="checkbox" id="wartremover-flag"><label for="wartremover-flag">wartremover</label> + <hr> + + <button id="create-project" onclick="createProject()">create project</button> + +</div> +<script src="jquery-3.1.1.min.js"></script> +<script src="main.js"></script> +</body> +</html> diff --git a/tools/gui/resources/web/jquery-3.1.1.min.js b/tools/gui/resources/web/jquery-3.1.1.min.js new file mode 100644 index 0000000..4c5be4c --- /dev/null +++ b/tools/gui/resources/web/jquery-3.1.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=r.isArray(d)))?(e?(e=!1,f=c&&r.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===r.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||"[object Object]"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n))},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if("string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=!!a&&"length"in a&&a.length,c=r.type(a);return"function"!==c&&!r.isWindow(a)&&("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d="";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e);return!1}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}return!1}}function ua(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,M,e),g(f,c,N,e)):(f++,j.call(a,g(f,c,M,e),g(f,c,N,e),g(f,c,M,c.notifyWith))):(d!==M&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), +a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},T=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function U(){this.expando=r.expando+U.uid++}U.uid=1,U.prototype={cache:function(a){var b=a[this.expando];return b||(b={},T(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){r.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(K)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var V=new U,W=new U,X=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Y=/[A-Z]/g;function Z(a){return"true"===a||"false"!==a&&("null"===a?null:a===+a+""?+a:X.test(a)?JSON.parse(a):a)}function $(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Y,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c=Z(c)}catch(e){}W.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return W.hasData(a)||V.hasData(a)},data:function(a,b,c){return W.access(a,b,c)},removeData:function(a,b){W.remove(a,b)},_data:function(a,b,c){return V.access(a,b,c)},_removeData:function(a,b){V.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=W.get(f),1===f.nodeType&&!V.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),$(f,d,e[d])));V.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){W.set(this,a)}):S(this,function(b){var c;if(f&&void 0===b){if(c=W.get(f,a),void 0!==c)return c;if(c=$(f,a),void 0!==c)return c}else this.each(function(){W.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=V.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var _=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,aa=new RegExp("^(?:([+-])=|)("+_+")([a-z%]*)$","i"),ba=["Top","Right","Bottom","Left"],ca=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},da=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function ea(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&aa.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var fa={};function ga(a){var b,c=a.ownerDocument,d=a.nodeName,e=fa[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&&(e="block"),fa[d]=e,e)}function ha(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=V.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&ca(d)&&(e[f]=ga(d))):"none"!==c&&(e[f]="none",V.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ha(this,!0)},hide:function(){return ha(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){ca(this)?r(this).show():r(this).hide()})}});var ia=/^(?:checkbox|radio)$/i,ja=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c<d;c++)V.set(a[c],"globalEval",!b||V.get(b[c],"globalEval"))}var oa=/<|&#?\w+;/;function pa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(oa.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ja.exec(f)||["",""])[1].toLowerCase(),i=la[h]||la._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!("click"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+" ",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==wa()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===wa()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&r.nodeName(this,"input"))return this.click(),!1},_default:function(a){return r.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ua:va,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:va,isPropagationStopped:va,isImmediatePropagationStopped:va,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ua,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ua,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ua,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&ra.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&sa.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return xa(this,a,b,c,d)},one:function(a,b,c,d){return xa(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=va),this.each(function(){r.event.remove(this,a,c,b)})}});var ya=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/<script|<style|<link/i,Aa=/checked\s*(?:[^=]|=\s*.checked.)/i,Ba=/^true\/(.*)/,Ca=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}W.hasData(a)&&(h=W.access(a),i=r.extend({},h),W.set(b,i))}}function Ha(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ia.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ia(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,ma(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Fa),l=0;l<i;l++)j=h[l],ka.test(j.type||"")&&!V.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Ca,""),k))}return a}function Ja(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(ma(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&na(ma(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(ya,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);if(b)if(c)for(f=f||ma(a),g=g||ma(h),d=0,e=f.length;d<e;d++)Ga(f[d],g[d]);else Ga(a,h);return g=ma(h,"script"),g.length>0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(ma(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ia(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(ma(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var Ka=/^margin/,La=new RegExp("^("+_+")(?!px)[a-z%]+$","i"),Ma=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",qa.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,qa.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Na(a,b,c){var d,e,f,g,h=a.style;return c=c||Ma(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&La.test(g)&&Ka.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Oa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Pa=/^(none|table(?!-c[ea]).+)/,Qa={position:"absolute",visibility:"hidden",display:"block"},Ra={letterSpacing:"0",fontWeight:"400"},Sa=["Webkit","Moz","ms"],Ta=d.createElement("div").style;function Ua(a){if(a in Ta)return a;var b=a[0].toUpperCase()+a.slice(1),c=Sa.length;while(c--)if(a=Sa[c]+b,a in Ta)return a}function Va(a,b,c){var d=aa.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Wa(a,b,c,d,e){var f,g=0;for(f=c===(d?"border":"content")?4:"width"===b?1:0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+ba[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+ba[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+ba[f]+"Width",!0,e))):(g+=r.css(a,"padding"+ba[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+ba[f]+"Width",!0,e)));return g}function Xa(a,b,c){var d,e=!0,f=Ma(a),g="border-box"===r.css(a,"boxSizing",!1,f);if(a.getClientRects().length&&(d=a.getBoundingClientRect()[b]),d<=0||null==d){if(d=Na(a,b,f),(d<0||null==d)&&(d=a.style[b]),La.test(d))return d;e=g&&(o.boxSizingReliable()||d===a.style[b]),d=parseFloat(d)||0}return d+Wa(a,b,c||(g?"border":"content"),e,f)+"px"}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Na(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=a.style;return b=r.cssProps[h]||(r.cssProps[h]=Ua(h)||h),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=aa.exec(c))&&e[1]&&(c=ea(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b);return b=r.cssProps[h]||(r.cssProps[h]=Ua(h)||h),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Na(a,b,d)),"normal"===e&&b in Ra&&(e=Ra[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Pa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?Xa(a,b,d):da(a,Qa,function(){return Xa(a,b,d)})},set:function(a,c,d){var e,f=d&&Ma(a),g=d&&Wa(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=aa.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Va(a,c,g)}}}),r.cssHooks.marginLeft=Oa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Na(a,"marginLeft"))||a.getBoundingClientRect().left-da(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+ba[d]+b]=f[d]||f[d-2]||f[0];return e}},Ka.test(a)||(r.cssHooks[a+b].set=Va)}),r.fn.extend({css:function(a,b){return S(this,function(a,b,c){var d,e,f={},g=0;if(r.isArray(b)){for(d=Ma(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function fb(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&ca(a),q=V.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],_a.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=V.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ha([a],!0),j=a.style.display||j,k=r.css(a,"display"),ha([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=V.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ha([a],!0),m.done(function(){p||ha([a]),V.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=eb(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function gb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],r.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function hb(a,b,c){var d,e,f=0,g=hb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Za||cb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:Za||cb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(gb(k,j.opts.specialEasing);f<g;f++)if(d=hb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,eb,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}r.Animation=r.extend(hb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return ea(c.elem,a,aa.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(K);for(var c,d=0,e=a.length;d<e;d++)c=a[d],hb.tweeners[c]=hb.tweeners[c]||[],hb.tweeners[c].unshift(b)},prefilters:[fb],prefilter:function(a,b){b?hb.prefilters.unshift(a):hb.prefilters.push(a)}}),r.speed=function(a,b,c){var e=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off||d.hidden?e.duration=0:"number"!=typeof e.duration&&(e.duration in r.fx.speeds?e.duration=r.fx.speeds[e.duration]:e.duration=r.fx.speeds._default),null!=e.queue&&e.queue!==!0||(e.queue="fx"),e.old=e.complete,e.complete=function(){r.isFunction(e.old)&&e.old.call(this),e.queue&&r.dequeue(this,e.queue)},e},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(ca).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=hb(this,r.extend({},a),f);(e||V.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=V.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&ab.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=V.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(db(b,!0),a,d,e)}}),r.each({slideDown:db("show"),slideUp:db("hide"),slideToggle:db("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(Za=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),Za=void 0},r.fx.timer=function(a){r.timers.push(a),a()?r.fx.start():r.timers.pop()},r.fx.interval=13,r.fx.start=function(){$a||($a=a.requestAnimationFrame?a.requestAnimationFrame(bb):a.setInterval(r.fx.tick,r.fx.interval))},r.fx.stop=function(){a.cancelAnimationFrame?a.cancelAnimationFrame($a):a.clearInterval($a),$a=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var ib,jb=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return S(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), +void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!r.nodeName(c.parentNode,"optgroup"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Qb=[],Rb=/(=)\?(?=&|$)|\?\?/;r.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Qb.pop()||r.expando+"_"+rb++;return this[a]=!0,a}}),r.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Rb.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Rb.test(b.data)&&"data");if(h||"jsonp"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Rb,"$1"+e):b.jsonp!==!1&&(b.url+=(sb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||r.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Qb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),"script"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=B.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=pa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=mb(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&r.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?r("<div>").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length};function Sb(a){return r.isWindow(a)?a:9===a.nodeType&&a.defaultView}r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),d.width||d.height?(e=f.ownerDocument,c=Sb(e),b=e.documentElement,{top:d.top+c.pageYOffset-b.clientTop,left:d.left+c.pageXOffset-b.clientLeft}):d):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),r.nodeName(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||qa})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return S(this,function(a,d,e){var f=Sb(a);return void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Oa(o.pixelPosition,function(a,c){if(c)return c=Na(a,b),La.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return S(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.parseJSON=JSON.parse,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var Tb=a.jQuery,Ub=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Ub),b&&a.jQuery===r&&(a.jQuery=Tb),r},b||(a.jQuery=a.$=r),r}); diff --git a/tools/gui/resources/web/main.js b/tools/gui/resources/web/main.js new file mode 100644 index 0000000..b4fe38b --- /dev/null +++ b/tools/gui/resources/web/main.js @@ -0,0 +1,129 @@ +Notification.requestPermission(); + +$.get("/api/cwd").done(cwd => { + $("#cwd").append(cwd[0]); +}).fail(e => notifyFail(e)); + +document.body.onkeydown = function (event) { + if (event.keyCode == 27) + hidePopup(); +}; + +$("#create-project")[0].disabled = false; + +let dependencies = []; + +function getFlags() { + return "readme/" + $("#readme-flag")[0].checked + + " dotty/" + $("#dotty-flag")[0].checked + + " uberJar/" + $("#uberJar-flag")[0].checked + + " wartremover/" + $("#wartremover-flag")[0].checked; +} + +function createProject() { + let button = $("#create-project")[0]; + let buttonText = button.innerHTML; + button.innerHTML = "..."; + button.blur(); + button.disabled = true; + $.post("/api/project", { + name: $("#name").val(), + pack: $("#package").val(), + dependencies: dependencies.length == 0 ? "" : + dependencies.map(l => l.group + "/" + l.artifact + "/" + l.version).reduce((a, b) => a + " " + b), + flags: getFlags() + }).done(() => { + notify("Done."); + }).fail(e => + notifyFail(e) + ).always(() => { + button.innerHTML = buttonText; + button.disabled = false; + }); +} + +function handleSearchInput(event) { + if (event.keyCode == 13) + search(); +} +function search() { + let query = $("#query").val(); + if (query) { + $.get("/api/dependency", { query: query }).done(data => { + let entries = data.response.docs.map(x => ({ + group: x.g, + artifact: x.a + })); + showPopup(makeRowsFrom(entries, selectDependency)); + }).fail(e => notifyFail(e)); + document.activeElement.blur(); + } +} +function selectDependency(selected) { + $.get("/api/dependency/version", { group: selected.group, artifact: selected.artifact }).done(data => { + let versions = data.response.docs.map(x => ({ version: x.v })); + $("#popup-table").html(makeRowsFrom(versions, function (version) { + selectDependencyVersion(selected.group, selected.artifact, version.version); + })); + }).fail(e => notifyFail(e)); +} +function selectDependencyVersion(group, artifact, version) { + let dependency = { group: group, artifact: artifact, version: version }; + dependencies.push(dependency); + let scalaName = artifact.match(/^(.+)_\d+\.\d+$/); + let name = scalaName && scalaName[1] ? scalaName[1] : artifact; + var depDiv = $("<div class='entry removable'>" + name + " " + version + "</div>"); + depDiv.click(function () { + removeDependency(dependency, depDiv); + }); + $("#dependencies").append(depDiv); + hidePopup(); +} +function removeDependency(d, div) { + dependencies.splice(dependencies.indexOf(d), 1); + div.remove(); +} + +function showPopup(contents) { + $("#popup").show(); + $("#popup-table").html(contents); +} +function hidePopup() { + $("#popup").hide(); + $("#popup-table").html(""); +} + +function makeRowsFrom(results, rowAction) { + if (results.length == 0) { + return []; + } else { + let rows = []; + let row = $("<tr></tr>"); + rows.push(row); + let fields = []; + for (let field in results[0]) + if (results[0].hasOwnProperty(field)) { + fields.push(field); + $("<td>" + field + "</td>").appendTo(row); + } + results.forEach(result => { + let row = $("<tr></tr>"); + row.click(function () { + rowAction(result); + }); + rows.push(row); + fields.forEach(field => $("<td>" + result[field] + "</td>").appendTo(row)); + }); + return rows; + } +} + +function notify(text, title) { + new Notification(title || "", { body: text }); +} + +function notifyFail(e) { + let head = e.status == 0 ? "Error" : e.status + " " + e.statusText; + let body = e.status == 0 ? "No response from UI server." : e.responseText; + notify(body, head); +} diff --git a/tools/gui/resources/web/styles.css b/tools/gui/resources/web/styles.css new file mode 100644 index 0000000..6aa45e9 --- /dev/null +++ b/tools/gui/resources/web/styles.css @@ -0,0 +1,196 @@ +* { + outline: none; +} + +body { + font-family: 'proxima nova', sans-serif; + font-size: 2em; + background: #002B36; + color: #fff; +} + +::-moz-selection { + background-color: #fff; + color: #073642; +} + +::selection { + background-color: #fff; + color: #073642; +} + +h1 { + font-weight: normal; + margin: 8%; +} + +hr { + border: none; + margin: 0.8em 0; + border-bottom: transparent solid 1px; +} + +button, .small-btn { + background: #dc322f; + color: #fff; + font-size: 1em; + padding: 0.2em; + border: transparent solid 0.1em; + border-radius: 1em; + text-transform: uppercase; + cursor: pointer; + transition: all 0.2s; +} + +.small-btn { + font-size: 0.8em; + padding: 0 1em; +} + +button:hover { + background: #073642; + border: #fff solid 0.1em; +} + +button:active { + background: #dc322f; +} + +button:disabled { + background: transparent; + cursor: default; +} + +input[type="text"] { + background: #073642; + color: #fff; + border: none; + width: 33%; + font-size: 1em; +} + +input[type="checkbox"] { + display: none; +} + +input[type="checkbox"] + label { + position: relative; + padding-left: 1.6em; + cursor: pointer; + display: inline-block; +} + +input[type="checkbox"] + label:before, input[type="checkbox"] + label:after { + content: ''; + margin-top: 0.25em; + border-radius: 1em; + position: absolute; + left: 0; + height: 0.8em; + transition: all .2s; +} + +input[type="checkbox"] + label:before { + width: 1.4em; + background: #073642; +} + +input[type="checkbox"] + label:after { + width: 0.8em; + background: #fff; + border: #073642 solid 0.15em; + box-sizing: border-box; +} + +input[type="checkbox"] + label:hover:before, input[type="checkbox"]:checked + label:before { + background-color: #dc322f; +} + +input[type="checkbox"] + label:hover:after { + border-color: #dc322f; +} + +input[type="checkbox"]:checked + label:after { + left: 0.6em; + border-color: #dc322f; +} + +.container { + margin: 0 8%; + text-align: center; +} + +button, .entry { + margin-bottom: 0.5em; +} + +.entry span:first-child { + color: #27b3d9; +} + +.removable:hover { + text-decoration: line-through; + cursor: pointer; +} + +#popup { + position: fixed; + left: 0; + right: 0; + margin: 0 auto; + background: #073642; + width: 84%; + max-height: 80%; + overflow-y: scroll; + text-align: center; + display: none; + z-index: 2; +} + +#popup div:first-child { + position: fixed; + padding: 0 0.2em; + text-align: right; + cursor: pointer; + transition: all 0.2s; +} + +#popup div:first-child:hover { + color: #dc322f; +} + +#popup table { + margin: 2%; + width: 96%; + border-collapse: collapse; +} + +#popup td { + border-right: #fff solid 1px; +} + +#popup td:last-child { + border-right: none; +} + +#popup tr { + cursor: pointer; +} + +#popup tr:hover { + color: #dc322f; +} + +#popup tr:nth-child(even) { + background: #002B36; +} + +#popup tr:first-child { + background: #fff; + color: #002B36; + cursor: default; +} + +#popup tr:first-child:hover { + color: #002B36; +} diff --git a/tools/gui/src/JettyServer.scala b/tools/gui/src/JettyServer.scala new file mode 100644 index 0000000..d6024c2 --- /dev/null +++ b/tools/gui/src/JettyServer.scala @@ -0,0 +1,67 @@ +import java.net.MalformedURLException +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} + +import org.eclipse.jetty.server.{Request, Server} +import org.eclipse.jetty.server.handler._ + +import scala.util.{Failure, Success, Try} + +abstract class JettyServer(port: Int, staticFilesUrl: String) { + + private val handlerOfStatic = { + val handler = new ContextHandler("/") + val resourceHandler = new ResourceHandler + resourceHandler.setDirectoriesListed(true) + resourceHandler.setResourceBase(staticFilesUrl) + handler.setHandler(resourceHandler) + handler + } + + private val handlerOfApi = { + val handlerWrapper = new ContextHandler("/api/") + val handler = new AbstractHandler { + override def handle(target: String, + baseRequest: Request, + request: HttpServletRequest, + response: HttpServletResponse) = { + response.setContentType("application/json") + response.setCharacterEncoding("UTF-8") + + route(request.getMethod, target, request.getParameter) match { + case Success(result) => + response.getWriter.write(result) + case Failure(e: MalformedURLException) => + response.setStatus(HttpServletResponse.SC_NOT_FOUND) + response.getWriter.write(e.getMessage) + case Failure(e) => + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR) + response.getWriter.write(s"${e.getClass.getName}: ${e.getMessage}") + } + + baseRequest.setHandled(true) + } + } + handlerWrapper.setHandler(handler) + handlerWrapper + } + + private val server = { + val s = new Server(port) + val handlers = new HandlerCollection + handlers.setHandlers(Array(handlerOfStatic, handlerOfApi, new DefaultHandler)) + s.setHandler(handlers) + s + } + + def start() = { + server.start() + println(s"UI server started at localhost:$port") + } + + def stop() = { + server.stop() + println("UI server stopped.") + } + + def route(method: String, path: String, param: String => String): Try[String] +} diff --git a/tools/gui/src/Main.scala b/tools/gui/src/Main.scala new file mode 100644 index 0000000..d7a9f7d --- /dev/null +++ b/tools/gui/src/Main.scala @@ -0,0 +1,85 @@ +import java.io.{File, IOException} +import java.net.MalformedURLException + +import scala.io.Source +import scala.util.{Failure, Success, Try} +import scalaj.http.Http + +object Main { + + private val maven_host = "search.maven.org" + private val cbt_home = System.getenv("CBT_HOME") + + implicit class StringExtensionMethods(str: String) { + def /(s: String): String = str + File.separator + s + } + + val uiPort = 9080 + + def main(args: Array[String]) = launchUi(new File(args(0)), args(1)) + + def launchUi(projectDirectory: File, scalaMajorVersion: String): Unit = { + val staticBase = new File(cbt_home / "tools" / "gui" / "resources" / "web").toURI.toURL.toExternalForm + val server = new JettyServer(uiPort, staticBase) { + override def route(method: String, path: String, param: String => String) = (method, path) match { + case ("GET", "/cwd") => + Success(s"""["$projectDirectory"]""") + case ("POST", "/project") => + val name = param("name") + val defaultPackage = param("pack") + val dependencies = param("dependencies") + val flags = param("flags") + handleIoException { + new ProjectBuilder(name, defaultPackage, dependencies, flags, projectDirectory, scalaMajorVersion).build() + Success("[]") + } + case ("GET", "/dependency") => + val query = param("query") + handleIoException(handleMavenBadResponse(searchDependency(query))) + case ("GET", "/dependency/version") => + val group = param("group") + val artifact = param("artifact") + handleIoException(handleMavenBadResponse(searchDependencyVersion(group, artifact))) + case _ => + Failure(new MalformedURLException(s"Incorrect path: $path")) + } + } + server.start() + java.awt.Desktop.getDesktop.browse(new java.net.URI(s"http://localhost:$uiPort/")) + + println("Press Enter to stop UI server.") + while (Source.stdin.getLines().next().nonEmpty) {} + server.stop() + } + + private def searchDependency(query: String) = { + Http(s"http://$maven_host/solrsearch/select") + .param("q", query) + .param("rows", "30") + .param("wt", "json") + .asString.body + } + + private def searchDependencyVersion(group: String, artifact: String) = { + val query = s"""q=g:"$group"+AND+a:"$artifact"""" + Http(s"http://$maven_host/solrsearch/select?" + query) + .param("rows", "30") + .param("wt", "json") + .param("core", "gav") + .asString.body + } + + private def handleIoException(f: => Try[String]) = try f catch { + case e: IOException => + e.printStackTrace() + Failure(e) + } + + private def handleMavenBadResponse(result: String) = { + if (result.startsWith("{")) + Success(result) + else + Failure(new Exception(s"Bad response from $maven_host: $result")) + } + +} diff --git a/tools/gui/src/ProjectBuilder.scala b/tools/gui/src/ProjectBuilder.scala new file mode 100644 index 0000000..ceba376 --- /dev/null +++ b/tools/gui/src/ProjectBuilder.scala @@ -0,0 +1,149 @@ +import java.io.File +import java.nio.file._ + +import Main.StringExtensionMethods + +import scala.io.Source + +class ProjectBuilder( + name: String, + defaultPackage: String, + dependencyString: String, + flagsString: String, + location: File, + scalaMajorVersion: String +) { + + private val newLine = System.lineSeparator() + private val blankLine = newLine + newLine + private val templateDir = System.getenv("CBT_HOME") / "tools" / "gui" / "resources" / "template-project" + private val projectPath = location.getPath / name + private val buildDir = projectPath / "build" + private val mainDir = projectPath / "src" / "main" / "scala" / defaultPackage.split("\\.").reduce(_ / _) + private val testDir = projectPath / "src" / "test" / "scala" / defaultPackage.split("\\.").reduce(_ / _) + + private val dependencies = + if (dependencyString.isEmpty) + "" + else { + def parseDependencies(input: String): Seq[Dependency] = { + input.split(" ").map { x => + val xs = x.split("/") + Dependency(xs(0), xs(1), xs(2)) + } + } + + val list = parseDependencies(dependencyString) + val str = list.map(" " ++ _.serialized).mkString(s",$newLine") + s""" override def dependencies = { + | super.dependencies ++ Resolver(mavenCentral).bind( + |$str + | ) + | }""".stripMargin + } + + private val flags = { + val map = flagsString.split(" ").map { x => + val Array(a, b) = x.split("/") + (a, b) + }.toMap + Flags(map("readme") == "true", map("dotty") == "true", map("uberJar") == "true", map("wartremover") == "true") + } + + private val plugins = { + var content = "" + if (flags.dotty) + content += " with Dotty" + if (flags.uberJar) + content += " with UberJar" + if (flags.wartremover) + content += " with WartRemover" + content + } + + private val buildBuildDependencies = { + var content = "" + if (flags.uberJar) + content += " :+ plugins.uberJar" + if (flags.wartremover) + content += " :+ plugins.wartremover" + content + } + + def build(): Unit = { + new File(buildDir).mkdirs() + new File(mainDir).mkdirs() + new File(testDir).mkdirs() + addBuild() + if (buildBuildDependencies.nonEmpty) + addBuildBuild() + addMain() + addReadme(flags) + } + + private def writeTemplate(templatePath: String, targetPath: String, replacements: (String, String, String)*) = { + var content = Source.fromFile(templatePath).mkString + for (replacement <- replacements) + if (replacement._1.nonEmpty) + content = content.replace(replacement._3, replacement._2) + else + content = content.replace(replacement._3, "") + write(new File(targetPath), content, StandardOpenOption.CREATE_NEW) + } + + private def addBuild() = + writeTemplate( + templateDir / "build" / "build.scala", + buildDir / "build.scala", + (name, s""" override def projectName = "$name"$blankLine""", "##projectName##"), + (dependencyString, dependencies + blankLine, "##dependencies##"), + (plugins, plugins, "##with##") + ) + + private def addBuildBuild() = + writeTemplate( + templateDir / "build" / "build" / "build.scala", + buildDir / "build" / "build.scala", + (buildBuildDependencies, buildBuildDependencies, "##plus##") + ) + + private def addMain() = + writeTemplate( + templateDir / "src" / "main" / "scala" / "Main.scala", + mainDir / "Main.scala", + (defaultPackage, s"package $defaultPackage$blankLine", "##package##") + ) + + private def addReadme(flags: Flags) = { + if (flags.readme) { + val content = + if (name.nonEmpty) + s"# $name$blankLine" + else + "" + write(new File(projectPath / "readme.md"), content, StandardOpenOption.CREATE_NEW) + } + } + + private case class Dependency(group: String, artifact: String, version: String) { + val nameVersion = """^(.+)_(\d+\.\d+)$""".r + val (name, scalaVersion) = artifact match { + case nameVersion(n, v) => (n, Some(v)) + case str => (str, None) + } + + def serialized = + if (scalaVersion.contains(scalaMajorVersion)) + s"""ScalaDependency("$group", "$name", "$version")""" + else + s"""MavenDependency("$group", "$artifact", "$version")""" + } + + private def write(file: File, content: String, option: OpenOption) = { + file.getParentFile.mkdirs() + Files.write(file.toPath, content.getBytes, option) + } + + private case class Flags(readme: Boolean, dotty: Boolean, uberJar: Boolean, wartremover: Boolean) + +} |